A Next.js application that allows users to explore Rick and Morty characters and save their favorites. Built with Supabase for authentication and database, using Edge Functions to fetch data from the Rick & Morty GraphQL API.
- Frontend: Next.js 16.1+ (App Router), React 19, TypeScript, Tailwind CSS 4
- State Management: TanStack Query v5 (server state, optimistic updates,
useInfiniteQuerywith Load More) - Virtualization: TanStack Virtual v3 (windowed grid for large favorites lists)
- Validation: Zod v4 (runtime schema validation)
- Backend: Supabase (PostgreSQL + Auth)
- Edge Functions: Deno (Supabase Edge Functions, in-memory caching)
- API: Rick & Morty GraphQL API
- Testing: Vitest + React Testing Library (unit/component), Playwright (E2E), Deno test (Edge Functions)
βββ src/ # Frontend (Next.js)
β βββ app/ # App Router pages
β β βββ dashboard/ # Characters list with Load More pagination
β β βββ favorites/ # User favorites with export
β β βββ login/ # Login page
β β βββ signup/ # Sign up page
β β βββ not-found.tsx # Custom 404 page
β βββ components/ # React components
β β βββ CharacterCard.tsx # Character card (favorite + comparison buttons)
β β βββ CharacterModal.tsx # Character details modal (accessible)
β β βββ CharacterFilters.tsx # Search/filter controls (debounced, URL-synced)
β β βββ ComparisonModal.tsx # Side-by-side character comparison (2β3 chars)
β β βββ ErrorBoundary.tsx # Global error boundary
β β βββ Footer.tsx # Global footer with author credit
β β βββ Loading.tsx # Skeleton loaders
β β βββ Navbar.tsx # Navigation with active links
β β βββ PasswordStrength.tsx # Password requirements indicator
β β βββ Toast.tsx # Toast notification system
β β βββ icons/ # Reusable SVG icon components
β βββ lib/ # Utilities & Supabase clients
β β βββ constants.ts # Centralized constants
β β βββ imageLoadQueue.ts # Concurrency-limited off-DOM preloader for CDN avatars
β β βββ queryKeys.ts # Centralized TanStack Query keys
β β βββ hooks/ # Custom React hooks
β β β βββ useDebounce.ts # Debounce hook for values/callbacks
β β β βββ useFavorites.ts # Favorites with TanStack Query optimistic updates
β β β βββ useUrlFilters.ts # URL-synced filter state (clean URLs)
β β β βββ useInfiniteCharactersQuery.ts # Paginated characters query (Load More)
β β β βββ useCurrentUser.ts # Auth user hook
β β β βββ useLock.ts # Lock mechanism for async operations
β β β βββ useThrottledImage.ts # Viewport-gated, concurrency-limited avatar loading
β β βββ providers.tsx # QueryClientProvider wrapper
β β βββ schemas.ts # Zod validation schemas
β β βββ logger.ts # Structured logger
β β βββ supabase/ # Supabase clients
β β βββ client.ts # Browser client
β β βββ server.ts # Server client
β β βββ middleware.ts # Middleware client
β β βββ hooks.ts # useSupabase singleton hook
β βββ test/ # Test setup
β β βββ setup.ts # Vitest global setup
β βββ types/ # TypeScript types
β
βββ supabase/ # Backend (Supabase)
β βββ functions/ # Edge Functions
β β βββ get-characters/ # GraphQL proxy with validation + in-memory cache
β β βββ index.ts # Function handler (exported for testing)
β β βββ index.test.ts # Deno unit tests
β βββ migrations/ # SQL migrations
β
βββ tests/ # E2E tests
β βββ e2e/
β βββ auth.spec.ts # Auth flows (login, signup, redirect)
β βββ favorites.spec.ts # Favorites flows (add, remove, export, filter)
β
βββ vitest.config.ts # Vitest configuration
βββ playwright.config.ts # Playwright configuration
- Node.js 18+
- npm or yarn
- Supabase account
- Supabase CLI (optional, for local development)
git clone <repository-url>
cd mindpal_tasknpm install- Create a new project at supabase.com
- Go to Project Settings β API to get your credentials
- Copy
.env.exampleto.env.localand fill in the values:
cp .env.example .env.localNEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_hereNavigate to your Supabase project's SQL Editor and run the migration file:
-- Copy contents from: supabase/migrations/create_favorite_characters.sqlThis will create:
favorite_characterstable- Row Level Security (RLS) policies
- Necessary indexes
Using Supabase CLI:
# Login to Supabase (will open browser for authentication)
npx supabase login
# Link your local project to remote Supabase project
npx supabase link --project-ref <your-project-ref>
# Deploy the Edge Function (--no-verify-jwt makes it publicly accessible)
npx supabase functions deploy get-characters --no-verify-jwtNote: The
--no-verify-jwtflag is required because this function proxies a public API and doesn't need authentication. Without this flag, you'll get 401 Unauthorized errors.
You can find your project-ref in your Supabase Dashboard URL: https://supabase.com/dashboard/project/<project-ref>
Or manually via Supabase Dashboard:
- Go to Edge Functions in your Supabase Dashboard
- Create a new function named
get-characters - Copy the code from
supabase/functions/get-characters/index.ts - In function settings, disable "Verify JWT"
In your Supabase Dashboard:
- Go to Authentication β Settings
- Enable Email/Password sign-in
- (Optional) Disable email confirmation for easier testing
npm run devOpen http://localhost:3000 in your browser.
| Variable | Description |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Your Supabase anonymous (anon) key |
NEXT_PUBLIC_APP_URL |
(Optional) Canonical app URL for OG metadata |
E2E_TEST_EMAIL |
(E2E only) Test account email |
E2E_TEST_PASSWORD |
(E2E only) Test account password |
PLAYWRIGHT_BASE_URL |
(E2E only) Base URL for Playwright (default: http://localhost:3000) |
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | Foreign key to auth.users |
character_id |
INT | Rick & Morty character ID |
character_name |
TEXT | Character name |
character_image |
TEXT | Character image URL (optional) |
character_status |
TEXT | Character status (Alive/Dead/unknown) |
character_species |
TEXT | Character species |
created_at |
TIMESTAMPTZ | Creation timestamp |
Constraints:
- Unique constraint on
(user_id, character_id) - RLS enabled: users can only access their own favorites
- SELECT: Users can only view their own favorites
- INSERT: Users can only insert favorites for themselves
- DELETE: Users can only delete their own favorites
The get-characters Edge Function acts as a proxy to the Rick & Morty GraphQL API:
- Fetches paginated character data
- Validates input parameters against a strict whitelist
- In-memory caching (5-minute TTL) with
X-Cache: HIT/MISSresponse header - Adds
Cache-Control: public, s-maxage=300, stale-while-revalidate=60for CDN caching - Handles errors gracefully
- Returns formatted JSON response
- Exports
validateFilterandhandleRequestfor unit testing (Deno test)
Endpoint: POST /functions/v1/get-characters
Request body:
{
"page": 1,
"filter": {
"name": "Rick",
"status": "Alive"
}
}-
GraphQL via Edge Function: All Rick & Morty API requests go through Supabase Edge Functions, not directly from the frontend. This provides:
- Better security
- Rate limiting control
- Server-side error handling
- Input validation (filter values validated against whitelist)
- In-memory response caching (5-minute TTL, Cache-Control headers)
-
TanStack Query v5: Server state managed with
useQuery,useMutation, anduseInfiniteQuery. Provides automatic cache invalidation, background refetching, and optimistic updates for favorites. -
Load More Pagination: Dashboard uses
useInfiniteCharactersQuery(TanStack QueryuseInfiniteQuery) and appends the next page of 20 characters on an explicit Load More button click. Loading in user-driven batches keeps avatar requests well under the CDN's burst limit (vs. auto-fetch-on-scroll, which fired dozens of concurrent requests and tripped HTTP 429). -
URL-Synced Filters:
useUrlFilterskeeps filter state in URL search params. Empty params are removed to keep URLs clean. This enables deep linking and back-button support. -
Character Comparison: Select 2β3 characters using the compare button on each card. A floating action bar appears and opens
ComparisonModalshowing a side-by-side attribute table. -
Export Favorites: Favorites can be exported as JSON or CSV via a dropdown button. Pure client-side β uses
Blob+URL.createObjectURL, no server round-trip. -
Virtualized Favorites Grid: When the favorites list exceeds 50 items, TanStack Virtual renders only visible rows, preventing DOM bloat.
-
Supabase SSR: Using
@supabase/ssrfor proper server-side rendering and cookie-based authentication in Next.js App Router. -
Row Level Security: Database-level security ensures users can only access their own data, even if the client is compromised.
-
Middleware Protection: Routes
/dashboardand/favoritesare protected at the middleware level, redirecting unauthenticated users to login. -
Debounced Filters: Search input uses debounce (300ms delay) to reduce API calls while typing.
-
Accessibility: Modal components include focus trap, ARIA attributes, and keyboard navigation support.
-
Throttled Avatar Loading: The Rick & Morty image CDN rate-limits by burst (~10 concurrent requests load cleanly, 20+ trip HTTP 429, and it stays banned while hammered). Avatars are served straight from the CDN (
next/imagewithunoptimized, no optimizer proxy) and gated throughuseThrottledImage(IntersectionObserver, so only near-viewport images are requested) on top ofimageLoadQueueβ an off-DOMnew Image()preloader with a concurrency cap of 6 that hands the visible<img>an already-cached URL (instant paint, no flicker). On a 429 a global circuit breaker pauses all fetching for a growing cooldown instead of retrying per-image, so the CDN ban window can clear.
- β User authentication (Sign up / Login)
- β Protected routes with middleware
- β Character listing with Load More pagination (loads next page on click)
- β Add/Remove favorites (optimistic updates via TanStack Query)
- β Favorites page with virtual grid (>50 items)
- β Row Level Security (users own data only)
- β Edge Function for GraphQL proxy with validation + in-memory caching
- β Load More β characters load in batches of 20 on an explicit button click
- β URL-synced Filters β name/status/species reflected in URL; empty params removed (clean URLs)
- β Character Comparison β select 2β3 characters for side-by-side comparison modal
- β Export Favorites β download your favorites list as JSON or CSV
- β Character Details Modal - Click on any card to see detailed info
- β Toast Notifications - Feedback on add/remove favorites
- β Active Navigation Links - Visual indication of current page
- β Custom 404 Page - Rick & Morty themed error page
- β Skeleton Loading - Animated loading cards
- β Password Strength Indicator - Visual password requirements
- β Responsive Design - Mobile-friendly
- β Dark Theme - Modern dark UI
- β TanStack Query v5 β server state, cache, optimistic updates
- β TanStack Virtual v3 β windowed grid for large favorites lists
- β Zod Validation β runtime schema validation at boundaries
- β Error Boundary β Global error handling with recovery UI
- β Debounced Search β Auto-apply filters after 300ms typing pause
- β Operation Locking β Prevents double-click race conditions
- β Accessible Modals β Focus trap, ARIA attributes, keyboard navigation
- β Reusable Icons β SVG icon components library
- β Centralized Constants β No magic strings
Characters grid with filters, Load More pagination, and clickable cards.

Detailed character information including status, species, gender, origin, location, and episode count.

Personal collection with local search and status filter.

# Unit and component tests (Vitest + React Testing Library)
npm test
# Run tests with coverage report
npm run test:coverage
# E2E tests (Playwright) β requires a running dev server or uses webServer config
npm run test:e2e
# Edge Function tests (Deno)
deno test supabase/functions/get-characters/index.test.ts
# Type-check and lint
npm run build
npm run lintTest coverage includes:
- Hook unit tests:
useDebounce,useFavorites - Component tests:
CharacterCard,CharacterFilters,CharacterModal - E2E auth flow: login, signup, redirect protection
- E2E favorites flow: add/remove favorites, search, filter, export
- Edge Function:
validateFilterandhandleRequesthandler tests
# Login to Supabase
npx supabase login
# Link project (run once per project)
npx supabase link --project-ref <your-project-ref>
# Deploy Edge Function
npx supabase functions deploy get-characters --no-verify-jwt
# Push database migrations
npx supabase db push
# View function logs
npx supabase functions logs get-charactersMIT


