diff --git a/src/App.tsx b/src/App.tsx index 69eeb5f..54c75bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,18 +13,30 @@ export type WallpaperType = 'xp' | 'win7' | 'win10'; type OverlayType = 'case1' | 'case2' | 'case3' | 'case4' | 'case5' | 'deepfake' | 'quiz' | 'wiki' | null; +const PROGRESS_KEY = 'cybersec_progress'; + +const loadProgress = (): Record => { + try { return JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}'); } + catch { return {}; } +}; + const App: React.FC = () => { const [overlay, setOverlay] = useState(null); + const [completedItems, setCompletedItems] = useState>(loadProgress); + + const markComplete = (key: string) => { + setCompletedItems(prev => { + const next = { ...prev, [key]: true }; + localStorage.setItem(PROGRESS_KEY, JSON.stringify(next)); + return next; + }); + }; const close = () => setOverlay(null); const handleSelectLevel = (level: number | string) => { const map: Record = { - 1: 'case1', - 2: 'case2', - 3: 'case3', - 4: 'case4', - 5: 'case5', + 1: 'case1', 2: 'case2', 3: 'case3', 4: 'case4', 5: 'case5', 'deepfake': 'deepfake', }; setTimeout(() => setOverlay(map[level] ?? null), 1200); @@ -34,8 +46,9 @@ const App: React.FC = () => {
setTimeout(() => setOverlay('wiki'), 1200)} - onOpenQuiz={() => setTimeout(() => setOverlay('quiz'), 1200)} + onOpenWiki={() => { markComplete('wiki'); setTimeout(() => setOverlay('wiki'), 1200); }} + onOpenQuiz={() => { markComplete('quiz'); setTimeout(() => setOverlay('quiz'), 1200); }} + completedItems={completedItems} /> {overlay && ( @@ -54,11 +67,11 @@ const App: React.FC = () => { }}>✕ ЗАКРЫТЬ
- {overlay === 'case1' && } - {overlay === 'case2' && } - {overlay === 'case3' && } - {overlay === 'case4' && } - {overlay === 'case5' && } + {overlay === 'case1' && { markComplete('case1'); close(); }} />} + {overlay === 'case2' && { markComplete('case2'); close(); }} />} + {overlay === 'case3' && { markComplete('case3'); close(); }} />} + {overlay === 'case4' && { markComplete('case4'); close(); }} />} + {overlay === 'case5' && { markComplete('case5'); close(); }} />} {overlay === 'deepfake' && } {overlay === 'quiz' && } {overlay === 'wiki' && } diff --git a/src/cases/CybersecMenu.css b/src/cases/CybersecMenu.css index c95f6f2..c2e47b2 100644 --- a/src/cases/CybersecMenu.css +++ b/src/cases/CybersecMenu.css @@ -384,6 +384,7 @@ justify-content: center; gap: 10px; position: relative; + position: relative; transition: all 0.2s ease; } .csm-block-shape::before { @@ -459,6 +460,124 @@ border-top-color: var(--csm-magenta); } +/* Progress bar */ +.csm-progress-bar { + margin-top: 12px; +} +.csm-progress-label { + font-family: 'Share Tech Mono', monospace; + font-size: 11px; + color: #555; + letter-spacing: 2px; + margin-bottom: 6px; +} +.csm-progress-count { + color: var(--csm-green); +} +.csm-progress-track { + width: 220px; + height: 3px; + background: rgba(0,255,255,0.08); + border-radius: 2px; + overflow: hidden; + margin: 0 auto; +} +.csm-progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--csm-green), var(--csm-cyan)); + border-radius: 2px; + transition: width 0.6s ease; + box-shadow: 0 0 8px rgba(0,255,65,0.6); +} + +/* Done badge on hexagon */ +.csm-done-badge { + position: absolute; + top: -4px; + right: -4px; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--csm-green); + color: #000; + font-size: 10px; + font-weight: 900; + display: flex; + align-items: center; + justify-content: center; + z-index: 3; + box-shadow: 0 0 8px rgba(0,255,65,0.8); + font-family: 'Orbitron', monospace; +} + +/* Done state dims the hexagon border to green */ +.csm-done .csm-hexagon-bg { + stroke: var(--csm-green); + filter: drop-shadow(0 0 6px rgba(0,255,65,0.4)); +} + +/* Done badge on wiki/quiz blocks */ +.csm-block-done { + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--csm-green); + color: #000; + font-size: 11px; + font-weight: 900; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 8px rgba(0,255,65,0.8); + font-family: 'Orbitron', monospace; +} + +/* Ironic ticker */ +.csm-ticker { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 32px; + background: rgba(0,0,0,0.9); + border-top: 1px solid rgba(255,0,64,0.3); + display: flex; + align-items: center; + overflow: hidden; + z-index: 20; +} +.csm-ticker-label { + font-family: 'Orbitron', monospace; + font-size: 9px; + color: #FF0040; + letter-spacing: 1px; + padding: 0 12px; + white-space: nowrap; + border-right: 1px solid rgba(255,0,64,0.3); + flex-shrink: 0; +} +.csm-ticker-track { + flex: 1; + overflow: hidden; + padding: 0 16px; + display: flex; + align-items: center; +} +.csm-ticker-msg { + font-family: 'Share Tech Mono', monospace; + font-size: 12px; + color: #aaa; + white-space: nowrap; + animation: csm-ticker-fade 0.4s ease; +} +@keyframes csm-ticker-fade { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + /* Notification */ .csm-notification { position: fixed; diff --git a/src/cases/CybersecMenu.tsx b/src/cases/CybersecMenu.tsx index f9debbd..393c001 100644 --- a/src/cases/CybersecMenu.tsx +++ b/src/cases/CybersecMenu.tsx @@ -12,9 +12,25 @@ interface CybersecMenuProps { onSelectLevel?: (level: number | string) => void; onOpenWiki?: () => void; onOpenQuiz?: () => void; + completedItems?: Record; } -const CybersecMenu: React.FC = ({ onSelectLevel, onOpenWiki, onOpenQuiz }) => { +const TICKER_MESSAGES = [ + '👤 Кто-то только что открыл вложение из письма «Вы выиграли iPhone»...', + '🔴 Иван Иваныч ввёл пароль на фишинговом сайте. Снова.', + '⚠️ Целых 5 минут никто не лажал! Ам нет, поспешил..', + '💾 Кто-то скачал ТелеЛитр с сайта telelitr-skachat.ru', + '📶 Неизвестный подключился к открытому WiFi «FREE_COFFEE_HACKER»', + '🛒 Пользователь ввёл данные карты на сайте yandex-market-shop.ru.biz', + '🔑 Сисадмин сменил пароль с «123456» на «1234567». Надёжно!', + '🎭 Голосовой дипфейк гендиректора попросил перевести 3 млн. Перевели.', + '💻 Бухгалтерия обновила Windows XP... до Windows XP SP3. Прогресс!', + '📧 Оп, Иван Иваныч слил данные — а сольёшь ли их ты?', + '🚨 Кто-то скомпрометировал все рабочие данные. Понедельник удался.', + '🔓 Пароль «qwerty123» снова признан самым популярным в офисе.', +]; + +const CybersecMenu: React.FC = ({ onSelectLevel, onOpenWiki, onOpenQuiz, completedItems = {} }) => { const canvasRef = useRef(null); const animFrameRef = useRef(0); const smokeOffsetRef = useRef(0); @@ -22,6 +38,7 @@ const CybersecMenu: React.FC = ({ onSelectLevel, onOpenWiki, const [notification, setNotification] = useState(null); const notifIdRef = useRef(0); const notifTimerRef = useRef | null>(null); + const [tickerIdx, setTickerIdx] = useState(0); const showNotification = useCallback((title: string, message: string) => { if (notifTimerRef.current) clearTimeout(notifTimerRef.current); @@ -57,6 +74,14 @@ const CybersecMenu: React.FC = ({ onSelectLevel, onOpenWiki, onOpenQuiz?.(); }, [showNotification, onOpenQuiz]); + // Ticker cycling + useEffect(() => { + const id = setInterval(() => { + setTickerIdx(i => (i + 1) % TICKER_MESSAGES.length); + }, 8000); + return () => clearInterval(id); + }, []); + // Keyboard navigation useEffect(() => { const handleKey = (e: KeyboardEvent) => { @@ -236,6 +261,25 @@ const CybersecMenu: React.FC = ({ onSelectLevel, onOpenWiki,

CYBERSEC TRAINING

[ TACTICAL SECURITY SIMULATION ]
+
+ {(() => { + const total = menuItems.length + 2; // cases + deepfake + wiki + quiz + const done = [ + ...menuItems.map(m => String(m.level)), + 'wiki', 'quiz', + ].filter(k => completedItems[k]).length; + return ( + <> +
+ ПРОГРЕСС: {done}/{total} +
+
+
+
+ + ); + })()} +
{/* Character */} @@ -325,39 +369,55 @@ const CybersecMenu: React.FC = ({ onSelectLevel, onOpenWiki, {/* Right Side Menu */}
- {menuItems.map(item => ( -
selectLevel(item.level)}> -
- - - - {item.icon} + {menuItems.map(item => { + const isDone = completedItems[String(item.level)]; + return ( +
selectLevel(item.level)}> +
+ + + + {item.icon} + {isDone && } +
+
+
{item.title}
+
{item.desc}
+
-
-
{item.title}
-
{item.desc}
-
-
- ))} + ); + })}
{/* Bottom Container */}
-
+
📖 WIKI + {completedItems['wiki'] && }
-
База знаний по безопастности
+
База знаний по безопасности
-
+
QUIZ + {completedItems['quiz'] && }
Проверь свои знания!
+ + {/* Ironic ticker */} +
+ // LIVE FEED +
+ + {TICKER_MESSAGES[tickerIdx]} + +
+
{/* Notification */}