Mistrz Kierownicy - Arttube
#drive-eval-standalone { background: #f8fafc; color: #1e293b; line-height: 1.5; } #drive-eval-standalone * { box-sizing: border-box; } #drive-eval-standalone button { cursor: pointer; transition: all 0.2s; border: none; outline: none; } #drive-eval-standalone button:active { transform: scale(0.95); } @media print { .no-print { display: none !important; } #drive-eval-standalone { background: white !important; } } (function() { const React = window.React; const ReactDOM = window.ReactDOM; const { useState, useEffect, createElement: h } = React; const { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } = window.Recharts; const STORAGE_KEY = 'drive_eval_final_data'; const EXAM_COST = 240; const CATEGORIES = [ { id: 'P1', poz: '1', text: 'Przygotowanie do jazdy' }, { id: 'P2', poz: '2', text: 'Łuk (przód i tył)' }, { id: 'P3', poz: '3', text: 'Parkowanie skośne' }, { id: 'P4.1', poz: '4.1', text: 'Parkowanie prostopadłe (przód)' }, { id: 'P4.2', poz: '4.2', text: 'Parkowanie prostopadłe (tył)' }, { id: 'P5', poz: '5', text: 'Parkowanie równoległe' }, { id: 'P6', poz: '6', text: 'Ruszanie na wzniesieniu' }, { id: 'P7', poz: '7', text: 'Slalom wolny' }, { id: 'P8', poz: '8', text: 'Slalom szybki' }, { id: 'P9', poz: '9', text: 'Ominięcie przeszkody' }, { id: 'P10', poz: '10', text: 'Ósemka' }, { id: 'R1', poz: '1', text: 'Włączanie się do ruchu' }, { id: 'R5', poz: '5', text: 'Skrzyżowania równorzędne' }, { id: 'R6', poz: '6', text: 'Skrzyżowania z pierwszeństwem' }, { id: 'R8', poz: '8', text: 'Ronda' }, { id: 'R10', poz: '10', text: 'Przejścia dla pieszych' }, { id: 'R12', poz: '12', text: 'Zawracanie' }, { id: 'R16', poz: '16', text: 'Wyprzedzanie' }, { id: 'R28', poz: '28', text: 'Prędkość' }, { id: 'R34', poz: '34', text: 'Technika kierowania' } ]; const IconCar = () => h('svg', { width: 40, height: 40, viewBox: '0 0 24 24', fill: 'none', stroke: 'white', strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round' }, h('path', { d: 'M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-1.1 0-2 .9-2 2v9c0 .6.4 1 1 1h2' }), h('circle', { cx: 7, cy: 17, r: 2 }), h('path', { d: 'M9 17h6' }), h('circle', { cx: 17, cy: 17, r: 2 }) ); const LevelPicker = ({ value, label, onChange }) => { const [isOpen, setIsOpen] = useState(false); return h('div', { className: 'relative w-12 h-10 border-r' }, h('button', { onClick: () => setIsOpen(!isOpen), className: 'w-full h-full flex flex-col items-center justify-center hover:bg-slate-50' }, h('span', { className: 'text-[7px] font-black uppercase text-slate-400' }, label), h('span', { className: 'text-[11px] font-black ' + (value ? 'text-blue-600' : 'text-slate-300') }, value || '?') ), isOpen && h('div', { className: 'absolute top-full right-0 z-[100] bg-white shadow-2xl border rounded-xl p-1 grid grid-cols-5 gap-1 min-w-[150px]' }, [1,2,3,4,5,6,7,8,9,10].map(v => h('button', { key: v, onClick: () => { onChange(v); setIsOpen(false); }, className: 'w-6 h-6 rounded-lg text-[10px] font-bold ' + (value === v ? 'bg-blue-600 text-white' : 'hover:bg-slate-100') }, v)) ) ); }; function App() { const [showSplash, setShowSplash] = useState(true); const [view, setView] = useState('HOME'); const [students, setStudents] = useState([]); const [sessions, setSessions] = useState([]); const [activeStud, setActiveStud] = useState(null); const [currRes, setCurrRes] = useState({}); const [lastSess, setLastSess] = useState(null); useEffect(() => { const data = localStorage.getItem(STORAGE_KEY); if (data) { const p = JSON.parse(data); setStudents(p.students || []); setSessions(p.sessions || []); } }, []); const save = (newStuds, newSess) => { const d = { students: newStuds || students, sessions: newSess || sessions }; localStorage.setItem(STORAGE_KEY, JSON.stringify(d)); }; const calc = (res) => { let n2 = 0, n1 = 0; Object.values(res).forEach(r => { if (r.badCount === 2) n2++; if (r.badCount === 1) n1++; }); return { n2, n1, gain: Math.max(1, n2) * EXAM_COST, score: n2 > 0 ? Math.max(0, 40 - n2 * 10) : 100 - n1 * 5 }; }; if (showSplash) { return h('div', { className: 'fixed inset-0 z-[9999] bg-slate-900 flex items-center justify-center p-6 text-center' }, h('div', { className: 'max-w-xs w-full space-y-8' }, h('div', { className: 'bg-blue-600 w-20 h-20 rounded-[24px] mx-auto flex items-center justify-center shadow-2xl' }, h(IconCar)), h('div', null, h('h1', { className: 'text-white text-4xl font-black tracking-tight' }, 'DriveEval'), h('p', { className: 'text-slate-400 text-xs font-bold uppercase tracking-widest mt-2' }, 'System Analizy Postępów') ), h('button', { onClick: () => setShowSplash(false), className: 'w-full bg-white text-slate-900 py-5 rounded-2xl font-black text-lg' }, 'ROZPOCZNIJ PRACĘ') ) ); } if (view === 'HOME') { return h('div', { className: 'max-w-xl mx-auto p-6 space-y-6' }, h('div', { className: 'flex justify-between items-center bg-white p-6 rounded-[32px] shadow-sm border' }, h('div', null, h('h1', { className: 'text-2xl font-black text-slate-800' }, 'DriveEval'), h('p', { className: 'text-[10px] font-black text-blue-600 uppercase tracking-widest' }, 'Twoi Kursanci') ), h('button', { onClick: () => { const n = prompt('Imię i nazwisko:'); if(n && n.trim()) { const nl = [...students, { id: Date.now().toString(), name: n.trim() }]; setStudents(nl); save(nl); } }, className: 'bg-blue-600 text-white w-12 h-12 rounded-2xl shadow-lg font-black text-2xl' }, '+') ), h('div', { className: 'grid gap-3' }, students.map(s => { const ss = sessions.filter(x => x.studentId === s.id); const total = ss.reduce((a, b) => a + calc(b.results).gain, 0); return h('div', { key: s.id, onClick: () => { setActiveStud(s); setView('DETAIL'); }, className: 'bg-white p-6 rounded-[24px] border border-slate-100 flex justify-between items-center cursor-pointer hover:shadow-md transition-shadow' }, h('div', null, h('div', { className: 'font-bold text-lg' }, s.name), h('div', { className: 'text-[10px] font-black text-emerald-600 uppercase' }, 'ZYSK: ' + total + ' ZŁ') ), h('div', { className: 'text-blue-600 font-black' }, '➜') ); }), students.length === 0 && h('div', { className: 'text-center py-20 border-2 border-dashed rounded-[32px] text-slate-300 font-bold uppercase text-[10px]' }, 'Brak kursantów. Dodaj pierwszego.') ) ); } if (view === 'DETAIL') { const mySess = sessions.filter(x => x.studentId === activeStud.id); return h('div', { className: 'max-w-xl mx-auto p-6 space-y-6' }, h('button', { onClick: () => setView('HOME'), className: 'bg-white px-5 py-2 rounded-xl border font-black text-[10px] text-slate-400' }, '← WRÓĆ'), h('div', { className: 'text-center' }, h('h2', { className: 'text-3xl font-black text-slate-900' }, activeStud.name), h('p', { className: 'text-[10px] font-black text-slate-400 uppercase tracking-widest mt-2' }, 'DZIENNIK POSTĘPÓW') ), h('button', { onClick: () => { const r = {}; CATEGORIES.forEach(c => r[c.id] = { good: false, badCount: 0, pre: 0, post: 0 }); setCurrRes(r); setView('EXAM'); }, className: 'w-full bg-blue-600 text-white py-5 rounded-[24px] font-black shadow-xl' }, 'NOWY ARKUSZ'), h('div', { className: 'space-y-2' }, [...mySess].reverse().map((s, i) => h('div', { key: s.id, onClick: () => { setLastSess(s); setView('SUMMARY'); }, className: 'bg-white p-4 rounded-xl border flex justify-between cursor-pointer hover:bg-slate-50' }, h('span', { className: 'font-bold' }, 'Sesja #' + (mySess.length - i)), h('span', { className: 'text-emerald-600 font-black' }, '+' + calc(s.results).gain + ' zł') )) ), h('button', { onClick: () => { if(confirm('Usunąć kursanta i całą historię?')) { const nl = students.filter(s => s.id !== activeStud.id); const ns = sessions.filter(s => s.studentId !== activeStud.id); setStudents(nl); setSessions(ns); save(nl, ns); setView('HOME'); } }, className: 'w-full text-red-400 text-[10px] font-black py-10 uppercase opacity-30 hover:opacity-100' }, 'USUŃ KURSANTA') ); } if (view === 'EXAM') { return h('div', { className: 'max-w-2xl mx-auto pb-32' }, h('div', { className: 'sticky top-0 z-50 bg-white/95 backdrop-blur p-4 border-b flex justify-between items-center shadow-sm' }, h('div', null, h('div', { className: 'font-black text-lg' }, activeStud.name), h('div', { className: 'text-[9px] font-black text-blue-600 uppercase tracking-widest' }, 'Egzamin Próbny') ), h('button', { onClick: () => { const nS = { id: Date.now().toString(), studentId: activeStud.id, results: currRes }; const nl = [...sessions, nS]; setSessions(nl); save(undefined, nl); setLastSess(nS); setView('SUMMARY'); }, className: 'bg-slate-900 text-white px-10 py-3 rounded-xl font-black shadow-lg' }, 'ZAKOŃCZ') ), h('div', { className: 'bg-white mt-4 border-x' }, CATEGORIES.map(cat => { const r = currRes[cat.id]; return h('div', { key: cat.id, className: 'flex border-b text-[11px] items-center min-h-[50px] hover:bg-slate-50 transition-colors' }, h('div', { className: 'w-8 text-center font-bold text-slate-300' }, cat.poz), h('div', { className: 'flex-1 px-3 font-semibold text-slate-700' }, cat.text), h('div', { className: 'flex border-l h-full items-stretch' }, h(LevelPicker, { label: 'Przed', value: r.pre, onChange: v => { const n = {...currRes}; n[cat.id].pre = v; setCurrRes(n); } }), h(LevelPicker, { label: 'Po', value: r.post, onChange: v => { const n = {...currRes}; n[cat.id].post = v; setCurrRes(n); } }), h('button', { onClick: () => { const n = {...currRes}; n[cat.id].badCount = n[cat.id].badCount === 1 ? 0 : 1; n[cat.id].good = false; setCurrRes(n); }, className: 'w-10 border-r flex items-center justify-center ' + (r.badCount === 1 ? 'bg-orange-500 text-white font-bold' : 'text-slate-200') }, '1'), h('button', { onClick: () => { const n = {...currRes}; n[cat.id].badCount = n[cat.id].badCount === 2 ? 0 : 2; n[cat.id].good = false; setCurrRes(n); }, className: 'w-10 border-r flex items-center justify-center ' + (r.badCount === 2 ? 'bg-red-600 text-white font-bold' : 'text-slate-200') }, '2'), h('button', { onClick: () => { const n = {...currRes}; n[cat.id].good = !n[cat.id].good; n[cat.id].badCount = 0; setCurrRes(n); }, className: 'w-10 flex items-center justify-center ' + (r.good ? 'bg-green-500 text-white font-bold' : 'text-slate-200') }, 'P') ) ); }) ) ); } if (view === 'SUMMARY') { const stats = calc(lastSess.results); const myS = sessions.filter(x => x.studentId === activeStud.id); const chartData = myS.map((s, i) => ({ n: i+1, v: calc(s.results).score })); return h('div', { className: 'max-w-2xl mx-auto p-6 space-y-6' }, h('div', { className: 'flex justify-between items-center no-print' }, h('button', { onClick: () => setView('DETAIL'), className: 'bg-white p-3 rounded-xl border' }, '←'), h('button', { onClick: () => window.print(), className: 'bg-blue-600 text-white px-8 py-3 rounded-xl font-black shadow-lg' }, 'DRUKUJ') ), h('div', { className: 'bg-white p-10 rounded-[40px] shadow-2xl border space-y-8' }, h('h2', { className: 'text-center text-3xl font-black uppercase mb-2' }, 'Wynik Treningu'), h('p', { className: 'text-center text-2xl font-black text-blue-600 mb-10' }, activeStud.name), h('div', { className: 'grid grid-cols-2 gap-4' }, h('div', { className: 'bg-slate-50 p-6 rounded-[30px] text-center border' }, h('div', { className: 'text-[9px] font-black uppercase opacity-40 mb-1' }, 'Status'), h('div', { className: 'text-xl font-black ' + (stats.n2 > 0 ? 'text-red-600' : 'text-green-600') }, stats.n2 > 0 ? 'NEGATYWNY' : 'POZYTYWNY') ), h('div', { className: 'bg-emerald-50 p-6 rounded-[30px] text-center border' }, h('div', { className: 'text-[9px] font-black uppercase text-emerald-600 mb-1' }, 'Uratowano'), h('div', { className: 'text-xl font-black text-emerald-700' }, stats.gain + ' zł') ) ), h('div', { className: 'h-48 w-full' }, h(ResponsiveContainer, null, h(LineChart, { data: chartData }, h(CartesianGrid, { strokeDasharray: '3 3', vertical: false, stroke: '#f1f5f9' }), h(XAxis, { dataKey: 'n', hide: true }), h(YAxis, { hide: true, domain: [0, 100] }), h(Line, { type: 'monotone', dataKey: 'v', stroke: '#2563eb', strokeWidth: 5, dot: { fill: '#2563eb', r: 5 } }) ) ) ), h('div', { className: 'border rounded-[24px] overflow-hidden' }, CATEGORIES.map(cat => { const r = lastSess.results[cat.id]; if(!r.good && r.badCount === 0 && !r.pre && !r.post) return null; return h('div', { key: cat.id, className: 'flex p-4 border-b items-center text-[10px]' }, h('span', { className: 'w-8 font-bold text-slate-300' }, cat.poz), h('span', { className: 'flex-1 font-semibold text-slate-700' }, cat.text), h('div', { className: 'flex gap-6' }, h('span', { className: 'text-slate-400 font-black' }, 'P: ' + (r.pre || '-')), h('span', { className: 'text-blue-500 font-black' }, 'Po: ' + (r.post || '-')), h('span', { className: 'font-black ' + (r.good ? 'text-green-500' : r.badCount === 2 ? 'text-red-600' : 'text-orange-500') }, r.good ? 'P' : r.badCount === 2 ? 'N' : r.badCount === 1 ? 'B' : '-') ) ); }) ) ) ); } return null; } const root = ReactDOM.createRoot(document.getElementById('drive-eval-root')); root.render(h(App)); })();