Initial commit: learning network protocols
This commit is contained in:
commit
082867f9f2
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
13
jsconfig.json
Normal file
13
jsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
3113
package-lock.json
generated
Normal file
3113
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "networking-tutor",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@sveltejs/adapter-auto": "^6.1.1",
|
||||
"@sveltejs/kit": "^2.43.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/prettier": "^2.7.3",
|
||||
"eslint": "^9.38.0",
|
||||
"prettier": "^3.6.2",
|
||||
"svelte": "^5.39.5",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
11
src/app.html
Normal file
11
src/app.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
1
src/lib/assets/favicon.svg
Normal file
1
src/lib/assets/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
111
src/lib/components/BitEditor.svelte
Normal file
111
src/lib/components/BitEditor.svelte
Normal file
@ -0,0 +1,111 @@
|
||||
<script>
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let buffer = new Uint8Array([0x00]);
|
||||
export let onBufferChange;
|
||||
|
||||
const bits = writable([]);
|
||||
|
||||
$: {
|
||||
const value = buffer[0] || 0;
|
||||
const newBits = [];
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
newBits.push({
|
||||
position: i,
|
||||
isSet: !!(value & (1 << i))
|
||||
});
|
||||
}
|
||||
bits.set(newBits);
|
||||
}
|
||||
|
||||
function toggleBit(position) {
|
||||
const currentValue = buffer[0] || 0;
|
||||
const newValue = currentValue ^ (1 << position);
|
||||
const newBuffer = new Uint8Array([newValue]);
|
||||
onBufferChange(newBuffer);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bit-editor">
|
||||
<h4>Редактор битов:</h4>
|
||||
|
||||
<div class="bit-positions">
|
||||
<span class="label">Бит:</span>
|
||||
{#each [7,6,5,4,3,2,1,0] as pos}
|
||||
<span class="position">{pos}</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="bit-controls">
|
||||
<span class="label">Значение:</span>
|
||||
{#each $bits as bit}
|
||||
<button
|
||||
class:active={bit.isSet}
|
||||
class="bit-toggle"
|
||||
on:click={() => toggleBit(bit.position)}
|
||||
>
|
||||
{bit.isSet ? '1' : '0'}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="bit-help">
|
||||
<small>Кликните на бит, чтобы переключить 0/1</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bit-editor {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.bit-positions, .bit-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.position {
|
||||
padding: 8px 12px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bit-toggle {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #2196F3;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
min-width: 40px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.bit-toggle.active {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bit-toggle:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bit-help {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
274
src/lib/components/EthernetBuilder.svelte
Normal file
274
src/lib/components/EthernetBuilder.svelte
Normal file
@ -0,0 +1,274 @@
|
||||
<script>
|
||||
export let buffer;
|
||||
export let onBufferChange;
|
||||
|
||||
let destinationMAC = 'AA:BB:CC:DD:EE:FF';
|
||||
let sourceMAC = 'AA:BB:CC:DD:EE:FF';
|
||||
let etherType = '0800';
|
||||
let errors = { dest: '', src: '' };
|
||||
let currentChecksum = [0x00, 0x00, 0x00, 0x00];
|
||||
|
||||
function validateMAC(mac) {
|
||||
const macRegex = /^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$/;
|
||||
if (!macRegex.test(mac)) {
|
||||
return 'Неверный формат MAC или некорректные значения';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function calculateChecksum(headerBuffer) {
|
||||
//имитация контрольной суммы (на практике CRC32)
|
||||
let sum = 0;
|
||||
for (let i = 0; i < headerBuffer.length; i++) {
|
||||
sum = (sum + headerBuffer[i]) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 46; i++) {
|
||||
sum = (sum + 0x00) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
return [
|
||||
(sum >> 24) & 0xFF,
|
||||
(sum >> 16) & 0xFF,
|
||||
(sum >> 8) & 0xFF,
|
||||
sum & 0xFF
|
||||
];
|
||||
}
|
||||
|
||||
function updateBuffer() {
|
||||
const destError = validateMAC(destinationMAC);
|
||||
const srcError = validateMAC(sourceMAC);
|
||||
errors = { dest: destError, src: srcError };
|
||||
|
||||
if (destError || srcError) {
|
||||
onBufferChange(new Uint8Array(0));
|
||||
return;
|
||||
}
|
||||
|
||||
const headerBuffer = new Uint8Array(14);
|
||||
|
||||
const destBytes = destinationMAC.split(':').map(byte => parseInt(byte, 16));
|
||||
destBytes.forEach((byte, i) => headerBuffer[i] = byte);
|
||||
|
||||
const srcBytes = sourceMAC.split(':').map(byte => parseInt(byte, 16));
|
||||
srcBytes.forEach((byte, i) => headerBuffer[i + 6] = byte);
|
||||
|
||||
headerBuffer[12] = parseInt(etherType.substring(0, 2), 16);
|
||||
headerBuffer[13] = parseInt(etherType.substring(2, 4), 16);
|
||||
|
||||
currentChecksum = calculateChecksum(headerBuffer);
|
||||
|
||||
const fullBuffer = new Uint8Array(64);
|
||||
fullBuffer.set(headerBuffer, 0);
|
||||
for (let i = 14; i < 60; i++) {
|
||||
fullBuffer[i] = 0x00;
|
||||
}
|
||||
fullBuffer.set(new Uint8Array(currentChecksum), 60);
|
||||
|
||||
onBufferChange(fullBuffer);
|
||||
}
|
||||
|
||||
function handleMACInput(field, value) {
|
||||
const upperValue = value.toUpperCase();
|
||||
if (field === 'dest') {
|
||||
destinationMAC = upperValue;
|
||||
} else {
|
||||
sourceMAC = upperValue;
|
||||
}
|
||||
updateBuffer();
|
||||
}
|
||||
|
||||
function handleTypeChange(value) {
|
||||
etherType = value;
|
||||
updateBuffer();
|
||||
}
|
||||
|
||||
$: if (buffer) {
|
||||
//инициализация при первом рендере
|
||||
if (buffer.length === 0) {
|
||||
updateBuffer();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ethernet-builder">
|
||||
<h3>Конструктор Ethernet кадра</h3>
|
||||
|
||||
<div class="field">
|
||||
<label>Destination MAC:</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={destinationMAC}
|
||||
on:input={(e) => handleMACInput('dest', e.target.value)}
|
||||
placeholder="AA:BB:CC:DD:EE:FF"
|
||||
class:error={errors.dest}
|
||||
/>
|
||||
{#if errors.dest}
|
||||
<span class="error-text">{errors.dest}</span>
|
||||
{/if}
|
||||
<span class="hint">Адрес получателя</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Source MAC:</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={sourceMAC}
|
||||
on:input={(e) => handleMACInput('src', e.target.value)}
|
||||
placeholder="AA:BB:CC:DD:EE:FF"
|
||||
class:error={errors.src}
|
||||
/>
|
||||
{#if errors.src}
|
||||
<span class="error-text">{errors.src}</span>
|
||||
{/if}
|
||||
<span class="hint">Адрес отправителя</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>EtherType:</label>
|
||||
<select bind:value={etherType} on:change={(e) => handleTypeChange(e.target.value)}>
|
||||
<option value="0800">0x0800 - IPv4</option>
|
||||
<option value="0806">0x0806 - ARP</option>
|
||||
<option value="86DD">0x86DD - IPv6</option>
|
||||
</select>
|
||||
<span class="hint">Тип инкапсулированного протокола</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Data (46 байт):</label>
|
||||
<div class="data-field">
|
||||
<code>0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00</code>
|
||||
</div>
|
||||
<span class="hint">Полезная нагрузка (заполнена нулями)</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>FCS (Frame Check Sequence):</label>
|
||||
<div class="checksum-field">
|
||||
<code>
|
||||
{'0x' + currentChecksum.map(b => b.toString(16).padStart(2,'0')).join('').toUpperCase()}
|
||||
</code>
|
||||
</div>
|
||||
<span class="hint">Контрольная сумма (рассчитывается автоматически)</span>
|
||||
</div>
|
||||
|
||||
<div class="frame-preview">
|
||||
<h4>Структура кадра (64 байта):</h4>
|
||||
<div class="frame-layout">
|
||||
<div class="frame-field" style="background: #e3f2fd;">
|
||||
<span>Destination MAC</span>
|
||||
<small>6 bytes</small>
|
||||
</div>
|
||||
<div class="frame-field" style="background: #fff3e0;">
|
||||
<span>Source MAC</span>
|
||||
<small>6 bytes</small>
|
||||
</div>
|
||||
<div class="frame-field" style="background: #e8f5e8;">
|
||||
<span>EtherType</span>
|
||||
<small>2 bytes</small>
|
||||
</div>
|
||||
<div class="frame-field" style="background: #f3e5f5;">
|
||||
<span>Data</span>
|
||||
<small>46 bytes</small>
|
||||
</div>
|
||||
<div class="frame-field" style="background: #ffebee;">
|
||||
<span>FCS</span>
|
||||
<small>4 bytes</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ethernet-builder {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin: 15px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.field input, .field select {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.field input:focus, .field select:focus {
|
||||
border-color: #2196F3;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
border-color: #f44336 !important;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f44336;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.data-field, .checksum-field {
|
||||
background: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.frame-preview {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.frame-layout {
|
||||
display: flex;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.frame-field {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-right: 1px solid #ccc;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.frame-field:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.frame-field span {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.frame-field small {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
69
src/lib/components/HexViewer.svelte
Normal file
69
src/lib/components/HexViewer.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<script>
|
||||
export let data = new Uint8Array([]);
|
||||
export let highlightedBytes = [];
|
||||
|
||||
$: hexString = Array.from(data)
|
||||
.map((byte, index) => {
|
||||
const hex = byte.toString(16).padStart(2, '0').toUpperCase();
|
||||
return { hex, index };
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="hex-viewer">
|
||||
<div class="bytes">
|
||||
{#each hexString as {hex, index}}
|
||||
<span
|
||||
class:highlighted={highlightedBytes.includes(index)}
|
||||
class="byte"
|
||||
>
|
||||
{'0x'+hex}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Размер: {data.length} байт(а)
|
||||
{#if data.length === 1}
|
||||
| Десятичное: {data[0]} | Двоичное: {data[0].toString(2).padStart(8, '0')}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hex-viewer {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.bytes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.byte {
|
||||
padding: 4px 8px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.byte.highlighted {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border-color: #388E3C;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
143
src/lib/components/LessonLayout.svelte
Normal file
143
src/lib/components/LessonLayout.svelte
Normal file
@ -0,0 +1,143 @@
|
||||
<script>
|
||||
import { lessons } from "$lib/data/lessons";
|
||||
|
||||
export let lesson;
|
||||
</script>
|
||||
|
||||
<div class="lesson-layout">
|
||||
<header class="lesson-header">
|
||||
<div class="header-content">
|
||||
<a href="/" class="back-button">← Назад к урокам</a>
|
||||
<h1>{lesson.title}</h1>
|
||||
<div class="meta">
|
||||
<span class="category">{lesson.category}</span>
|
||||
<span class="difficulty">{lesson.difficulty}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="lesson-body">
|
||||
<section class="theory-section">
|
||||
<h2>Теория</h2>
|
||||
<div class="theory-content">
|
||||
{@html lesson.theory}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="practice-section">
|
||||
<h2>Практика</h2>
|
||||
<div class="objective">
|
||||
<h2>Задание</h2>
|
||||
<p>{lesson.objective}</p>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lesson-layout {
|
||||
min-height: 100vh;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.lesson-header {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.lesson-header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category, .difficulty {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.lesson-body {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.theory-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/*секция теории*/
|
||||
.theory-section h2 {
|
||||
color: #2196F3;
|
||||
border-bottom: 2px solid #2196F3;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.theory-content {
|
||||
line-height: 1.6;
|
||||
background: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #51adf8;
|
||||
}
|
||||
|
||||
.theory-content {
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/*секция практики*/
|
||||
.practice-section h2 {
|
||||
color: rgb(234, 88, 9);
|
||||
border-bottom: 2px solid #FF9800;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
<!--.objective {
|
||||
background: #fffcf6;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #FF9800;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.objective p {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
}-->
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.lesson-body {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
src/lib/components/Notification.svelte
Normal file
122
src/lib/components/Notification.svelte
Normal file
@ -0,0 +1,122 @@
|
||||
<script>
|
||||
export let message = '';
|
||||
export let type = 'success'; //'success' или 'error'
|
||||
export let duration = 3000;
|
||||
export let onClose;
|
||||
|
||||
let isVisible = false;
|
||||
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
isVisible = true;
|
||||
}, 100);
|
||||
|
||||
if (duration > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
close();
|
||||
}, duration);
|
||||
|
||||
onDestroy(() => clearTimeout(timer));
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
isVisible = false;
|
||||
setTimeout(() => {
|
||||
onClose && onClose();
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<div class="notification-container">
|
||||
<div class:visible={isVisible} class="notification {type}">
|
||||
<div class="notification-content">
|
||||
{#if type === 'success'}
|
||||
<span class="icon">✅</span>
|
||||
{:else}
|
||||
<span class="icon">❌</span>
|
||||
{/if}
|
||||
<span class="text">{message}</span>
|
||||
</div>
|
||||
<button class="close-button" on:click={close}>×</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
transform: translateY(-100px);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.notification.visible {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
border-left: 4px solid #4CAF50;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
181
src/lib/data/lessons.js
Normal file
181
src/lib/data/lessons.js
Normal file
@ -0,0 +1,181 @@
|
||||
export const lessons = [
|
||||
{
|
||||
id: 1,
|
||||
slug: 'bit-manipulation',
|
||||
title: 'Бит и байт - фундаментальные понятия',
|
||||
category: 'Основы',
|
||||
difficulty: 'Начинающий',
|
||||
component: 'BitEditor',
|
||||
theory: `
|
||||
<div style="max-width: 1000px; margin: 0 auto; line-height: 1.6;">
|
||||
<h2 style="color: #2196F3; border-bottom: 2px solid #2196F3; padding-bottom: 10px;">
|
||||
Бит и байт - основа сетевых протоколов
|
||||
</h2>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Что такое бит?</h3>
|
||||
<ul>
|
||||
<li><strong>Бит</strong> - минимальная единица информации (0 или 1)</li>
|
||||
<li>Основа всей компьютерной техники</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Что такое байт?</h3>
|
||||
<ul>
|
||||
<li><strong>Байт</strong> = 8 битов</li>
|
||||
<li>Пример: <code>01011011</code> = 1 байт</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Двоичная арифметика:</h3>
|
||||
<p>Каждая позиция бита - это степень двойки:</p>
|
||||
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #4CAF50;">
|
||||
<code>10101010</code> = 1×2⁷ + 0×2⁶ + 1×2⁵ + 0×2⁴ + 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 1×128 + 0×64 + 1×32 + 0×16 + 1×8 + 0×4 + 1×2 + 0×1 = 170
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Шестнадцатеричная система (Hex):</h3>
|
||||
<p>В обычной жизни мы используем десятичную систему (0-9). В шестнадцатеричной системе 16 цифр:</p>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||
<table style="flex: 1; border-collapse: collapse; border: 1px solid #ddd;">
|
||||
<tr><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Десятичная</th><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Hex</th><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Двоичная</th></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0000</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0001</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">2</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">2</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0010</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">3</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">3</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0011</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">4</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">4</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0100</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">5</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">5</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0101</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">6</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">6</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0110</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">7</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">7</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">0111</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">8</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">8</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1000</td></tr>
|
||||
</table>
|
||||
|
||||
<table style="flex: 1; border-collapse: collapse; border: 1px solid #ddd;">
|
||||
<tr><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Десятичная</th><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Hex</th><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Двоичная</th></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">9</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">9</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1001</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">10</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">A</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1010</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">11</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">B</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1011</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">12</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">C</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1100</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">13</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">D</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1101</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">14</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">E</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1110</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">15</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">F</td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">1111</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Как преобразовать двоичное в Hex:</h3>
|
||||
<p>Разбиваем байт на две половинки по 4 бита и преобразуем каждую отдельно:</p>
|
||||
|
||||
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #4CAF50;">
|
||||
<p><strong>Пример: <code>11010011</code></strong></p>
|
||||
<ol>
|
||||
<li>Разбиваем на две группы: <code>1101</code> и <code>0011</code></li>
|
||||
<li><code>1101</code> = 1×8 + 1×4 + 0×2 + 1×1 = 13 = <code>D</code></li>
|
||||
<li><code>0011</code> = 0×8 + 0×4 + 1×2 + 1×1 = 3 = <code>3</code></li>
|
||||
<li>Результат: <code>0xD3</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<p>Удобнее читать <code>0xA1</code>, чем <code>10100001</code>!</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
objective: 'Установите биты так, чтобы получить число 184 (0xB8)',
|
||||
initialBuffer: new Uint8Array([0x00]),
|
||||
validate: (buffer) => {
|
||||
return buffer[0] === 0xB8;
|
||||
},
|
||||
hints: [
|
||||
'184 в двоичной системе = 10111000',
|
||||
'Каждый бит представляет степень двойки',
|
||||
'Попробуйте установить биты в позициях 7,5,4,3'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
slug: 'ethernet-frame',
|
||||
title: 'Структура Ethernet кадра',
|
||||
category: 'Ethernet',
|
||||
difficulty: 'Начинающий',
|
||||
component: 'EthernetBuilder',
|
||||
|
||||
theory: `
|
||||
<div style="max-width: 1000px; margin: 0 auto; line-height: 1.6;">
|
||||
<h2 style="color: #2196F3; border-bottom: 2px solid #2196F3; padding-bottom: 10px;">
|
||||
Ethernet кадр
|
||||
</h2>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p><strong>Ethernet II и IEEE 802.3</strong> - это два стандарта Ethernet-кадров. Они идентичны по структуре, кроме одного поля: в Ethernet II это поле Type (указывает тип протокола), а в IEEE 802.3 это поле Length (указывает длину поля данных в байтах).</p>
|
||||
<p> На практике Ethernet II используется чаще, потому что поле Type более полезно для определения, какой протокол находится внутри кадра.</p>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Структура кадра Ethernet II:</h3>
|
||||
|
||||
<div style="background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #4CAF50; font-family: 'Courier New', monospace; font-size: 0.9em;">
|
||||
<div style="display: flex; margin: 5px 0;">
|
||||
<span style="min-width: 140px; font-weight: bold;">Destination MAC:</span>
|
||||
<span>6 байт | MAC адрес получателя</span>
|
||||
</div>
|
||||
<div style="display: flex; margin: 5px 0;">
|
||||
<span style="min-width: 140px; font-weight: bold;">Source MAC:</span>
|
||||
<span>6 байт | MAC адрес отправителя</span>
|
||||
</div>
|
||||
<div style="display: flex; margin: 5px 0;">
|
||||
<span style="min-width: 140px; font-weight: bold;">Type:</span>
|
||||
<span>2 байта | Тип протокола</span>
|
||||
</div>
|
||||
<div style="display: flex; margin: 5px 0;">
|
||||
<span style="min-width: 140px; font-weight: bold;">Data:</span>
|
||||
<span>46-1500 байт | Полезная нагрузка</span>
|
||||
</div>
|
||||
<div style="display: flex; margin: 5px 0;">
|
||||
<span style="min-width: 140px; font-weight: bold;">FCS:</span>
|
||||
<span>4 байта | Контрольная сумма</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: #f9fcff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<h3 style="color: #1976D2; margin-top: 0;">Типы протоколов (EtherType):</h3>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin: 15px 0;">
|
||||
<table style="flex: 1; border-collapse: collapse; border: 1px solid #ddd;">
|
||||
<tr><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Код</th><th style="background: #e3f2fd; padding: 8px; border: 1px solid #ddd;">Протокол</th></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;"><code>0x0800</code></td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">IPv4</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;"><code>0x0806</code></td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">ARP</td></tr>
|
||||
<tr><td style="padding: 6px; border: 1px solid #ddd; text-align: center;"><code>0x86DD</code></td><td style="padding: 6px; border: 1px solid #ddd; text-align: center;">IPv6</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
objective: 'Отправьте широковещательный ARP запрос с MAC адресом вашего компьютера 12:34:56:78:9A:BC',
|
||||
initialBuffer: new Uint8Array(64),
|
||||
validate: (buffer) => {
|
||||
const isBroadcast = buffer.slice(0, 6).every(byte => byte === 0xFF);
|
||||
|
||||
const expectedSource = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC];
|
||||
const isSourceCorrect = buffer.slice(6, 12).every((byte, i) => byte === expectedSource[i]);
|
||||
|
||||
const etherType = (buffer[12] << 8) | buffer[13];
|
||||
const isARP = etherType === 0x0806;
|
||||
|
||||
return isBroadcast && isSourceCorrect && isARP;
|
||||
},
|
||||
|
||||
hints: [
|
||||
'Используйте широковещательный адрес FF:FF:FF:FF:FF:FF',
|
||||
'Установите EtherType 0x0806 для ARP',
|
||||
'MAC адрес отправителя должен быть 12:34:56:78:9A:BC'
|
||||
]
|
||||
}
|
||||
];
|
||||
1
src/lib/index.js
Normal file
1
src/lib/index.js
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
22
src/lib/utils/storage.js
Normal file
22
src/lib/utils/storage.js
Normal file
@ -0,0 +1,22 @@
|
||||
export const progressStorage = {
|
||||
getProgress() {
|
||||
if (typeof window === 'undefined') return {};
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('networking-progress') || '{}');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
setProgress(lessonId, completed) {
|
||||
if (typeof window === 'undefined') return;
|
||||
const progress = this.getProgress();
|
||||
progress[lessonId] = completed;
|
||||
localStorage.setItem('networking-progress', JSON.stringify(progress));
|
||||
},
|
||||
|
||||
isCompleted(lessonId) {
|
||||
const progress = this.getProgress();
|
||||
return !!progress[lessonId];
|
||||
}
|
||||
};
|
||||
11
src/routes/+layout.svelte
Normal file
11
src/routes/+layout.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{@render children?.()}
|
||||
107
src/routes/+page.svelte
Normal file
107
src/routes/+page.svelte
Normal file
@ -0,0 +1,107 @@
|
||||
<script>
|
||||
import { lessons } from '$lib/data/lessons';
|
||||
import { progressStorage } from '$lib/utils/storage';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let progress = {};
|
||||
|
||||
onMount(() => {
|
||||
progress = progressStorage.getProgress();
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>Обучение сетевым протоколам</h1>
|
||||
|
||||
<div class="lessons-list">
|
||||
{#each lessons as lesson}
|
||||
<a href="/lessons/{lesson.slug}" class="lesson-card">
|
||||
<div class="lesson-header">
|
||||
<h3>{lesson.title}</h3>
|
||||
{#if progress[lesson.id]}
|
||||
<span class="completed-badge">✅ Пройден</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p>{lesson.objective}</p>
|
||||
<div class="lesson-footer">
|
||||
<span class="difficulty">{lesson.difficulty}</span>
|
||||
<span class="category">{lesson.category}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #2196F3;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.lessons-list {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lesson-card {
|
||||
display: block;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.lesson-card:hover {
|
||||
border-color: #2196F3;
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.lesson-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.lesson-card h3 {
|
||||
margin: 0;
|
||||
color: #2196F3;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.completed-badge {
|
||||
background: #E8F5E8;
|
||||
color: #2E7D32;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.lesson-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.difficulty, .category {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.difficulty {
|
||||
background: #E3F2FD;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.category {
|
||||
background: #F3E5F5;
|
||||
color: #7B1FA2;
|
||||
}
|
||||
</style>
|
||||
14
src/routes/lessons/[slug]/+page.js
Normal file
14
src/routes/lessons/[slug]/+page.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { lessons } from '$lib/data/lessons';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export function load({ params }) {
|
||||
const lesson = lessons.find(l => l.slug === params.slug);
|
||||
|
||||
if (!lesson) {
|
||||
throw error(404, 'Урок не найден');
|
||||
}
|
||||
|
||||
return {
|
||||
lesson
|
||||
};
|
||||
}
|
||||
194
src/routes/lessons/[slug]/+page.svelte
Normal file
194
src/routes/lessons/[slug]/+page.svelte
Normal file
@ -0,0 +1,194 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { lessons } from '$lib/data/lessons';
|
||||
import { progressStorage } from '$lib/utils/storage';
|
||||
import { onMount, afterUpdate } from 'svelte';
|
||||
|
||||
import HexViewer from '$lib/components/HexViewer.svelte';
|
||||
import LessonLayout from '$lib/components/LessonLayout.svelte';
|
||||
import Notification from '$lib/components/Notification.svelte';
|
||||
|
||||
import BitEditor from '$lib/components/BitEditor.svelte';
|
||||
import EthernetBuilder from '$lib/components/EthernetBuilder.svelte';
|
||||
|
||||
let currentBuffer;
|
||||
let isCompleted = false;
|
||||
let showHint = false;
|
||||
let currentHintIndex = 0;
|
||||
let notification = { show: false, message: '', type: 'success' };
|
||||
let lesson = null;
|
||||
let lessonComponent = null;
|
||||
|
||||
afterUpdate(() => {
|
||||
if ($page.params.slug) {
|
||||
lesson = lessons.find(l => l.slug === $page.params.slug);
|
||||
|
||||
if (lesson) {
|
||||
lessonComponent = getLessonComponent(lesson.component);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getLessonComponent(componentName) {
|
||||
switch(componentName) {
|
||||
case 'BitEditor':
|
||||
return BitEditor;
|
||||
case 'EthernetBuilder':
|
||||
return EthernetBuilder;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
//инициализация при загрузке на клиенте
|
||||
if ($page.params.slug) {
|
||||
lesson = lessons.find(l => l.slug === $page.params.slug);
|
||||
|
||||
if (lesson) {
|
||||
lessonComponent = getLessonComponent(lesson.component);
|
||||
currentBuffer = new Uint8Array(lesson.initialBuffer);
|
||||
isCompleted = progressStorage.isCompleted(lesson.id.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function handleBufferChange(newBuffer) {
|
||||
currentBuffer = newBuffer;
|
||||
}
|
||||
|
||||
function showNextHint() {
|
||||
if (!lesson) return;
|
||||
showHint = true;
|
||||
currentHintIndex = (currentHintIndex + 1) % lesson.hints.length;
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'success') {
|
||||
notification = { show: true, message, type };
|
||||
}
|
||||
|
||||
function checkSolution() {
|
||||
if (!lesson) return;
|
||||
const isValid = lesson.validate(currentBuffer);
|
||||
|
||||
if (isValid) {
|
||||
isCompleted = true;
|
||||
progressStorage.setProgress(lesson.id.toString(), true);
|
||||
showNotification('Правильно! 🎉 Задание выполнено', 'success');
|
||||
} else {
|
||||
showNotification('Пока неверно. Попробуйте еще раз!', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function hideNotification() {
|
||||
notification.show = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if lesson}
|
||||
<LessonLayout {lesson}>
|
||||
<div class="lesson-content">
|
||||
<svelte:component
|
||||
this={lessonComponent}
|
||||
bind:buffer={currentBuffer}
|
||||
onBufferChange={handleBufferChange}
|
||||
/>
|
||||
|
||||
{#if currentBuffer}
|
||||
<HexViewer data={currentBuffer} />
|
||||
{/if}
|
||||
|
||||
<div class="controls">
|
||||
<button on:click={checkSolution} class="check-button">
|
||||
Проверить решение
|
||||
</button>
|
||||
|
||||
<button on:click={showNextHint} class="hint-button">
|
||||
Подсказка
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showHint && lesson.hints[currentHintIndex]}
|
||||
<div class="hint">
|
||||
💡 {lesson.hints[currentHintIndex]}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isCompleted}
|
||||
<div class="completion-badge">
|
||||
✅ Урок завершен!
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</LessonLayout>
|
||||
{:else}
|
||||
<p>Урок не найден</p>
|
||||
{/if}
|
||||
|
||||
{#if notification.show}
|
||||
<Notification
|
||||
message={notification.message}
|
||||
type={notification.type}
|
||||
onClose={hideNotification}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.lesson-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.check-button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.check-button:hover {
|
||||
background: #388E3C;
|
||||
}
|
||||
|
||||
.hint-button {
|
||||
background: #FF9800;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.hint-button:hover {
|
||||
background: #F57C00;
|
||||
}
|
||||
|
||||
.hint {
|
||||
background: #FFF3E0;
|
||||
border-left: 4px solid #FF9800;
|
||||
padding: 12px 16px;
|
||||
margin: 16px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.completion-badge {
|
||||
background: #E8F5E8;
|
||||
border: 2px solid #4CAF50;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #2E7D32;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
3
static/robots.txt
Normal file
3
static/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
13
svelte.config.js
Normal file
13
svelte.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
6
vite.config.js
Normal file
6
vite.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user