// screens.jsx — wszystkie ekrany finalnej strony urodzinowej Klaudii // Styl: V1 Vintage Postcard. Teksty po polsku. // Paleta z PostcardTheme (import z illustrations, tu zdefiniowana lokalnie) const T = { bg: '#f4ead4', ink: '#2a1810', burgundy: '#7a1f2a', navy: '#1f3a5f', accent: '#c8342b', mustard: '#d4a028', paper: '#fcf5e3', paperDark: '#efe2be', shadow: 'rgba(42,24,16,0.18)', }; // ───────────────────────── Layout wrapper ───────────────────────── function Page({ children, noBorder }) { return (
{!noBorder &&
} {children}
); } function PolkaButton({ children, onClick, variant='primary', style }) { const bg = variant === 'primary' ? T.burgundy : T.paper; const fg = variant === 'primary' ? T.paper : T.ink; return ( ); } // ───────────────────────── 1. Koperta ───────────────────────── function ScreenEnvelope({ onNext }) { const [opened, setOpened] = React.useState(false); return ( {/* Stemple */}
Par Avion 25.04.2026 Kraków
{/* Znaczek */}
USA · PL
List dla
Klaudia
24 urodziny
{/* koperta */}
{ if(!opened) setOpened(true); else onNext(); }} style={{ marginTop:38, width:320, height:200, position:'relative', cursor:'pointer', filter:`drop-shadow(5px 7px 0 ${T.shadow})`, transform: opened ? 'translateY(-10px)' : 'translateY(0)', transition:'transform .6s cubic-bezier(.2,.8,.2,1)', }}> {/* koperta dół */} {/* złożenia */} {/* klapa */} {/* pieczęć woskowa */} {!opened && ( K )} {/* list wystający */} {opened && ( Moja najdroższa Jammuś… )}
kliknij kopertę
{opened && (
Przeczytaj list →
)}
{/* maskotki */}
); } // ───────────────────────── 2. List ───────────────────────── function ScreenLetter({ onNext }) { return (
Lotnicza · PL
Kraków, 25 kwietnia 2026
Moja najdroższa Jammuś,
dziś kończysz twenty-four, a ja wciąż nie mogę uwierzyć, ile radości potrafi się zmieścić w jednej osobie. Przygotowałem dla Ciebie taką małą przygodę. Kilka prezentów po drodze {'<3'}.

Pomiędzy prezentami są też malutkie zadania. Bez nich nie odblokujesz kolejnej niespodzianki. Ostrzegam: będzie dużo jamników.

Good jamnik won't bite, but jamnikov will.

