Animap
A full-stack anime discovery platform built on a Turborepo monorepo,
powered by dual-provider API aggregation and a custom fuzzy search engine.
Overview • Features • Architecture • Getting Started • Roadmap
Animap aggregates data from AniList and MyAnimeList (via Jikan) into a single, unified discovery experience. Search, browse by genre, explore trending titles, and manage a personal watch history — all wrapped in a distinctive retro-terminal CRT aesthetic.
This project intentionally prioritizes the discovery experience — rich metadata, intelligent search, curated visual design — over video streaming.
🚀 Live deployment: Coming soon — configured for Vercel with
next-pwaservice worker support.
| Feature | Description |
|---|---|
| 🔥 Trending & Popular feeds | Real-time AniList trending + popularity rankings on homepage |
| 🎠 Hero carousel | Auto-rotating featured anime with poster, metadata, and description cross-fades |
| 🔍 Advanced search | Full-text search with genre, format, status, sort filtering; URL-driven state |
| 🤔 Fuzzy search + correction | Handles misspellings, partial matches, romanization variants; "did you mean?" |
| 🏷️ Genre browsing | 22 genre categories with dedicated filtered views and pagination |
| 📺 Anime detail pages | Synopsis, studio credits, trailer, episode list, character grid, recommendations |
| 🌗 Dark/Light theme | Flash-prevention script + CSS variable-based design tokens |
| 📌 Watch history & favorites | Client-side state via Zustand + localStorage |
| 📱 PWA | Service worker, manifest, Workbox runtime caching |
| 📐 Responsive design | Mobile-first layout with Framer Motion animated navigation drawer |
| Feature | Description |
|---|---|
| 🔀 Dual-provider aggregation | AnimeService façade routes to AniList (GraphQL) + Jikan (REST) |
| 🧠 Custom fuzzy search pipeline | Multi-variant expansion → Fuse.js weighted ranking → relevance scoring |
| ⚡ Server-side caching | unstable_cache with CDN Cache-Control headers (5min search, 2min suggest) |
| 🗄️ Zustand state management | AppStore + PlayerStore with devtools + persist middleware |
| 🎞️ Framer Motion microinteractions | AnimatePresence menu drawer, staggered links, icon morphs |
| 🖥️ CRT terminal aesthetic | CSS scanline overlays, .geometric-box card system, neon glow hover effects |
| 🐳 Docker-ready | Multi-stage Dockerfile with turbo prune, standalone output, non-root runner |
| Layer | Technologies |
|---|---|
| Language | TypeScript 5.9 (strict) |
| Framework | Next.js 16.2 (App Router, React 19, Turbopack) |
| Monorepo | Turborepo + pnpm workspaces |
| Styling | Tailwind CSS 3.4 + CSS custom properties design system |
| Animations | Framer Motion 12 |
| State Management | Zustand 5 (devtools + persist middleware) |
| Data Fetching | TanStack React Query 5 (client), Server Components + fetch (server) |
| API Clients | graphql-request + graphql-tag (AniList), native fetch with rate limiter (Jikan) |
| Search | Fuse.js 7 (fuzzy full-text ranking) |
| Icons | Lucide React |
| UI Primitives | Radix UI (Dialog, Dropdown, Select, Tabs, Toast, Slider, Switch, etc.) |
| PWA | next-pwa (Workbox runtime caching) |
| E2E Testing | Playwright (Chromium, Firefox, WebKit) |
| Linting | ESLint 9 (flat config), Prettier, lint-staged + Husky pre-commit hooks |
| Containerization | Docker (multi-stage), Docker Compose |
| Deployment | Vercel (configured), standalone Next.js output |
anitube/
├── apps/
│ └── Anitube/ # Main Next.js 16 application
│ ├── app/ # App Router pages and layouts
│ │ ├── api/search/ # Search and suggest API route handlers
│ │ ├── anime/[id]/ # Anime detail page (SSR)
│ │ ├── genre/ # Genre index and genre-filtered pages
│ │ ├── search/ # Advanced search page (client-side)
│ │ ├── watch/ # Watch redirect route
│ │ ├── layout.tsx # Root layout with SEO, fonts, providers
│ │ └── globals.css # Design system (CSS variables, scanlines)
│ ├── components/
│ │ ├── anime/ # Detail page components (Header, Episodes, Characters)
│ │ ├── home/ # Homepage components (Hero, AnimeCard, Section)
│ │ ├── layout/ # Header and Footer with Framer Motion
│ │ └── player/ # VideoPlayer with keyboard shortcuts
│ ├── lib/
│ │ ├── store/ # Zustand stores (app-store, player-store)
│ │ ├── query/ # React Query provider and client config
│ │ ├── search/ # Client-side search fetch utilities
│ │ └── constants.ts # Application constants and route definitions
│ ├── e2e/ # Playwright tests (home, search, layout, anime, nav)
│ └── playwright.config.ts # E2E test configuration
├── packages/
│ ├── api/ # @anitube/api — data access layer
│ │ └── src/
│ │ ├── providers/ # AniList (GraphQL) and Jikan (REST) clients
│ │ ├── search/ # Fuzzy search pipeline (normalize, rank, advanced)
│ │ ├── services/ # AnimeService unified facade
│ │ ├── types/ # Canonical and provider-specific type definitions
│ │ └── utils/ # Mappers and pagination helpers
│ ├── ui/ # @anitube/ui — shared component library
│ ├── eslint-config/ # Shared ESLint configurations
│ └── typescript-config/ # Shared TSConfig base files
├── Dockerfile # Multi-stage production build
├── docker-compose.yml # Container orchestration
├── turbo.json # Turborepo pipeline configuration
└── pnpm-workspace.yaml # Workspace package definitions
flowchart TD
Browser(["🌐 Browser"])
SC["Server Components\n(page.tsx, anime/[id]/page.tsx)"]
CC["Client Components\n(SearchContent, HeroCarousel)"]
RQ["TanStack\nReact Query"]
API["/api/search\nRoute Handler"]
Cache["unstable_cache\n5min TTL"]
AS["AnimeService\nFacade"]
AL["AniList Client\n(GraphQL)"]
JK["Jikan Client\n(REST + rate limiter)"]
Browser --> SC
Browser --> CC
SC -->|"direct import at request time"| AS
CC --> RQ --> API --> Cache --> AS
AS --> AL
AS --> JK
AL -->|"graphql-request"| AniListAPI(["graphql.anilist.co"])
JK -->|"fetch + queue"| JikanAPI(["api.jikan.moe/v4"])
style AS fill:#6366f1,color:#fff
style Cache fill:#f59e0b,color:#000
style AL fill:#02a9ff,color:#fff
style JK fill:#1d4ed8,color:#fff
flowchart LR
Q(["User Query"])
N["1. Normalize\nNFKC + strip punctuation"]
V["2. Variant Generation\noriginal · stripped · no-spaces"]
F["3. Fetch Candidates\n3× AniList searches"]
D["4. Deduplicate\nby AniList ID"]
R["5. Fuse.js Ranking\nEN 0.35 · romaji 0.30\nnative 0.25 · synonyms 0.20"]
T["6. Threshold Filter\nscore ≥ 25/100"]
C["7. Query Correction\nscore ≥ 55 → 'did you mean?'"]
OUT(["Ranked Results"])
Q --> N --> V --> F --> D --> R --> T --> C --> OUT
style Q fill:#10b981,color:#fff
style OUT fill:#10b981,color:#fff
style R fill:#6366f1,color:#fff
style C fill:#f59e0b,color:#000
flowchart LR
subgraph Zustand Stores
AS2["AppStore\n─────────────\n• theme\n• watch history\n• favorites\n• continue watching"]
PS["PlayerStore\n─────────────\n• volume\n• playback rate\n• auto-play"]
end
LS[("localStorage")]
AS2 <-->|"persist middleware\n(partialize)"| LS
PS <-->|"persist middleware\n(partialize)"| LS
style AS2 fill:#6366f1,color:#fff
style PS fill:#8b5cf6,color:#fff
style LS fill:#374151,color:#fff
This section summarizes the engineering decisions that make this project non-trivial.
-
💪 Strong TypeScript proficiency — strict typing throughout, discriminated unions for provider types, generic utility functions for mappers and pagination, typed GraphQL operations, and a well-defined public API surface with explicit exports.
-
🧠 Non-trivial search algorithm — multi-variant query expansion, fuzzy full-text ranking with weighted fields, relevance scoring, and query correction. This is a custom search pipeline with real information retrieval concepts, not a simple API passthrough.
-
🧱 Clean architectural separation —
@anitube/apihas zero coupling to React or Next.js. Provider clients, mappers, types, and search logic are independently testable behind a unified service interface. -
🏭 Production patterns — server-side caching with CDN headers, rate-limiting for the Jikan API, error boundaries with loading/error/not-found states per route, SEO metadata per page, flash-prevention for theme persistence.
-
📦 Monorepo competency — Turborepo task graph with
dependsOn, shared TypeScript and ESLint configs, workspace protocol dependencies, Docker withturbo prune --docker. -
🎨 Cohesive design system — CSS custom properties for theming, Tailwind extensions, reusable animation primitives (
.geometric-box,.neon-text,.animate-blink), component-level Framer Motion patterns. -
🧪 E2E test infrastructure — Playwright tests organized by feature domain (home, search, navigation, anime details, layout) with auto-starting dev server configuration.
-
✅ Code quality enforcement — ESLint flat config, Prettier, Husky pre-commit hooks running lint-staged, TypeScript strict mode across all packages.
- Node.js ≥ 18
- pnpm 9.x (
corepack enableto activate) - Docker (optional, for containerized builds)
# Clone the repository
git clone https://github.com/ArDnath/AniMap.git
cd AniMap
# Install dependencies
pnpm install
# Copy environment variables
cp apps/Anitube/.env.example apps/Anitube/.env.local# Start all workspace packages in parallel (API + Next.js app)
pnpm devThe app will be available at http://localhost:3000.
pnpm builddocker compose up --buildpnpm test:e2e # Playwright E2E tests
pnpm lint # Lint all packages
pnpm check-types # TypeScript type check
pnpm format # Prettier formatNo API keys required — both AniList and Jikan are public APIs.
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_APP_URL |
Application base URL | http://localhost:3000 |
NEXT_PUBLIC_API_URL |
Jikan API base URL | https://api.jikan.moe/v4 |
NEXT_PUBLIC_ANILIST_API_URL |
AniList GraphQL endpoint | https://graphql.anilist.co |
Quality-gate configuration files:
tsconfig.json— strict TypeScript across all packages with shared base configseslint.config.mjs— ESLint 9 flat config with Next.js and React internal presets.lintstagedrc.js— runs ESLint fix and Prettier on staged files.husky/pre-commit— gates all commits through lint-staged
⏱️ Rate Limiting (Jikan Client)
The Jikan API enforces ~3 requests/second. JikanClient implements a queue-based rate limiter using promise chaining (this.chain), serializing requests with a minimum interval. This avoids 429 responses without external libraries.
📊 Search Relevance Scoring
Fuse.js scores (0 = perfect, 1 = no match) are converted to a 0–100 relevance scale. Results below 25 are filtered. When fewer than 5 results pass the threshold, unmatched candidates are appended with a baseline score of 10 and flagged as fuzzy matches.
🌗 Theme Flash Prevention
A blocking <script> in <head> reads localStorage before React hydrates and adds the .light class immediately — preventing the dark-to-light flash common in client-side theme implementations.
🔀 Provider Abstraction
AnimeService routes calls to the right provider: AniList for metadata-heavy queries (details, search, trending, genre) and Jikan for MAL-specific data (episodes, recommendations, top rankings). Each API's strengths are fully leveraged.
🎯 Deliberate Simplifications
- No authentication — discovery platform; watch history/favorites are client-side only.
- No database — all data from external APIs; persistence via Zustand + localStorage.
- Video player is a demo — fully functional (keyboard shortcuts, seek, volume, playback speed, fullscreen) but uses a sample URL since this is a discovery platform, not a streaming service.
- Unit and integration tests — Vitest tests for
@anitube/apisearch pipeline, mappers, and service layer - CI pipeline — GitHub Actions for lint, type-check, build, and Playwright E2E on push/PR
- Accessibility audit — ARIA landmarks, focus management, reduced-motion queries, screen reader testing
- Search suggestions UI — wire
/api/search/suggestto a typeahead dropdown in the header - Infinite scroll — replace pagination with intersection-observer-based infinite loading
- Image optimization —
loading="eager"andpriorityon above-the-fold hero images - Error monitoring — integrate Sentry for production error tracking
- Analytics — GA and Vercel Analytics (env placeholders already exist)
Contributions are welcome!
# Fork and clone
git clone https://github.com/<your-username>/AniMap.git
cd AniMap
# Create a feature branch
git checkout -b feat/your-feature
# Install and validate
pnpm install
pnpm lint && pnpm check-types && pnpm build
# Commit (Husky runs lint-staged automatically)
git commit -m "feat: description of change"
# Open a Pull RequestThis project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). See the LICENSE file for full terms.
Built by Ariyaman Debnath