Fork it. Drop in your images. Ship it. A zero-backend gallery SPA that goes from empty folder to live GitHub Pages site in under five minutes.
Built with Vite + React + TypeScript. No server. No database. No fuss.
- 🗂️ Gallery — responsive grid of animated thumbnail cards
- 📅 Timeline — chronological view grouped by year (optional age annotation)
- 📺 Slideshow — fullscreen auto-advancing presentation, great for a TV display
- 🔍 Lightbox — full-size overlay with keyboard navigation (
←→Esc)
Plus a small suite of Python CLI scripts to manage your images without ever touching metadata.json by hand:
process_images.py → resize + thumbnail + manifest in one go
rotate_image.py → fix sideways photos across all dirs at once
relabel_image.py → give an image a friendly name
delete_image.py → remove an image cleanly from everything
browser
└── fetches metadata.json at runtime (useManifest hook)
└── renders one of three views (useState in App.tsx)
🗂️ Gallery → ThumbnailCard grid (lazy-loaded <img>)
📅 Timeline → year-grouped list
📺 Slideshow → fullscreen + auto-advance timer
🔍 Lightbox → modal overlay, keyboard-navigable
Design choices worth knowing:
- 🚫 No router — view switching is a single
useState<ViewMode>. Simple and URL-change-free. - 🚫 No image bundling — Vite copies
public/images/verbatim intodist/. The JS bundle never imports an image. Add 500 images and the bundle stays the same size. - ⚡ Runtime manifest — the app fetches
metadata.jsonon load. To add/remove images, regenerate the manifest and redeploy. No JS rebuild needed. - 🎨 Pure CSS — design tokens in
src/styles/theme.cssas CSS custom properties. Co-located.cssper component. Zero CSS frameworks. - 🐍 Python pipeline — one script does everything.
Pillowis the only external dependency.
├── public/
│ ├── metadata.json ← generated; fetched at runtime
│ └── images/
│ ├── full/ ← full-res images (max 1920 px longest edge)
│ └── thumbs/ ← thumbnails (400×300 bounding box)
├── raw_images/ ← drop originals here (gitignored)
├── scripts/
│ ├── _image_utils.py ← shared helpers
│ ├── process_images.py ← resize + thumbnail + manifest
│ ├── rotate_image.py ← rotate in all three dirs at once
│ ├── relabel_image.py ← set a persistent display title
│ └── delete_image.py ← remove from disk + manifest
├── custom_labels.json ← your title overrides (tracked in git)
└── src/
├── App.tsx ← root: view state + lightbox state
├── components/
│ ├── Gallery/
│ ├── Timeline/
│ ├── Slideshow/
│ ├── Lightbox/
│ └── Nav.tsx
├── hooks/
│ ├── useManifest.ts ← fetches and sorts metadata.json
│ └── useSlideshow.ts ← auto-advance interval + pause logic
├── styles/
│ └── theme.css ← CSS custom properties / design tokens
└── types/
└── gallery.types.ts
- Node.js 20+ and pnpm
- Python 3.10+ with Pillow —
pip install Pillow
pnpm installmkdir raw_images
# copy your JPG / PNG files inpython3 scripts/process_images.pyThe script will:
- Resize to max 1920 px (longest edge), JPEG quality 88
- Generate 400×300 thumbnails
- Extract dates from EXIF → filename pattern (
YYYYMMDD_HHMMSS) → file mtime - Assign sequential labels:
000001,000002, … - Write
public/metadata.json
⚠️ If a date falls back to file mtime, the script prints a warning — mtime is unreliable after copying files.
On subsequent runs, skip already-processed images:
python3 scripts/process_images.py --skip-existingpnpm devOpen http://localhost:5173 and enjoy. 🎉
Every script accepts these target formats interchangeably:
| Format | Example | Resolves to |
|---|---|---|
| Short number | 1 |
label 000001 |
| Full label | 000042 |
label 000042 |
| Custom title | Stars |
whatever you relabeled it to |
| Exact filename | 20240215_143000.jpg |
that file |
python3 scripts/rotate_image.py 1 --rotate left # 90° counter-clockwise
python3 scripts/rotate_image.py 1 --rotate right # 90° clockwise
python3 scripts/rotate_image.py 1 --rotate 180 # upside-downUpdates raw_images/, public/images/full/, and public/images/thumbs/ in one shot.
python3 scripts/relabel_image.py 1 "Unicorn Rainbow"Saves the title to custom_labels.json (tracked in git) and patches metadata.json immediately. Survives re-runs of process_images.py.
python3 scripts/delete_image.py 1 # asks for confirmation
python3 scripts/delete_image.py 1 --yes # skip the promptRemoves from all three image directories, from metadata.json, and from custom_labels.json.
Set your base path in vite.config.ts:
base: process.env.NODE_ENV === 'production' ? '/your-gallery/' : '/',Then build and ship:
pnpm build
cp -r dist/* ../my-github-pages/public/your-gallery/
cd ../my-github-pages && git add . && git commit -m "Deploy gallery" && git pushThat's it — your gallery is live. 🌍
{
"generated": "2025-03-10T12:00:00Z",
"images": [
{
"id": "20240215-143000",
"filename": "20240215_143000.jpg",
"thumbnail": "thumbs/20240215_143000.jpg",
"title": "000001",
"date": "2024-02-15",
"order": 1,
"width": 1200,
"height": 900
}
]
}title defaults to the zero-padded order number; override it any time with relabel_image.py. date follows the chain: EXIF → filename pattern → file mtime.