Skip to content

Latest commit

Β 

History

149 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

3D Models Gallery

A modern web application for browsing and discovering 3D models, built with Next.js, TypeScript, and Drizzle ORM.

πŸ› οΈ Tech Stack

Next.js React TypeScript Panda CSS Drizzle ORM Better Auth Biome Ultracite Formatted with Biome Linted with Biome

  • Framework: Next.js 16.4.0-canary.2 with App Router, Cache Components, React Compiler, typed routes (typedRoutes), experimental useOffline, and root maxDuration = 45 (platform hard kill ceiling)
  • Language: TypeScript 7.0.2 with React 19.3 canary (19.3.0-canary-eb8feb71-20260814, aged for bunfig minimumReleaseAge)
  • Styling: Panda CSS 2.0.0-beta.14 (@pandacss/dev, @pandacss/preset-base, @pandacss/preset-panda, @pandacss/preset-typography, panda.config.ts); generated styled-system/ from panda build (gitignored; run via bun install / prepare); imports use the @styled-system/* path alias (tsconfig.json); @layer stack in src/app/index.css; view-transition animations colocated via Panda helpers / ViewTransition (duration tokens + group where needed); shared Skeleton uses shimmer CSS vars (color / highlightColor props)
  • Database: Turso (libSQL / SQLite) with Drizzle ORM 1.0.0-rc.4 (dialect: "turso", @libsql/client)
  • Authentication: Better Auth 1.7.1 with email/password and GitHub OAuth, cookie caching enabled, ElysiaJS API backend; Drizzle adapter uses relations-v2 with experimental joins (provider: "sqlite"); account identity keyed by (issuer, accountId)
  • Search Params: nuqs 2.10.0 for type-safe URL state (query, page, limit, sort); NuqsAdapterBoundary wraps the 3d-models layout so search works on index and category routes; listing canonical URLs use nuqs/server loaders/serializers (features/pagination/listing-canonical.ts) with clearOnDefault for SEO metadata; model detail from return paths are allowlisted via features/pagination/listing-path.ts
  • Linting & Formatting: Biome 2.5.9 with Ultracite 7.10.6 presets (ultracite/biome/core, react, next); React Doctor on PRs and pushes to main (.github/workflows/react-doctor.yml, pinned actions, doctor.config.ts) and on staged TS/TSX via Husky + lint-staged (type, react-doctor:staged); Cursor agent hooks in .cursor/hooks.json (afterFileEdit: Ultracite fix skipping unused-import removal + test:affected; stop: full fix, fallow audit, full test)
  • Type Checking: TypeScript 7 via tsc (bun run type / typegen); Next build uses project-local tsc (experimental.useTypeScriptCli in next.config.ts) because TS 7 has no JS compiler API
  • Package Manager: Bun (install, tests, Drizzle scripts, prepare); bunfig.toml sets minimumReleaseAge (3 days)
  • Next.js runtime: Bun is the desired runtime (bun --bun for inspect / debug build). Temporarily, dev, production build, and start run Next on Node (next dev, bun varlock run -- next build, bun run next start) because Next.js 16 Cache Components + bun --bun can surface spurious AbortError unhandled rejections during prerender. Plan to re-enable bun --bun for all Next scripts once Bun/Next compatibility improves (see vercel/next.js#87630, oven-sh/bun#26508).
  • Build Tool: Turbopack for dev and build; partialPrefetching, experimental view transitions, MCP server, cached navigations, appNewScrollHandler, Turbopack filesystem caches (turbopackFileSystemCacheForDev / ForBuild), and turbopackRustReactCompiler (next.config.ts); env types from Varlock (.env.schema, src/env.d.ts), not Next typedEnv
  • Environment: Varlock 1.17.0 with .env.schema (@currentEnv=$VARLOCK_ENV, @encryptInjectedEnv=forEnv(production, preview)), committed .env.test fixtures for Bun tests (no Bitwarden fetch), @varlock/nextjs-integration plugin in next.config.ts, optional Bitwarden Secrets Manager via @varlock/bitwarden-plugin (see docs/VARLOCK.md)
  • Validation: Varlock for environment; Valibot 1.4.2 for server action and form schemas; model slugs validated via slugify idempotency (lib/slugify.ts + isModelSlug in db/brands.ts)

πŸš€ Features

  • Browse 3D Models: View a curated collection of 3D models across various categories
  • Category Filtering: Filter models by category (3D Printer, Art, Education, Fashion, etc.)
  • Sort Controls: Sort listings by A-Z, Popular, or Recent via nuqs sort search param (default A-Z omitted from URL)
  • Search across listings: Search bar lives in ModelsGridHeader (index + category routes); grid title shows Results for "{query}" via nuqs when a query is present
  • Model detail back link: Detail pages restore the prior listing via allowlisted from query (features/models/back-link/) with runtime prefetch under Partial Prefetching
  • Shimmer skeletons: Shared Skeleton shimmer (CSS vars / color props) for listing and detail loading states
  • Offline indicator: Navbar OfflineBanner via Next.js experimental useOffline β€” persistent pill with a wifi-off icon and container-responsive status detail; visible on all breakpoints (components/offline-indicator.tsx)
  • Mobile navigation: Below sm (640px), navbar uses native <button popovertarget> + popover="auto" menu; the compact icon logo is used below xs (480px), with the full wordmark from xs upward (src/components/navbar/)
  • Responsive Design: Optimized for desktop, tablet, and mobile devices
  • Smooth Page Transitions: View Transitions API with composable fade and slide animations for pagination (full-viewport slide on small screens, compact slide from md)
  • Type-Safe Database: Full TypeScript support with Drizzle ORM
  • Performance Optimized: Caching for frequently accessed data; listing DAL combines cache + timeout abort signals (utils/with-abort.ts, ABORT_TIMEOUT_MS)
  • Modern Stack: Built with Next.js 16.3, TypeScript, and Panda CSS
  • Feature-Based Architecture: Well-organized codebase with clear separation of concerns

Note: Like/dislike functionality with optimistic updates and real-time like count synchronization is fully implemented.

πŸ“ Project Structure

Static assets are served from public/ at the repository root (not under src/), including logos, hero images, and public/img/models/*.jpg thumbnails referenced by seed data. Supplemental docs live in docs/ (for example AUTH_SETUP.md, VARLOCK.md, MODEL_CACHE_SPLIT.md, PSEUDO_CLASS_TRANSITIONS.md, PERFORMANCE_IMPROVEMENTS.md, REACT_STINKY.md). Panda CSS writes generated files to styled-system/ at the repo root (panda.config.ts β†’ outdir); that folder is gitignoredβ€”run bun install (or bunx panda build) so imports like @styled-system/css resolve. Root tooling includes doctor.config.ts and .github/workflows/react-doctor.yml for PR diagnostics.

src/
β”œβ”€β”€ app/                          # Next.js App Router
β”‚   β”œβ”€β”€ @navbar/                  # Parallel route for navbar
β”‚   β”‚   β”œβ”€β”€ default.tsx
β”‚   β”‚   └── error.tsx
β”‚   β”œβ”€β”€ @footer/                  # Parallel route for footer
β”‚   β”‚   └── default.tsx
β”‚   β”œβ”€β”€ 3d-models/                # 3D models routes
β”‚   β”‚   β”œβ”€β”€ @categories/          # Parallel route for categories nav
β”‚   β”‚   β”‚   β”œβ”€β”€ default.tsx
β”‚   β”‚   β”‚   └── error.tsx         # Error boundary for categories
β”‚   β”‚   β”œβ”€β”€ @results/             # Parallel route for search results
β”‚   β”‚   β”‚   β”œβ”€β”€ [...catchAll]/
β”‚   β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ default.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ error.tsx         # Error boundary for results with retry functionality
β”‚   β”‚   β”‚   β”œβ”€β”€ loading.tsx       # Loading state for results
β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”œβ”€β”€ [slug]/               # Individual model page
β”‚   β”‚   β”‚   β”œβ”€β”€ error.tsx         # Error boundary for model detail page
β”‚   β”‚   β”‚   β”œβ”€β”€ not-found.tsx
β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”œβ”€β”€ categories/           # Category-specific pages
β”‚   β”‚   β”‚   └── [categoryName]/
β”‚   β”‚   β”‚       β”œβ”€β”€ error.tsx     # Error boundary for category pages with retry functionality
β”‚   β”‚   β”‚       β”œβ”€β”€ loading.tsx   # Loading state for category pages
β”‚   β”‚   β”‚       β”œβ”€β”€ not-found.tsx
β”‚   β”‚   β”‚       └── page.tsx
β”‚   β”‚   β”œβ”€β”€ layout.tsx            # Models layout
β”‚   β”‚   └── page.tsx              # Models landing page
β”‚   β”œβ”€β”€ about/                    # About page
β”‚   β”‚   └── page.tsx
β”‚   β”œβ”€β”€ (auth)/                   # Authentication group route
β”‚   β”‚   β”œβ”€β”€ layout.tsx            # Centered auth layout
β”‚   β”‚   β”œβ”€β”€ signin/
β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   └── signup/
β”‚   β”‚       └── page.tsx
β”‚   β”œβ”€β”€ api/                      # API routes
β”‚   β”‚   └── [[...slugs]]/
β”‚   β”‚       β”œβ”€β”€ better-auth-openapi.ts  # Better Auth OpenAPI spec for Elysia docs
β”‚   β”‚       └── route.ts          # ElysiaJS handler mounting Better Auth (`basePath` /api/auth)
β”‚   β”œβ”€β”€ index.css                 # Global `@layer` stack (reset β†’ utilities)
β”‚   β”œβ”€β”€ styles.ts                 # Shared Panda `css` / pattern exports for app shells
β”‚   β”œβ”€β”€ icon.png                  # App icon (metadata)
β”‚   β”œβ”€β”€ layout.tsx                # Root layout
β”‚   β”œβ”€β”€ page.tsx                  # Home page
β”‚   β”œβ”€β”€ global-error.tsx          # Root error boundary (App Router)
β”‚   β”œβ”€β”€ robots.ts                 # robots.txt Route Handler
β”‚   └── sitemap.ts                # Sitemap generation
β”œβ”€β”€ assets/
β”‚   └── images/                   # App-local image assets
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ auth/                     # Authentication feature
β”‚   β”‚   β”œβ”€β”€ actions/              # Server actions
β”‚   β”‚   β”‚   β”œβ”€β”€ sign-in-action.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ sign-out-action.ts
β”‚   β”‚   β”‚   └── sign-up-action.ts  # SignUpData type co-located here
β”‚   β”‚   β”œβ”€β”€ components/           # Auth components
β”‚   β”‚   β”‚   β”œβ”€β”€ auth-buttons.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ auth-buttons-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ auth-card.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ auth-footer-link.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ avatar.tsx        # User avatar (GitHub image, fallback icon)
β”‚   β”‚   β”‚   β”œβ”€β”€ has-auth.tsx      # Generic auth component with session provider
β”‚   β”‚   β”‚   β”œβ”€β”€ sign-in-button.tsx
β”‚   β”‚   β”‚   └── sign-in-nav-link.tsx  # Icon-only sign-in NavLink for navbar
β”‚   β”‚   β”œβ”€β”€ auth-types.ts         # Shared auth type definitions
β”‚   β”‚   β”œβ”€β”€ constants.ts          # Auth validation constants
β”‚   β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   β”‚   └── get-user.ts
β”‚   β”‚   └── types.ts              # IsAuthenticated, UserAuthState discriminated union
β”‚   β”œβ”€β”€ categories/               # Categories feature
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ categories-block-transition.tsx
β”‚   β”‚   β”‚   └── categories-nav.tsx
β”‚   β”‚   β”œβ”€β”€ constants.ts          # ALL_CATEGORIES, CATEGORY_LIST_ITEMS, not-found metadata
β”‚   β”‚   β”œβ”€β”€ types.ts              # CategoryName (CategorySlug route param)
β”‚   β”‚   └── queries/
β”‚   β”‚       β”œβ”€β”€ get-all-categories.ts
β”‚   β”‚       β”œβ”€β”€ get-all-category-slugs.ts
β”‚   β”‚       └── get-category-by-slug.ts
β”‚   β”œβ”€β”€ models/                   # Models feature
β”‚   β”‚   β”œβ”€β”€ back-link/            # Detail β†’ listing return path (`from` query)
β”‚   β”‚   β”‚   β”œβ”€β”€ from-search-params.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ model-back-link.tsx
β”‚   β”‚   β”‚   └── model-back-link-skeleton.tsx
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ model-card.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ model-card-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ model-detail.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid-header.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid-header-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid-title.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid-title-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-not-found.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-sort-controls.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-sort-controls-skeleton.tsx
β”‚   β”‚   β”‚   └── models-view.tsx
β”‚   β”‚   β”œβ”€β”€ constants.ts
β”‚   β”‚   β”œβ”€β”€ dal/
β”‚   β”‚   β”‚   β”œβ”€β”€ get-models.ts     # `{ result, query, isAuthenticated }`; search + user, batched likes
β”‚   β”‚   β”‚   └── search-models.ts  # Unified listing/search + abortable cached awaits
β”‚   β”‚   β”œβ”€β”€ sort/                 # Sort sub-feature (nuqs param, order mapping, controls hook)
β”‚   β”‚   β”‚   β”œβ”€β”€ brands.ts         # Valibot branded Sort type; `isSortList` guard
β”‚   β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”‚   └── sort-option.tsx  # Radio + label pill for one sort value
β”‚   β”‚   β”‚   β”œβ”€β”€ constants.ts      # SORT_VALUES, DEFAULT_SORT, SORT_LABELS
β”‚   β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”‚   └── use-sort-query.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ order-for-sort.ts # Drizzle orderBy for alphabetic / popular / recent
β”‚   β”‚   β”‚   └── sort-search-params.ts
β”‚   β”‚   β”œβ”€β”€ likes/                # Likes sub-feature (toggle, status, heart UI)
β”‚   β”‚   β”‚   β”œβ”€β”€ actions/
β”‚   β”‚   β”‚   β”‚   └── toggle-like.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-client.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-count.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-server.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-skeleton.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-icon.tsx
β”‚   β”‚   β”‚   β”‚   └── likes-count-transition.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ dal/
β”‚   β”‚   β”‚   β”‚   └── toggle-like.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-like-optimistic.ts
β”‚   β”‚   β”‚   β”‚   └── use-heart-like.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   β”‚   β”‚   └── like-status.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ constants.ts
β”‚   β”‚   β”‚   └── types.ts
β”‚   β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   β”‚   β”œβ”€β”€ build-models-where.ts  # Shared SQL where builder for list/count
β”‚   β”‚   β”‚   β”œβ”€β”€ get-all-model-slugs.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ get-model-by-slug.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ get-models-count.ts
β”‚   β”‚   β”‚   └── get-models-list.ts
β”‚   β”‚   └── types.ts              # ModelWithLikeStatus, SearchPattern, Category; component props extend IsAuthenticated
β”‚   └── pagination/
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ pagination-button.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-limit-control.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-offset-transition.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-page-control.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-skeleton.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-summary.tsx
β”‚       β”‚   └── pagination.tsx
β”‚       β”œβ”€β”€ dal/
β”‚       β”‚   └── paginate-items.ts
β”‚       β”œβ”€β”€ hooks/
β”‚       β”‚   └── use-pagination-query.ts
β”‚       β”œβ”€β”€ utils/
β”‚       β”‚   └── to-paginated-result.ts
β”‚       β”œβ”€β”€ listing-canonical.ts
β”‚       β”œβ”€β”€ listing-path.ts       # Allowlisted listing href β†’ Route (open-redirect safe)
β”‚       β”œβ”€β”€ pagination-search-params.ts
β”‚       β”œβ”€β”€ constants.ts
β”‚       └── types.ts              # PaginatedResult, PaginationMetadataObject; component props co-located in components/
β”œβ”€β”€ constants.ts                  # EMPTY_LIST_LENGTH, ABORT_TIMEOUT_MS
β”œβ”€β”€ components/                   # Shared/generic components
β”‚   β”œβ”€β”€ form/
β”‚   β”‚   β”œβ”€β”€ field-errors.tsx
β”‚   β”‚   β”œβ”€β”€ form-error.tsx
β”‚   β”‚   β”œβ”€β”€ form-field.tsx        # Shared labeled field + errors (auth ViewTransitions)
β”‚   β”‚   β”œβ”€β”€ input.tsx
β”‚   β”‚   β”œβ”€β”€ label.tsx
β”‚   β”‚   β”œβ”€β”€ reset-button.tsx
β”‚   β”‚   └── submit-button.tsx
β”‚   β”œβ”€β”€ nav-link/
β”‚   β”‚   β”œβ”€β”€ nav-link-list-item.tsx
β”‚   β”‚   β”œβ”€β”€ nav-link-skeleton.tsx
β”‚   β”‚   β”œβ”€β”€ nav-link.tsx          # Suspense-wrapped link with active state (prefetch off by default)
β”‚   β”‚   └── types.ts
β”‚   β”œβ”€β”€ navbar/                   # Sticky header (compact logo below xs; nav split at sm)
β”‚   β”‚   β”œβ”€β”€ navbar.tsx            # Orchestrator: logo, offline, desktop nav, mobile popover
β”‚   β”‚   β”œβ”€β”€ navbar.constants.ts   # MOBILE_NAVIGATION_ID, typed PRIMARY_NAV_LINKS
β”‚   β”‚   β”œβ”€β”€ navbar.types.ts       # NavbarNavLinkConfig (typed Route + NavLink props)
β”‚   β”‚   β”œβ”€β”€ navbar-shell.styles.ts # Header shell + :has() hamburger-open animation
β”‚   β”‚   β”œβ”€β”€ navbar-logo.tsx
β”‚   β”‚   β”œβ”€β”€ desktop-nav.tsx
β”‚   β”‚   β”œβ”€β”€ mobile-menu-button.tsx
β”‚   β”‚   β”œβ”€β”€ mobile-nav-popover.tsx
β”‚   β”‚   β”œβ”€β”€ mobile-nav-link.tsx   # Mobile row link; closePopoverOnClick by default
β”‚   β”‚   β”œβ”€β”€ mobile-nav-auth.tsx
β”‚   β”‚   └── navbar-auth-slot.tsx
β”‚   β”œβ”€β”€ offline-indicator.tsx     # OfflineBanner via next/offline useOffline
β”‚   β”œβ”€β”€ nuqs/
β”‚   β”‚   └── nuqs-adapter-boundary.tsx  # Suspense + NuqsAdapter for listing routes
β”‚   β”œβ”€β”€ button-recipe.ts          # Panda CVA recipe for Button variants
β”‚   β”œβ”€β”€ button.tsx
β”‚   β”œβ”€β”€ generic-component.tsx
β”‚   β”œβ”€β”€ not-found/
β”‚   β”‚   β”œβ”€β”€ unsuccessful-state-list-item.tsx
β”‚   β”‚   └── unsuccessful-state.tsx
β”‚   β”œβ”€β”€ pill.tsx
β”‚   β”œβ”€β”€ scroll-progress.tsx
β”‚   β”œβ”€β”€ skeleton.tsx              # Shared shimmer skeleton (CSS vars / color props)
β”‚   β”œβ”€β”€ search-input/
β”‚   β”‚   β”œβ”€β”€ search-input.tsx
β”‚   β”‚   β”œβ”€β”€ search-input-transition.tsx
β”‚   β”‚   └── search-input-skeleton.tsx
β”‚   β”œβ”€β”€ suspend.tsx
β”‚   └── top-link.tsx
β”œβ”€β”€ db/
β”‚   β”œβ”€β”€ schema/
β”‚   β”‚   β”œβ”€β”€ auth.ts
β”‚   β”‚   β”œβ”€β”€ likes.ts
β”‚   β”‚   β”œβ”€β”€ models.ts             # categories + models tables (sqlite text enum for category slug)
β”‚   β”‚   β”œβ”€β”€ relations.ts
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ migrations/               # Drizzle SQL migrations (drizzle-kit generate)
β”‚   β”œβ”€β”€ seed-data/
β”‚   β”‚   └── models.ts
β”‚   β”œβ”€β”€ brands.ts                 # CategorySlug / ModelSlug / User branded types; `isModelSlug`
β”‚   β”œβ”€β”€ categories.ts             # CATEGORIES constant (source of truth for enum values)
β”‚   β”œβ”€β”€ seed.ts
β”‚   β”œβ”€β”€ drop-tables.ts
β”‚   └── index.ts
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ api.ts
β”‚   β”œβ”€β”€ auth.cli.config.ts        # CLI-only Better Auth config for `auth:generate` (no secrets)
β”‚   β”œβ”€β”€ auth.ts                   # Runtime Better Auth (`server-only`; secrets + plugins)
β”‚   β”œβ”€β”€ auth-client.ts
β”‚   β”œβ”€β”€ date.ts
β”‚   β”œβ”€β”€ hero-image.ts
β”‚   β”œβ”€β”€ slugify.ts                # MODEL_SLUGIFY_OPTIONS + SlugifyOptions
β”‚   └── url.ts                    # Shared URL helpers (nuqs defaultOptions)
β”œβ”€β”€ types/
β”‚   └── index.ts
β”œβ”€β”€ utils/
β”‚   β”œβ”€β”€ cache-invalidation.ts
β”‚   β”œβ”€β”€ sanitise-name.ts
β”‚   β”œβ”€β”€ to-action-state/          # Server action result helpers
β”‚   β”‚   β”œβ”€β”€ form-data-to-safe-payload.ts  # Safe FormData β†’ client payload (#38)
β”‚   β”‚   β”œβ”€β”€ to-action-state.ts
β”‚   β”‚   └── types.ts
β”‚   β”œβ”€β”€ try-catch.ts
β”‚   └── with-abort.ts             # Combine cache/timeout AbortSignals; withAbort helper
β”œβ”€β”€ global.d.ts
└── proxy.ts

πŸ—οΈ Architecture Overview

Feature-Based Organization

The project follows a feature-based architecture where related functionality is co-located:

  • features/models/: Model listing, detail, and search components, queries, and DAL
  • features/models/back-link/: Detail-page back link + allowlisted from search-param helpers
  • features/models/sort/: Sort URL state (sort nuqs param), branded types, and Drizzle orderBy mapping
  • features/models/likes/: Like toggle action, DAL, queries, hooks, and heart-button UI
  • features/categories/: All category-related components and data queries
  • features/pagination/: Pagination utilities, types, listing canonical/path allowlist, and components shared across features
  • features/auth/: Authentication actions, components, queries, and types
  • components/: Shared components used across features (including navigation)

Directory Conventions

  • _ prefix: Private folders that are not part of Next.js routing
  • features/: Feature-based modules with their own components and queries
  • components/: Shared/generic components used across features
  • db/categories.ts: Source-of-truth category list; drives SQLite category slug enum values and Valibot branded slugs in db/brands.ts
  • db/seed-data/: Model seed data only (models.ts)

Performance Optimizations

  • NuqsAdapterBoundary: NuqsAdapter wraps entire 3d-models tree
  • Font Loading: Variable fonts are used as 3+ font weights are used
  • Error Handling: Centralized tryCatch utility for consistent error handling across database queries
  • Cache Components: Uses "use cache", "use cache: remote", and "use cache: private" directives for persistent caching; React cache() is used only for functions called multiple times in the same render pass (e.g., getModelBySlug and getCategoryBySlug called in both generateMetadata and page components)
  • Type Safety: Maybe<T> for nullable query results; UserAuthState discriminated union ({ isAuthenticated: true, user } | { isAuthenticated: false }) from getUser() and HasAuth; shared IsAuthenticated interface extended by models/pagination props; component-specific props co-located next to components where not reused
  • Query Builder: Migrated to Drizzle ORM RQBv2 for simple relational queries (db.query.tableName.findMany/findFirst) with object-based where clauses; complex queries and mutations remain on SQL builder
  • Error Recovery: Error boundaries with error.tsx for failed queries (results, category pages, and model detail pages) with built-in reset() retry functionality and helpful error guidance
  • Database Query Separation: Database queries return raw DatabaseQueryResult<T>; transformation to PaginatedResult<T> happens in higher-level functions using transformToPaginatedResult utility from features/pagination/utils/
  • View Transitions: Composable CSS animations using base fade and slide keyframes with CSS variables for slide distance, enabling smooth directional page transitions (enter-left, exit-left, enter-right, exit-right) for pagination; --slide-distance is responsive (100vw-based on small screens, compact from md)
  • Abortable listing fetches: search-models combines Next cache signal + AbortSignal.timeout(ABORT_TIMEOUT_MS) via utils/with-abort.ts so cancelled navigations drop DB awaits sooner
  • Platform timeout: Root layout exports maxDuration = 45 (literal) so Vercel/Next can apply a hard execution ceiling

πŸš€ Getting Started

Prerequisites

  • Bun for package management, tests, and database scripts
  • A current Node.js LTS (used by next build / next start until bun --bun is re-enabled for those scripts)
  • Turso database (TURSO_DATABASE_URL + TURSO_DATABASE_AUTH)
  • Optional: Bitwarden Secrets Manager machine account token if you use bitwarden() resolvers in .env.schema (see docs/VARLOCK.md)

Installation

  1. Clone the repository

    git clone <repository-url>
    cd 3dmodels
  2. Install dependencies

    bun install

    This runs the prepare lifecycle script (panda build to generate styled-system/, plus Husky). If codegen ever needs a manual rerun: bunx panda build.

  3. Environment Setup Configuration is defined in .env.schema (Varlock). Copy it to .env and fill in values, or use literal strings in place of bitwarden("…") UUIDs for local development. Typical variables:

    # Bootstrap (Bitwarden resolvers in .env.schema)
    BITWARDEN_ACCESS_TOKEN="your-machine-account-token"
    
    NEXT_PUBLIC_SITE_URL="http://localhost:3000"
    
    BETTER_AUTH_SECRET="your-secret-key-here-change-this-in-production"
    
    GITHUB_CLIENT_ID="your-github-oauth-client-id"
    GITHUB_CLIENT_SECRET="your-github-oauth-client-secret"
    
    TURSO_DATABASE_URL="libsql://your-db.turso.io"
    TURSO_DATABASE_AUTH="your-turso-auth-token"

    Run bun run env:typegen after changing .env.schema to refresh src/env.d.ts. Typed access uses import { ENV } from "varlock/env". Tests use committed .env.test (static fixtures; no secret fetch). See docs/VARLOCK.md and docs/AUTH_SETUP.md for Bitwarden, Bun, and Vercel notes.

  4. Database Setup Scripts use varlock run -- so Drizzle and seed commands receive resolved env (see package.json):

    bun run db:push
    bun run db:seed

    Alternatively, migrations (SQL and meta/ snapshots are written to src/db/migrations/ when you run generate; clones may use db:push only until migrations exist):

    bun run db:generate
    bun run db:migrate
    bun run db:seed

    For one-off Drizzle CLI use without the db:* scripts, use the same pattern as package.json (for example varlock run -- bun x drizzle-kit push).

  5. Start the development server

    bun run dev

    Open http://localhost:3000 to view the application.

πŸ“Š Database Schema

Categories Table

  • id: Primary key (auto-increment)
  • displayName: Human-readable category name
  • slug: Category slug text enum (unique); values defined in src/db/categories.ts

Models Table

  • slug: Primary key (text, auto-generated from name)
  • name: Model name (unique)
  • description: Model description
  • likes: Number of likes (counter)
  • image: Image URL
  • categorySlug: Foreign key to categories.slug
  • userId: Foreign key to user.id (cascade delete)
  • dateAdded: Timestamp when model was added

Likes Table

  • id: Primary key (auto-increment)
  • userId: Foreign key to users.id (cascade delete)
  • modelSlug: Foreign key to models.slug (cascade delete)
  • createdAt: Timestamp when like was created
  • Unique constraint on (userId, modelSlug) pair

Authentication Tables (Better Auth)

  • user: User accounts with email/password and OAuth support
  • account: OAuth / credential accounts keyed by (issuer, accountId) unique index
  • session: User sessions with cookie caching
  • verification: Email verification tokens

πŸ—„οΈ Database Operations

Available Scripts

  • bun run auth:generate β€” Regenerate Better Auth Drizzle schema (src/db/schema/auth.ts from src/lib/auth.cli.config.ts)
  • bun run db:generate β€” Generate migrations (varlock run -- bun x drizzle-kit generate)
  • bun run db:migrate β€” Run migrations (varlock run -- bun x drizzle-kit migrate)
  • bun run db:push β€” Push schema (varlock run -- bun x drizzle-kit push --force)
  • bun run db:studio β€” Drizzle Studio (varlock run -- bun x drizzle-kit studio)
  • bun run db:seed β€” Seed database (requires existing users for seeded models)
  • bun run db:drop β€” Drop all tables (development reset)

Database Relations

The application uses Drizzle ORM 1.0.0-rc.4 with defineRelations for type-safe relations:

  • Relations defined using the v1/rc syntax with r.one() and r.many() helpers
  • Relation names avoid conflicts with column names (e.g., modelLikes instead of likes to avoid conflict with models.likes column)
  • All relations exported from schema/relations.ts and included in the database schema

Query Builder (RQBv2)

The application uses Drizzle ORM's Relational Query Builder v2 (RQBv2) for type-safe relational queries:

  • Read queries: All read queries use RQBv2 syntax (db.query.tableName.findMany(), db.query.tableName.findFirst()) with object-based where clauses
  • Count / filter queries: Listing search uses SQL builder (and(), or(), like() + COLLATE NOCASE) since SQLite has no ilike
  • Mutations: Insert, update, and delete operations use the SQL builder syntax (mutations not yet available in RQBv2)
  • Hybrid approach: The codebase uses a hybrid strategy - RQBv2 object syntax for all read queries (including complex conditions with AND/OR arrays), SQL builder for count where conditions and mutations
  • Query organization: Model queries are split into focused functions (get-models-list.ts for listing with RQBv2, get-models-count.ts for counting with SQL builder, build-models-where.ts for shared filter conditions) and composed in higher-level DAL functions (get-models.ts, search-models.ts). Both helpers support optional searchPattern and category parameters; list ordering comes from features/models/sort/order-for-sort.ts via the sort search param
  • Better Auth adapter: Uses @better-auth/drizzle-adapter/relations-v2 with experimental joins (lib/auth.ts runtime, lib/auth.cli.config.ts for generate); provider: "sqlite"; mounted on ElysiaJS at /api/[[...slugs]]/route.ts with basePath /api/auth; OpenAPI via better-auth-openapi.ts

Cache Components

The application uses Next.js Cache Components for optimal performance:

  • Static content is pre-rendered at build time
  • Dynamic content (like authentication state) is rendered at request time
  • Server components use connection() to opt into dynamic rendering when needed (e.g. getUser())
  • Cache invalidation handled by cacheTag utilities
  • Error handling with error.tsx error boundaries for failed queries (categories, results, and category pages with built-in reset() retry functionality)
  • Loading states with loading.tsx for results and category pages

Caching Strategy

The application uses Next.js Cache Components with granular cache tags for efficient invalidation:

  • Models: Cached with models, model-{slug}, and models-category-{slug} tags
  • Categories: Cached at component level with categories tag and cacheLife("max") for pre-rendered HTML output
  • Cache Life: Hours profile for most queries (5 min stale, 1 hour revalidate, 1 day expire), max for static categories (component-level caching)
  • Query Functions: Unified getModels() function uses searchModels() which handles search (with optional query), category filtering, sort order, and listing. The function uses helper functions getModelsList and getModelsCount which support optional search and category parameters; sort maps through orderForSort
  • Like Status: like-status.ts queries use "use cache: private" for user-specific like status (cached on device)
  • Model Lists: get-models.ts adds hasLiked per model after a single batched like query for the page
  • Invalidation: invalidateAllModels() in utils/cache-invalidation.ts (updateTag("models")) β€” simple broad invalidation on like/unlike so counts and sort=popular stay correct; longer-term content/likes split documented in docs/MODEL_CACHE_SPLIT.md
  • Optimistic Updates: Heart button uses useOptimistic for immediate UI feedback with server state synchronization via form actions

🎨 Styling & Components

Design System

  • Tokens & utilities: Panda CSS 2 semantic tokens and preset utilities (panda.config.ts, @pandacss/preset-base, @pandacss/preset-panda, @pandacss/preset-typography); orange accent and shared patterns (e.g., navLink) live in config; treeshakeDesignSystem enabled
  • Typography: Albert Sans + Montserrat Alternates via next/font in root layout; heading font applied in Panda globalCss
  • Layout & spacing: Panda css() / layout patterns (e.g., grid for model grids in src/app/styles.ts)
  • View transitions: Colocated with components via React ViewTransition + Panda view-transition helpers (global VT CSS removed from index.css)
  • Responsive: Mobile-first breakpoints via Panda conditions and component styles

Key Components

Feature Components

  • features/models/components/model-card - Individual model display card
  • features/models/components/model-card-skeleton - Loading skeleton for model cards
  • features/models/components/model-detail - Detailed model view page
  • features/models/back-link/model-back-link - Server back link restoring allowlisted listing from (runtime prefetch={true}; arrowRecipe left chevron micro-interactions)
  • features/models/back-link/from-search-params - modelDetailHref / resolveBackHref with slugify-stable slug checks
  • features/models/components/models-grid - Grid layout for model cards (embeds from on detail links)
  • features/models/components/models-grid-header - Listing header: search input, query-aware title, sort controls
  • features/models/components/models-grid-title - Client title via nuqs (Results for "{query}" or fallback category / default title)
  • features/models/components/models-not-found - Empty search state with the active query in the subheading
  • features/models/components/models-sort-controls - Client fieldset of SortOption radios for A-Z / Popular / Recent (useSortQuery)
  • features/models/components/models-sort-controls-skeleton - Loading skeleton for sort controls
  • features/models/components/models-view - Shared server shell: header outside Suspense, async inner awaits getModels; pagination uses PaginationOffsetTransition
  • features/models/sort/components/sort-option - Single sort radio + label pill
  • features/models/sort/hooks/use-sort-query - nuqs hook for sort with pending state
  • features/models/sort/order-for-sort - Maps sort brand to Drizzle orderBy clauses
  • features/pagination/components/pagination - Reusable pagination with nuqs integration and View Transition support
  • features/pagination/components/pagination-button - Page control button (group for arrow micro-interactions)
  • features/pagination/components/pagination-limit-control - Per-page limit via customizable <select> (appearance: base-select, selectedcontent, picker transitions)
  • features/pagination/components/pagination-offset-transition - Directional View Transition wrapper; responsive --slide-distance
  • features/pagination/components/pagination-page-control - Prev/next page buttons with aria-label and arrowRecipe chevrons
  • features/pagination/components/pagination-summary - Result count / range summary
  • features/pagination/hooks/use-pagination-query - nuqs + View Transition hook for page/limit changes
  • features/pagination/listing-canonical - Canonical path serializer for listing SEO (query, page, limit, sort)
  • features/pagination/listing-path - Allowlisted listing Route validation (/3d-models, category listings)
  • features/models/likes/components/heart-button-client - Client component with useHeartLike hook, optimistic like/count state, View Transition types for count changes
  • features/models/likes/components/heart-icon - Heart glyph styled from HeartVisualState ("liked" | "unliked" | "pending")
  • features/models/likes/components/likes-count-transition - Wraps like count with ViewTransition update names for increase/decrease
  • features/models/likes/components/heart-button-server - Server component for detail pages (resolves like status server-side)
  • features/models/likes/components/heart-button-skeleton - Loading skeleton for heart button
  • features/models/likes/hooks/use-heart-like - Client hook for toggle action, optimistic state, and single visualState
  • components/search-input/search-input - Model search with nuqs URL state (mounted in ModelsGridHeader); Enter flushes current input value; search-input-transition for view transitions
  • features/categories/components/categories-nav - Category filtering sidebar (server component)
  • features/categories/components/categories-block-transition - View transition wrapper for category listing blocks
  • app/3d-models/@categories/error.tsx - Error boundary for categories with built-in retry functionality
  • app/3d-models/@results/error.tsx - Error boundary for search results with retry and error guidance
  • app/3d-models/@results/loading.tsx - Loading state for search results
  • app/3d-models/categories/[categoryName]/error.tsx - Error boundary for category pages with retry and error guidance
  • app/3d-models/categories/[categoryName]/loading.tsx - Loading state for category pages
  • app/3d-models/[slug]/error.tsx - Error boundary for model detail pages with retry and error guidance

Navigation Components

  • app/@navbar/default - Parallel route delegating to shared Navbar
  • app/@navbar/error.tsx - Error boundary for navbar with retry functionality
  • app/@footer/default - Footer parallel route with copyright
  • components/navbar/navbar - Thin orchestrator: NavbarLogo, OfflineBanner, DesktopNav, MobileMenuButton, MobileNavPopover (sm breakpoint)
  • components/navbar/mobile-nav-link - Mobile popover row; closes ancestor popover on navigate
  • components/offline-indicator - OfflineBanner using Next.js useOffline, with persistent reserved navbar placement and container-responsive detail (hidden when online)
  • components/nav-link/nav-link - NavLink with Suspense fallback, active state (includes or endsWith), border position (bottom or left); prefetch={false} by default to reduce network noise (opt-in per link, e.g. model back link)
  • components/nav-link/nav-link-skeleton - Width-matched skeleton for NavLink Suspense fallback
  • components/nav-link/nav-link-list-item - li + NavLink wrapper
  • components/nuqs/nuqs-adapter-boundary - Suspense + NuqsAdapter for the 3d-models layout
  • components/top-link - Top-of-page control used in layouts
  • features/auth/components/auth-buttons - Sign-out control wrapping avatar (authenticated navbar slot)
  • features/auth/components/auth-buttons-skeleton - Navbar auth slot loading state
  • features/auth/components/sign-in-nav-link - Icon-only sign-in link for unauthenticated navbar slot
  • features/auth/components/auth-card - Card shell for sign-in/sign-up pages
  • features/auth/components/auth-footer-link - Footer link between auth screens
  • features/auth/components/avatar - Avatar image with fallback

Shared Components

  • components/arrow-recipe - Panda CVA for directional arrow icons (left/right, compact/default distance, size; group hover/focus/active)
  • components/button - Shared button styled with Panda variants (primary, outline, brandOutline, ghost) and densities
  • components/button-recipe - Button CVA recipe (brandOutline for idle sort; neutral outline for GitHub sign-in)
  • components/form/input - Text input with consistent field styling
  • components/form/label - Accessible labels for form fields
  • components/form/form-field - Shared labeled field shell with field errors and shared ViewTransition names for auth forms
  • components/form/submit-button - Submit control wired for pending state
  • components/form/reset-button - Reset control for forms
  • components/form/field-errors - Field-level error display component with ViewTransition support
  • components/form/form-error - Form-level error display component with ViewTransition support
  • components/not-found/unsuccessful-state - Unified component for not-found and error states with conditional styling based on isError prop
  • components/not-found/unsuccessful-state-list-item - List item component for unsuccessful state suggestions
  • components/pill - Small label component
  • components/scroll-progress - Top-of-page reading progress indicator (client)
  • components/skeleton - Shared shimmer skeleton (color / highlightColor β†’ CSS vars; layout via className)
  • components/suspend - Suspense helper component
  • components/generic-component - Generic wrapper for collections

Authentication & Data Access

  • lib/auth.cli.config - CLI-only Better Auth config for schema generate (no secrets)
  • lib/auth - Runtime Better Auth (server-only; secrets, OAuth, cookies, OpenAPI)
  • lib/auth-client - Better Auth client instance for client-side usage
  • lib/slugify - Shared MODEL_SLUGIFY_OPTIONS / SlugifyOptions for seed + isModelSlug
  • utils/with-abort - Combine AbortSignals and race promises against abort
  • features/auth/actions - Sign-in, sign-up, and sign-out server actions with Valibot validation
  • features/auth/components/has-auth - Renders children(auth) with UserAuthState; HasAuthSuspense wraps in Suspend
  • features/auth/constants - Validation constants (password length, email length, name length limits)
  • features/auth/queries/get-user - User query with React.cache() (returns UserAuthState from session)
  • features/auth/components/sign-in-button - GitHub OAuth sign-in button
  • utils/to-action-state - Action state helpers (to-action-state.ts, types.ts, form-data-to-safe-payload.ts for allowlisted FormData β†’ client)
  • components/form/field-errors - Reusable field error component used in auth forms
  • components/form/form-error - Reusable form-level error component used in auth forms

πŸ”§ Development

Code Quality Tools

  • Biome / Ultracite: Linting and formatting (see biome.json and AGENTS.md)
  • React Doctor: React/Next.js diagnostics on pull requests and main pushes (.github/workflows/react-doctor.yml); pre-commit via lint-staged (bun run type, bun run react-doctor:staged); full local run with bun run react-doctor
  • Cursor hooks: .cursor/hooks.json β€” fast afterFileEdit (Ultracite with --skip=correctness/noUnusedImports, test:affected); heavier stop (full fix, fallow audit, full test)
  • TypeScript 7: Static type checking via tsc (bun run type); Next uses experimental.useTypeScriptCli

Available Scripts

  • prepare (automatic on bun install) β€” Panda styled-system/ build and Husky setup
  • bun run dev - Start development server (Turbopack; Node runtime β€” see Next.js runtime note above)
  • bun run dev:inspect - Start development server with Node.js inspector (bun --bun)
  • bun run next:upgrade - Upgrade Next.js to latest version
  • bun run next:analyze - Analyze Next.js bundle (experimental-analyze)
  • bun run build - Build for production (Node runtime via varlock run -- next build)
  • bun run build:debug - Build with debug prerender information (bun --bun)
  • bun run start - Start production server (Node runtime via bun run next start)
  • bun run test - Run tests (Bun test runner; VARLOCK_ENV=test loads .env.test β€” no Bitwarden)
  • bun run test:affected - Run tests affected by git changes (bun test --changed --pass-with-no-tests; Cursor afterFileEdit)
  • bun run test:watch - Run tests in watch mode
  • bun run test:unit - Run unit tests
  • bun run test:components - Run component tests
  • bun run test:integration - Run integration tests
  • bunfig.toml β€” env = false (Varlock owns .env* resolution); minimumReleaseAge (3 days) with Next/SWC excludes; test preload order: tests/setup/varlock-test-env.ts β†’ varlock/auto-load β†’ tests/setup/test-preload.ts (Happy DOM + server-only stub); tests/setup/next-mocks.ts stubs Next navigation; nuqs components use withNuqsTestingAdapter from nuqs/adapters/testing; integration DB helpers in tests/setup/db-test.ts use Drizzle 1.0 { client } config
  • bun run test:e2e - Run Playwright E2E tests
  • bun run e2e:open - Open Playwright UI
  • bun run e2e:codegen - Playwright codegen (localhost:3000)
  • bun run type - Run Next typegen then tsc type checking
  • bun run typegen - Generate Next.js routes (next typegen)
  • bun run env:typegen - Regenerate src/env.d.ts from .env.schema (Varlock)
  • bun run auth:generate - Regenerate Better Auth Drizzle schema from auth.cli.config.ts
  • bun run db:generate - Generate Drizzle migrations
  • bun run db:migrate - Run Drizzle migrations
  • bun run db:push - Push schema directly to database
  • bun run db:studio - Open Drizzle Studio
  • bun run db:seed - Seed database with initial data
  • bun run db:drop - Drop all tables (development reset)
  • bun run fix - Fix linting/formatting issues with Ultracite/Biome
  • bun run check - Check linting rules with Ultracite/Biome
  • bun run doctor - Run Ultracite doctor diagnostics
  • bun run ultracite:upgrade - Upgrade Ultracite configuration
  • bun run react-doctor - Run React Doctor on the repo (doctor.config.ts)
  • bun run react-doctor:staged - Run React Doctor on staged files only (lint-staged / pre-commit)

Code Style

The project follows a consistent coding style with:

  • ES modules (import/export syntax)
  • TypeScript for type safety
  • Panda CSS for styling (css recipes, semantic tokens)
  • Feature-based organization
  • Component-specific type definitions
  • Proper error handling and logging

πŸš€ Deployment

Vercel (Recommended)

  1. Connect your repository to Vercel
  2. Set environment variables in Vercel dashboard
  3. Deploy automatically on push to main branch
  4. Runtime note: Bun remains the desired package manager and future Next.js runtime. Vercel builds should use the default Next.js build (next build on Node) until bun --bun + Cache Components issues are resolved; vercel.json no longer forces bunVersion / bun --bun run next build. Re-enable when upstream fixes land.

Environment Variables

Mirror .env.schema: NEXT_PUBLIC_SITE_URL, BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, TURSO_DATABASE_URL, TURSO_DATABASE_AUTH, and BITWARDEN_ACCESS_TOKEN when using bitwarden() resolvers. Varlock validates at runtime; types live in src/env.d.ts. See docs/VARLOCK.md for Vercel and Bitwarden.

πŸ“ Data Management

Adding New Models

  1. Update src/db/seed-data/models.ts with new model data (note: userId and likes are omitted from seed data)
  2. Run bun run db:seed to update the database (requires existing users in the database)

Adding New Categories

  1. Add the category to src/db/categories.ts (updates the category slug enum source of truth)
  2. Run bun run db:generate then bun run db:migrate (or bun run db:push in development)
  3. Run bun run db:seed to update the database

Cache Management

  • Use centralized cache invalidation in utils/cache-invalidation.ts
  • Function: invalidateAllModels() β€” expires every entry tagged "models" via updateTag("models")
  • toggleLike calls invalidateAllModels() so list counts and sort=popular stay fresh (simple correctness choice for now)
  • Longer-term split of stable content vs likes (and popular-order tags): see docs/MODEL_CACHE_SPLIT.md
  • Session cache uses "use cache: private" directive with cacheTag("session") for responsive auth state
  • Like status uses features/models/likes/queries/like-status.ts with "use cache: private" for user-specific cache

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes following the feature-based architecture
  4. Run tests and linting
  5. Submit a pull request

πŸ“„ License

This project is licensed under the MIT License.

πŸ†˜ Support

For support and questions:

  • Check the documentation
  • Review existing issues
  • Create a new issue with detailed information

About

Modern 3D models gallery built with Next.js 16, React 19, TypeScript, PandaCSS, and Drizzle ORM. Features category filtering, authentication, like/dislike, and view transitions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages