1
0
Fork 0

track user progress

This commit is contained in:
koplenov 2026-04-05 11:31:00 +03:00
parent 3c92fbddca
commit b61a669344
3 changed files with 221 additions and 29 deletions

View file

@ -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<string, boolean> => {
try { return JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}'); }
catch { return {}; }
};
const App: React.FC = () => {
const [overlay, setOverlay] = useState<OverlayType>(null);
const [completedItems, setCompletedItems] = useState<Record<string, boolean>>(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<string | number, OverlayType> = {
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 = () => {
<div style={{ width: '100vw', height: '100vh', overflow: 'hidden', position: 'relative' }}>
<CybersecMenu
onSelectLevel={handleSelectLevel}
onOpenWiki={() => 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 = () => {
}}> ЗАКРЫТЬ</button>
<div style={{ clear: 'both' }} />
{overlay === 'case1' && <Case1Desktop onComplete={close} />}
{overlay === 'case2' && <Case2Desktop onComplete={close} />}
{overlay === 'case3' && <Case3Desktop onComplete={close} />}
{overlay === 'case4' && <Case4Desktop onComplete={close} />}
{overlay === 'case5' && <Case5Desktop onComplete={close} />}
{overlay === 'case1' && <Case1Desktop onComplete={() => { markComplete('case1'); close(); }} />}
{overlay === 'case2' && <Case2Desktop onComplete={() => { markComplete('case2'); close(); }} />}
{overlay === 'case3' && <Case3Desktop onComplete={() => { markComplete('case3'); close(); }} />}
{overlay === 'case4' && <Case4Desktop onComplete={() => { markComplete('case4'); close(); }} />}
{overlay === 'case5' && <Case5Desktop onComplete={() => { markComplete('case5'); close(); }} />}
{overlay === 'deepfake' && <ChatterboxTTS />}
{overlay === 'quiz' && <MyQuize />}
{overlay === 'wiki' && <CyberSecurityArticle />}

View file

@ -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;

View file

@ -12,9 +12,25 @@ interface CybersecMenuProps {
onSelectLevel?: (level: number | string) => void;
onOpenWiki?: () => void;
onOpenQuiz?: () => void;
completedItems?: Record<string, boolean>;
}
const CybersecMenu: React.FC<CybersecMenuProps> = ({ 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<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki, onOpenQuiz, completedItems = {} }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animFrameRef = useRef<number>(0);
const smokeOffsetRef = useRef(0);
@ -22,6 +38,7 @@ const CybersecMenu: React.FC<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
const [notification, setNotification] = useState<Notification | null>(null);
const notifIdRef = useRef(0);
const notifTimerRef = useRef<ReturnType<typeof setTimeout> | 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<CybersecMenuProps> = ({ 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<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
<div className="csm-header">
<h1 ref={titleRef}>CYBERSEC TRAINING</h1>
<div className="csm-subtitle">[ TACTICAL SECURITY SIMULATION ]</div>
<div className="csm-progress-bar">
{(() => {
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 (
<>
<div className="csm-progress-label">
ПРОГРЕСС: <span className="csm-progress-count">{done}/{total}</span>
</div>
<div className="csm-progress-track">
<div className="csm-progress-fill" style={{ width: `${(done / total) * 100}%` }} />
</div>
</>
);
})()}
</div>
</div>
{/* Character */}
@ -325,39 +369,55 @@ const CybersecMenu: React.FC<CybersecMenuProps> = ({ onSelectLevel, onOpenWiki,
{/* Right Side Menu */}
<div className="csm-right-menu">
{menuItems.map(item => (
<div key={String(item.level)} className={`csm-menu-item ${item.cls}`} onClick={() => selectLevel(item.level)}>
{menuItems.map(item => {
const isDone = completedItems[String(item.level)];
return (
<div key={String(item.level)} className={`csm-menu-item ${item.cls}${isDone ? ' csm-done' : ''}`} onClick={() => selectLevel(item.level)}>
<div className="csm-hexagon">
<svg className="csm-hexagon-bg" viewBox="0 0 70 80">
<polygon points="35,2 66,20 66,58 35,76 4,58 4,20"/>
</svg>
<span className="csm-hexagon-icon">{item.icon}</span>
{isDone && <span className="csm-done-badge"></span>}
</div>
<div className="csm-tooltip">
<div className="csm-tooltip-title">{item.title}</div>
<div className="csm-tooltip-desc">{item.desc}</div>
</div>
</div>
))}
);
})}
</div>
{/* Bottom Container */}
<div className="csm-bottom-container">
<div className="csm-bottom-block" onClick={openWiki}>
<div className={`csm-bottom-block${completedItems['wiki'] ? ' csm-done' : ''}`} onClick={openWiki}>
<div className="csm-block-shape">
<span className="csm-block-icon">📖</span>
<span className="csm-block-text">WIKI</span>
{completedItems['wiki'] && <span className="csm-block-done"></span>}
</div>
<div className="csm-block-tooltip">База знаний по безопастности</div>
<div className="csm-block-tooltip">База знаний по безопасности</div>
</div>
<div className="csm-bottom-block" onClick={openQuiz}>
<div className={`csm-bottom-block${completedItems['quiz'] ? ' csm-done' : ''}`} onClick={openQuiz}>
<div className="csm-block-shape">
<span className="csm-block-icon"></span>
<span className="csm-block-text">QUIZ</span>
{completedItems['quiz'] && <span className="csm-block-done"></span>}
</div>
<div className="csm-block-tooltip">Проверь свои знания!</div>
</div>
</div>
{/* Ironic ticker */}
<div className="csm-ticker">
<span className="csm-ticker-label">// LIVE FEED</span>
<div className="csm-ticker-track">
<span key={tickerIdx} className="csm-ticker-msg">
{TICKER_MESSAGES[tickerIdx]}
</span>
</div>
</div>
</div>
{/* Notification */}