From c07cca0e33b712a75be4a75804c602b7892dfe2b Mon Sep 17 00:00:00 2001 From: Citali Date: Wed, 4 Feb 2026 14:51:29 +0100 Subject: [PATCH] Add an easter egg to the app --- src/app/(normal)/page.tsx | 16 +- src/app/(normal)/tos/page.tsx | 8 +- src/app/error.tsx | 45 +++++ src/app/global-error.tsx | 49 ++++++ src/app/loading.tsx | 17 ++ src/app/not-found.tsx | 48 ++++++ src/components/global/GameOfLifeMini.tsx | 200 +++++++++++++++++++++++ 7 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 src/app/error.tsx create mode 100644 src/app/global-error.tsx create mode 100644 src/app/loading.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/components/global/GameOfLifeMini.tsx diff --git a/src/app/(normal)/page.tsx b/src/app/(normal)/page.tsx index 1c55a4e..d39b276 100644 --- a/src/app/(normal)/page.tsx +++ b/src/app/(normal)/page.tsx @@ -10,15 +10,23 @@ export default function Home() { Welcome to my place!

- I'm an illustrator, character designer, miniature painter, 3d modeller, makeup artist and much more and happy to show you things i've created. + I'm an illustrator, character designer, miniature painter, 3d + modeller, makeup artist and much more and happy to show you things + i've created.

- If you want to commission me
you can find all the information you need here:
Commissions + If you want to commission me +
+ you can find all the information you need here: +
{" "} + + Commissions +

- + ); -} +} \ No newline at end of file diff --git a/src/app/(normal)/tos/page.tsx b/src/app/(normal)/tos/page.tsx index 2bf507f..e4fc43e 100644 --- a/src/app/(normal)/tos/page.tsx +++ b/src/app/(normal)/tos/page.tsx @@ -1,10 +1,10 @@ -import { prisma } from '@/lib/prisma'; -import ReactMarkdown from 'react-markdown'; +import { prisma } from "@/lib/prisma"; +import ReactMarkdown from "react-markdown"; export default async function TosPage() { const tos = await prisma.termsOfService.findFirst({ orderBy: [{ version: "desc" }], - }) + }); return (
@@ -13,4 +13,4 @@ export default async function TosPage() {
); -} \ No newline at end of file +} diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..627bdf2 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; + +export default function ErrorPage({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+
+
+
+
+ +
+
+ Something went wrong +
+

+ We hit a snag. +

+

+ The page failed to load. You can try again or head back home. +

+
+ + +
+
+ {error.digest ? `Error ID: ${error.digest}` : "Unexpected error"} +
+
+
+ ); +} diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 0000000..83aa506 --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,49 @@ +"use client"; + +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + + +
+
+
+
+
+ +
+
+ Critical error +
+

+ We could not recover. +

+

+ A global error occurred. Try again or return home. +

+
+ + +
+
+ {error.digest ? `Error ID: ${error.digest}` : "Unexpected error"} +
+
+
+ + + ); +} diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..854f28e --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,17 @@ +export default function Loading() { + return ( +
+
+
+
+
+
+
+
+ Loading +
+
Preparing the gallery.
+
+
+ ); +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..1fb5124 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,48 @@ +import Link from "next/link"; + +import { GameOfLifeMini } from "@/components/global/GameOfLifeMini"; +import { Button } from "@/components/ui/button"; + +export default function NotFound() { + return ( +
+
+
+
+
+
+ +
+
+
+ Lost and Found +
+

+ Page not found. +

+

+ This page wandered off. While we look for it, you can play a quick + Game of Life. Click cells to shape the pattern. +

+
+ + +
+
+ Tip: The gliders are shy. Try clicking five cells in an L shape. +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/src/components/global/GameOfLifeMini.tsx b/src/components/global/GameOfLifeMini.tsx new file mode 100644 index 0000000..b130ca7 --- /dev/null +++ b/src/components/global/GameOfLifeMini.tsx @@ -0,0 +1,200 @@ +"use client"; + +import type { MouseEvent } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; + +function createGrid(cols: number, rows: number) { + return Array.from({ length: rows }, () => Array.from({ length: cols }, () => 0)); +} + +function seedRandom(grid: number[][], density: number) { + for (let y = 0; y < grid.length; y += 1) { + for (let x = 0; x < grid[0].length; x += 1) { + grid[y][x] = Math.random() < density ? 1 : 0; + } + } +} + +function stepGrid(grid: number[][]) { + const rows = grid.length; + const cols = grid[0]?.length ?? 0; + const next = createGrid(cols, rows); + + for (let y = 0; y < rows; y += 1) { + const yPrev = (y - 1 + rows) % rows; + const yNext = (y + 1) % rows; + for (let x = 0; x < cols; x += 1) { + const xPrev = (x - 1 + cols) % cols; + const xNext = (x + 1) % cols; + const neighbors = + grid[yPrev][xPrev] + + grid[yPrev][x] + + grid[yPrev][xNext] + + grid[y][xPrev] + + grid[y][xNext] + + grid[yNext][xPrev] + + grid[yNext][x] + + grid[yNext][xNext]; + + if (grid[y][x]) { + next[y][x] = neighbors === 2 || neighbors === 3 ? 1 : 0; + } else { + next[y][x] = neighbors === 3 ? 1 : 0; + } + } + } + + return next; +} + +export function GameOfLifeMini() { + const canvasRef = useRef(null); + const [running, setRunning] = useState(true); + const [speed, setSpeed] = useState(6); + const cellSize = 10; + const width = 420; + const height = 260; + + const { rows, cols } = useMemo( + () => ({ + cols: Math.floor(width / cellSize), + rows: Math.floor(height / cellSize), + }), + [], + ); + + const gridRef = useRef(createGrid(cols, rows)); + + useEffect(() => { + gridRef.current = createGrid(cols, rows); + seedRandom(gridRef.current, 0.22); + }, [cols, rows]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let raf = 0; + let last = performance.now(); + const tickMs = 1000 / speed; + + const draw = () => { + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = "#141414"; + ctx.fillRect(0, 0, width, height); + + const grid = gridRef.current; + ctx.fillStyle = "#f2f2f2"; + for (let y = 0; y < rows; y += 1) { + for (let x = 0; x < cols; x += 1) { + if (grid[y][x]) { + ctx.fillRect( + x * cellSize, + y * cellSize, + cellSize - 1, + cellSize - 1, + ); + } + } + } + }; + + const loop = (now: number) => { + if (running && now - last >= tickMs) { + gridRef.current = stepGrid(gridRef.current); + last = now; + } + draw(); + raf = requestAnimationFrame(loop); + }; + + raf = requestAnimationFrame(loop); + return () => cancelAnimationFrame(raf); + }, [running, speed, cols, rows]); + + const handleToggleCell = (event: MouseEvent) => { + const rect = event.currentTarget.getBoundingClientRect(); + const x = Math.floor((event.clientX - rect.left) / cellSize); + const y = Math.floor((event.clientY - rect.top) / cellSize); + if (x < 0 || y < 0 || y >= rows || x >= cols) return; + gridRef.current[y][x] = gridRef.current[y][x] ? 0 : 1; + }; + + return ( +
+
+
+
+ Game of Life +
+
+ Click to toggle cells. Space-time improvisation. +
+
+
+ + + +
+
+ +
+ +
+ +
+ Speed +
+ + {speed} + +
+
+
+ ); +}