A cozy, pastel-themed Pinterest-style gallery for browsing anime art.
An endless-scroll discovery feed plus character/anime search, backed by two purpose-built image APIs, with buttery-smooth motion and an image pipeline tuned for a masonry feed that never jank-scrolls.
- πΌοΈ Pinterest-style masonry gallery β CSS-column layout that flows naturally at any viewport, no grid library required.
- βΎοΈ Endless discovery feed β a random art stream from Nekos API that never runs out, deduplicated client-side across batches.
- π Character/anime search β debounced search against Safebooru's tag index, seamlessly crossfading from the discovery feed to search results.
- π‘οΈ Forced safe-for-work content β every request is rating-filtered at
the API level, plus a server-side tag blocklist (in the
/api/discoveryroute) as a second gate on top of Nekos' own occasionally-unreliable rating classification β unsafe images never reach the browser. - π Skeleton loading states β pastel shimmer tiles with staggered heights so the loading state already looks like the masonry grid.
- π Blur-up images β Nekos images fade in from their real dominant color; Safebooru images fall back to a generated shimmer placeholder.
- π¨ Fully token-driven theme β every color, radius, and font in the app
traces back to a single
@themeblock inglobals.css. Zero hardcoded hex values in components. - π Light & dark pastel palettes β soft pinks and lavender in both modes, tuned for contrast, not just vibe.
- π¬ Tasteful motion β card entrances, hover lifts, and a discovery/search crossfade via Framer Motion, all defined in one variants file.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS v4 (CSS-first @theme) |
| Components | shadcn UI on Base UI |
| Data fetching | TanStack Query |
| Motion | Framer Motion |
| Discovery feed | Nekos API (random tagged anime art) |
| Search | Safebooru (tag/character indexed booru) |
| Tooling | ESLint Β· Prettier Β· Husky Β· lint-staged |
| Package manager | pnpm |
src/
βββ app/
β βββ api/
β β βββ discovery/route.ts # Same-origin proxy + normalizer for Nekos API
β β βββ search/route.ts # Same-origin proxy + normalizer for Safebooru
β βββ ... # Routes, root layout, providers, global theme
βββ components/
β βββ ui/ # shadcn primitives (button, input, badge, skeleton)
β βββ layout/ # Header, footer
β βββ search/ # Search bar
β βββ gallery/ # Masonry grid, card, skeleton, empty/error state
βββ config/
β βββ motion-variants.ts # Every Framer Motion variant, one file
βββ hooks/ # use-discovery-feed, use-character-search, use-debounced-value, ...
βββ lib/
β βββ api-paths.ts # Every API endpoint, centralized
β βββ content-safety.ts # NSFW tag blocklist (defense-in-depth, applied server-side)
β βββ image-placeholder.ts
βββ services/ # Thin fetch wrappers around our own /api/* routes
βββ types/
βββ gallery.ts # The unified GalleryImage type both APIs normalize into
- Node.js 20+
- pnpm 9+
git clone https://github.com/<your-username>/anime-gallery.git
cd anime-gallery
pnpm installpnpm devOpen http://localhost:3000.
pnpm build # Production build
pnpm start # Run the production build
pnpm lint # ESLint
pnpm format # Prettier, write mode
pnpm format:check # Prettier, check modeA Husky pre-commit hook runs lint-staged automatically, so staged files
are linted and formatted before every commit.
This project uses Tailwind v4's CSS-first configuration β there's no
tailwind.config.js. Every color, radius, and font is defined once as a
CSS variable inside the @theme block in
src/app/globals.css, then consumed everywhere
through Tailwind utility classes (bg-primary, text-muted-foreground,
rounded-2xl, etc.). Change a value there and it propagates through the
entire app β no component ever hardcodes a color.
The discovery feed and search results come from two structurally different
APIs β Nekos API (a random image endpoint with tags
and a rating field) and Safebooru (a paginated
booru with tag-based search). Neither sends CORS headers, so the browser
can't call them directly β src/app/api/discovery/route.ts and
src/app/api/search/route.ts proxy them server-side (where CORS doesn't
apply) and normalize each response into the same
GalleryImage type before it ever reaches the
client. src/services/*.ts are thin fetch wrappers around our own routes;
hooks, components, and state management only ever work with
GalleryImage[] β swapping either upstream API later means touching one
route handler, nothing else.
The two features are mutually exclusive views, not simultaneous fetches:
src/app/page.tsx derives isSearching from the
debounced query and only enables the matching React Query hook
(useDiscoveryFeed / useCharacterSearch), so the idle view's data stays
cached but stops fetching.
Discovery art is served by Nekos API. Search results are served by Safebooru, a safe-for-work anime imageboard. Both are free and require no API key β please be a good citizen of their infrastructure if you extend this project.
Licensed under the MIT License.