|
| 1 | +'use client' |
| 2 | + |
| 3 | +import React, { useEffect, useMemo, useState } from 'react' |
| 4 | +import { |
| 5 | + Dialog, |
| 6 | + DialogContent, |
| 7 | + DialogDescription, |
| 8 | + DialogHeader, |
| 9 | + DialogTitle, |
| 10 | +} from '@/components/ui/dialog' |
| 11 | +import PixelSnakeGame from '@/components/pixel-snake-game' |
| 12 | + |
| 13 | +// A small wrapper that listens for a secret sequence and opens a dialog with the snake game. |
| 14 | +// New code: s n a k e Enter |
| 15 | +const SECRET = ['s', 'n', 'a', 'k', 'e', 'Enter'] |
| 16 | + |
| 17 | +export default function BattlesnakeEasterEgg() { |
| 18 | + const [open, setOpen] = useState(false) |
| 19 | + const [ix, setIx] = useState(0) |
| 20 | + |
| 21 | + const instructions = useMemo( |
| 22 | + () => 'Hint: Type S N A K E then Enter anywhere on this page.', |
| 23 | + [], |
| 24 | + ) |
| 25 | + |
| 26 | + useEffect(() => { |
| 27 | + const normalize = (k: string) => (k.length === 1 ? k.toLowerCase() : k) |
| 28 | + const onKey = (e: KeyboardEvent) => { |
| 29 | + const key = normalize(e.key) |
| 30 | + const expected = normalize(SECRET[ix]) |
| 31 | + if (key === expected) { |
| 32 | + const next = ix + 1 |
| 33 | + if (next >= SECRET.length) { |
| 34 | + // Prevent the Enter key (final step) from interacting with the dialog immediately. |
| 35 | + e.preventDefault() |
| 36 | + e.stopPropagation() |
| 37 | + // Open on next frame to let the key event fully resolve first. |
| 38 | + requestAnimationFrame(() => setOpen(true)) |
| 39 | + setIx(0) |
| 40 | + return |
| 41 | + } |
| 42 | + setIx(next) |
| 43 | + } else { |
| 44 | + // If mismatch but current key could restart the sequence |
| 45 | + if (key === normalize(SECRET[0])) setIx(1) |
| 46 | + else setIx(0) |
| 47 | + } |
| 48 | + } |
| 49 | + const opts: AddEventListenerOptions = { capture: true } |
| 50 | + window.addEventListener('keydown', onKey, opts) |
| 51 | + return () => window.removeEventListener('keydown', onKey, opts) |
| 52 | + }, [ix]) |
| 53 | + |
| 54 | + // One-time console hints to tease the easter egg (non-intrusive, styled). |
| 55 | + useEffect(() => { |
| 56 | + try { |
| 57 | + const title = '🕹 Hidden Fun' |
| 58 | + const sTitle = |
| 59 | + 'background:#111;color:#B19EEF;padding:2px 8px;border-radius:6px;font-weight:700;' |
| 60 | + const sLine = 'color:#9ca3af' |
| 61 | + const sHot = |
| 62 | + 'background:#1f2937;color:#e5e7eb;padding:1px 6px;border-radius:4px' |
| 63 | + // Grouped to keep console tidy; collapsed so it doesn’t spam. |
| 64 | + // Shown in both dev and prod—this is intentional for the easter egg. |
| 65 | + // Feel free to gate via NODE_ENV if you want it dev-only. |
| 66 | + console.groupCollapsed('%c' + title, sTitle) |
| 67 | + console.log('%cThere’s a tiny easter egg on this page.', sLine) |
| 68 | + console.log('Hint: type: %cS N A K E Enter', sHot) |
| 69 | + console.log('%cPsst: share the fun, not the secret 😉', sLine) |
| 70 | + console.groupEnd() |
| 71 | + } catch { |
| 72 | + // Ignore if console is unavailable |
| 73 | + } |
| 74 | + }, []) |
| 75 | + |
| 76 | + return ( |
| 77 | + <> |
| 78 | + {/* Accessible-only hint for screen readers; visually hidden to keep it an easter egg */} |
| 79 | + <p className="sr-only" aria-live="polite"> |
| 80 | + {instructions} |
| 81 | + </p> |
| 82 | + <Dialog open={open} onOpenChange={setOpen}> |
| 83 | + <DialogContent |
| 84 | + className="max-w-xl p-4 sm:p-6" |
| 85 | + onOpenAutoFocus={(e) => e.preventDefault()} |
| 86 | + > |
| 87 | + <DialogHeader> |
| 88 | + <DialogTitle>Pixel Snake</DialogTitle> |
| 89 | + <DialogDescription> |
| 90 | + Use arrow keys or WASD to move. Press Space to pause. |
| 91 | + </DialogDescription> |
| 92 | + </DialogHeader> |
| 93 | + <div className="mt-2"> |
| 94 | + <PixelSnakeGame className="relative mx-auto aspect-square w-full max-w-[520px]" /> |
| 95 | + </div> |
| 96 | + </DialogContent> |
| 97 | + </Dialog> |
| 98 | + </> |
| 99 | + ) |
| 100 | +} |
0 commit comments