All my love,
Twój Kruk
Zaczynamy →
); } // ───────────────────────── 3. Prezent 1 — voucher okulary ───────────────────────── function ScreenGift1({ onNext }) { return (
Prezent pierwszy
Okulary z filtrem światła niebieskiego
dla najsłodszej nauczycielki angielskiego
Voucher · wizyta u okulisty
Żeby jammusia nie bolały oczka
Opłacone
{/* okulary */}
Jammusiu, trza Ci kupić nowe oksy bo te już się rozlatują. Nie kupuję ich sam bo musisz je sobie sama wybrać, ale masz tutaj w cenie transport, zakup i huggs (te huggs to taki bonusik od kruka) {'<3'}
Nr 24/0425 Ważny: bez limitu Podpis: Kruk
Dalej →
); } // ───────────────────────── 4. Gra — Trening Jumpnika ───────────────────────── // Jamnik w 3 torach. Z kruka na górze spadają: // ♥ serce (+1) 🦴 kość (+1) 🔥 grzejniczek (-1) // Cel: 15 pkt. Jeśli wynik spadnie poniżej zera — Game Over, restart. function ScreenGame({ onNext }) { const LANES = 3; const GOAL = 15; const FALL_STEP = 0.58; // % areny / frame @60fps — szybszy spadek const [lane, setLane] = React.useState(1); const [score, setScore] = React.useState(0); const [items, setItems] = React.useState([]); const [phase, setPhase] = React.useState('intro'); // intro | playing | won | lost const [combo, setCombo] = React.useState(0); const [flash, setFlash] = React.useState(null); const laneRef = React.useRef(lane); React.useEffect(() => { laneRef.current = lane; }, [lane]); const scoreRef = React.useRef(0); const phaseRef = React.useRef(phase); React.useEffect(() => { phaseRef.current = phase; }, [phase]); const rafRef = React.useRef(null); const spawnRef = React.useRef(null); const lastTRef = React.useRef(0); const reset = () => { setScore(0); scoreRef.current = 0; setItems([]); setLane(1); setCombo(0); }; const begin = () => { reset(); setPhase('playing'); }; // Pętla rAF — płynnie React.useEffect(() => { if (phase !== 'playing') return; lastTRef.current = performance.now(); const loop = (t) => { const dt = Math.min(50, t - lastTRef.current); lastTRef.current = t; const step = (dt / (1000 / 60)) * FALL_STEP; setItems(list => { const next = []; for (const it of list) { const ny = it.y + step; // strefa łapania if (ny > 80 && ny < 96 && it.lane === laneRef.current) { let pts = 0, label = '', good = true; if (it.kind === 'heart') { pts = 1; label = '+1 ♥'; good = true; } if (it.kind === 'cold') { pts = 1; label = '+1 ❄'; good = true; } if (it.kind === 'heater') { pts = -1; label = '−1 🔥'; good = false; } const ns = scoreRef.current + pts; scoreRef.current = ns; setScore(ns); if (pts > 0) setCombo(c => c + 1); else setCombo(0); setFlash({ good, label, id: Math.random() }); if (ns < 0) setPhase('lost'); else if (ns >= GOAL) setPhase('won'); continue; } if (ny > 105) continue; next.push({ ...it, y: ny }); } return next; }); if (phaseRef.current === 'playing') rafRef.current = requestAnimationFrame(loop); }; rafRef.current = requestAnimationFrame(loop); const spawn = () => { if (phaseRef.current !== 'playing') return; const r = Math.random(); // 60% grzejniczek (🔥 −1), 20% cold (❄ +1), 20% serce (♥ +1) const kind = r < 0.60 ? 'heater' : r < 0.80 ? 'cold' : 'heart'; setItems(list => [...list, { id: Math.random() + performance.now(), lane: Math.floor(Math.random() * LANES), y: -6, kind, }]); spawnRef.current = setTimeout(spawn, 450 + Math.random() * 400); }; spawnRef.current = setTimeout(spawn, 400); return () => { cancelAnimationFrame(rafRef.current); clearTimeout(spawnRef.current); }; }, [phase]); // Klawiatura React.useEffect(() => { const onKey = (e) => { if (phaseRef.current !== 'playing') return; if (e.key === 'ArrowLeft' || e.key === 'a') setLane(l => Math.max(0, l - 1)); if (e.key === 'ArrowRight' || e.key === 'd') setLane(l => Math.min(LANES - 1, l + 1)); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); // Flash auto-clear React.useEffect(() => { if (!flash) return; const t = setTimeout(() => setFlash(null), 400); return () => clearTimeout(t); }, [flash]); const glyph = (k) => k === 'heart' ? '♥' : k === 'cold' ? '❄' : '🔥'; const iColor = (k) => k === 'heart' ? T.accent : k === 'cold' ? '#3a7ab0' : '#e86a2a'; return (
przerywnik · mini-gra
Trening Jumpnika
♥ serce +1  ·  ❄ cold +1  ·  🔥 grzejniczek −1. Zbierz {GOAL}, nie spadnij poniżej zera.
{/* HUD */}
Wynik: {score} / {GOAL} Combo: {combo}×
{/* Arena */}
{[0,1,2].map(i => (
phase === 'playing' && setLane(i)} style={{ flex:1, borderRight: i < 2 ? `1px dashed ${T.ink}33` : 'none', background: lane === i && phase === 'playing' ? `linear-gradient(to bottom, transparent, ${T.accent}18)` : 'transparent', cursor: phase === 'playing' ? 'pointer' : 'default', transition:'background .2s', }}/> ))}
{/* Kruk */}
{/* Linia łapania */}
{/* Przedmioty */} {items.map(it => (
{glyph(it.kind)}
))} {/* Jumpnik */}
{/* Flash */} {flash && (
{combo >= 3 && flash.good ? `${flash.label} · ${combo}×!` : flash.label}
)} {/* Intro */} {phase === 'intro' && (
Gotowa, Jammuś?
Kruk zrzuca serca ♥, śnieżki ❄ i uwaga, grzejniczki 🔥. Serca i cold: +1. Grzejniczek: −1. Zbierz {GOAL}, nie spadnij poniżej zera.
Start! ♥
)} {/* Lost */} {phase === 'lost' && (
grzejniczek wygrał
Spróbuj jeszcze raz!
Wynik spadł poniżej zera. Jumpnik wierzy w Ciebie, jeszcze jedno podejście.
Od nowa ↻
)} {/* Won */} {phase === 'won' && (
Jumpnik wytrenowany
{GOAL} punktów miłości!
Wynik: {score}. Serce Jumpnika bije dla Ciebie ♥
Następny prezent →
)}
); } // ───────────────────────── 5. Prezent 2 — Obiad ───────────────────────── function ScreenGift2({ onNext }) { return (
Prezent drugi
Obiad, jaki tylko chcesz
bez limitu budżetu, Ty wybierasz miejsce
Rezerwacja · kolacja urodzinowa
Bawietka? A może coś jeszcze lepszego?
Twój wybór
{/* menu-like divider with icon */}
menu
Wiem jammusiu, jak lubisz Bawietkę, tam zawsze znajdzie się dla nas stolik. Ale jeśli masz dziś ochotę na coś innego, absolutnie cokolwiek. Sushi, ramen, włoskie, stek, menu degustacyjne - no limit. Wybierz miejsce. Ja prowadzę, rezerwuję i płacę.
Nr 24/0425/II Stolik dla dwojga Rezerwuje: Kruk
Wielki finał →
); } // ───────────────────────── 6. Quiz ───────────────────────── const QUIZ = [ { q: 'Dokończ: "Good jamnik won\'t bite…"', options: ['…but jamnikov will.', '…but he will steal your heart.', '…but cats might.'], correct: 0, after: 'Oczywiście. Klasyk gatunku.', }, { q: 'Jak Kruk czasem nazywa Klaudię?', options: ['Słoneczko', 'Jammuś', 'Myszka'], correct: 1, after: '♥ moja Jammuś.', }, { q: 'Czym różni się zwykły jamnik od Jumpnika?', options: ['Jumpnik jest krótszy', 'Jumpnik skacze', 'Jumpnik ma okulary'], correct: 1, after: 'Skaczący jamnik. Naturalne zjawisko.', }, { q: 'Z której strony łóżka ma spać Kruk?', options: ['Z prawej', 'Na środku', 'Z lewej'], correct: 2, after: 'Oczywiście. Lewa strona zawsze należała do Kruka.', }, { q: 'Gdzie jest Neścik?', options: ['Na drzewie', 'W leżance', 'W mieszkanku'], correct: 0, anyCorrect: true, after: 'Tak! Neścik jest wszędzie jednocześnie. Magia.', }, ]; function ScreenQuiz({ onNext }) { const [i, setI] = React.useState(0); const [picked, setPicked] = React.useState(null); const [score, setScore] = React.useState(0); const q = QUIZ[i]; const done = i >= QUIZ.length; const pick = (idx) => { if (picked !== null) return; setPicked(idx); if (q.anyCorrect || idx === q.correct) setScore(s => s + 1); }; const next = () => { setPicked(null); setI(n => n + 1); }; if (done) { return (
wyniki quizu
{score} / {QUIZ.length}
Świetnie Ci poszło Jammyn ♥
Ostatni prezent →
); } return (
quiz o nas
Pytanie {i+1} z {QUIZ.length}
{q.q}
{q.options.map((opt, idx) => { const isPicked = picked === idx; const isCorrect = q.anyCorrect ? (picked !== null) : idx === q.correct; const reveal = picked !== null; let bg = T.paper, fg = T.ink, border = T.ink; if (reveal && isCorrect) { bg = '#d4e6c8'; border = '#4a6a2a'; } else if (reveal && isPicked && !isCorrect) { bg = '#f0c8c8'; border = T.burgundy; } return ( ); })}
{picked !== null && (
{q.after}
)}
{picked !== null && (
{i === QUIZ.length - 1 ? 'Zobacz wynik →' : 'Kolejne pytanie →'}
)}
); } // ───────────────────────── 7. Finał ───────────────────────── function ScreenFinal({ onRestart }) { const [hearts, setHearts] = React.useState([]); React.useEffect(() => { const iv = setInterval(() => { setHearts(h => [...h.slice(-16), { id: Math.random(), x: Math.random()*100, dur: 3+Math.random()*2, size: 14+Math.random()*18, delay: Math.random()*1 }]); }, 600); return () => clearInterval(iv); }, []); return ( {/* spadające serca */}
{hearts.map(h => (
))}
Koniec listu
Kocham Cię
Wszystkiego najlepszego, Jammuś. Bądź moja na zawsze. Twój Kruk
25.04.2026
Zacznij od nowa
); } Object.assign(window, { ScreenEnvelope, ScreenLetter, ScreenGift1, ScreenGame, ScreenGift2, ScreenQuiz, ScreenFinal, T });