Skip to content

fringe4life/3dmodels

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

124 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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.2.6 with App Router, Cache Components, and typed routes (typedRoutes)
  • Language: TypeScript 6.0.3 with React 19.3 canary
  • Styling: Panda CSS 1.11.1 (@pandacss/dev, panda.config.ts); generated styled-system/ from panda codegen (gitignored; run via bun install / prepare); imports use the @styled-system/* path alias (tsconfig.json); global view transitions and @layer rules in src/app/index.css; Biome CSS parser with tailwindDirectives (Tailwind v4 directive syntax) for layered CSS
  • Database: Neon (PostgreSQL) with Drizzle ORM 1.0.0-rc.2
  • Authentication: Better Auth 1.6.11 with email/password and GitHub OAuth, cookie caching enabled, ElysiaJS API backend
  • Search Params: nuqs 2.8.9 for type-safe URL state management; listing canonical URLs use nuqs/server loaders/serializers (features/pagination/listing-canonical.ts) for SEO metadata
  • Linting & Formatting: Biome 2.4.15 with Ultracite 7.7.0 presets (ultracite/biome/core, react, next); React Doctor on PRs (.github/workflows/react-doctor.yml, react-doctor.config.json)
  • Type Checking: tsgo (TypeScript Native Preview)
  • Package Manager: Bun
  • Build Tool: Turbopack for dev and build; experimental view transitions, MCP server, and cached navigations (next.config.ts); env types from Varlock (.env.schema, src/env.d.ts), not Next typedEnv
  • Environment: Varlock 1.2.0 with .env.schema, @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.0 for server action and form schemas

πŸš€ 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.)
  • Responsive Design: Optimized for desktop, tablet, and mobile devices
  • Smooth Page Transitions: View Transitions API with composable fade and slide animations for pagination
  • Type-Safe Database: Full TypeScript support with Drizzle ORM
  • Performance Optimized: Caching for frequently accessed data
  • Modern Stack: Built with Next.js 16.2.6, 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 hero-image-square.png referenced by src/lib/hero-image.ts. Supplemental docs live in docs/ (for example AUTH_SETUP.md, VARLOCK.md, PSEUDO_CLASS_TRANSITIONS.md, PERFORMANCE_IMPROVEMENTS.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 codegen) so imports like @styled-system/css resolve. Root tooling includes react-doctor.config.json 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, view-transition animations
β”‚   β”œβ”€β”€ 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
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ auth/                     # Authentication feature
β”‚   β”‚   β”œβ”€β”€ actions/              # Server actions
β”‚   β”‚   β”‚   β”œβ”€β”€ sign-in-action.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ sign-out-action.ts
β”‚   β”‚   β”‚   └── sign-up-action.ts
β”‚   β”‚   β”œβ”€β”€ 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
β”‚   β”‚   β”œβ”€β”€ constants.ts          # Auth validation constants
β”‚   β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   β”‚   └── get-user.ts
β”‚   β”‚   └── types.ts              # Auth type definitions
β”‚   β”œβ”€β”€ categories/               # Categories feature
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ categories-block-transition.tsx
β”‚   β”‚   β”‚   └── categories-nav.tsx
β”‚   β”‚   β”œβ”€β”€ constants.ts          # ALL_CATEGORIES, CATEGORY_LIST_ITEMS, not-found metadata
β”‚   β”‚   β”œβ”€β”€ types.ts
β”‚   β”‚   └── queries/
β”‚   β”‚       β”œβ”€β”€ get-all-categories.ts
β”‚   β”‚       β”œβ”€β”€ get-all-category-slugs.ts
β”‚   β”‚       └── get-category-by-slug.ts
β”‚   β”œβ”€β”€ models/                   # Models feature
β”‚   β”‚   β”œβ”€β”€ actions/
β”‚   β”‚   β”‚   └── likes.ts
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-client.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-count.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-server.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-button-skeleton.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ heart-like-optimistic.ts
β”‚   β”‚   β”‚   β”‚   └── likes-count-transition.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ model-card.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ model-card-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ model-detail.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-grid-skeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ models-not-found.tsx
β”‚   β”‚   β”‚   └── models-view.tsx
β”‚   β”‚   β”œβ”€β”€ constants.ts
β”‚   β”‚   β”œβ”€β”€ dal/
β”‚   β”‚   β”‚   β”œβ”€β”€ get-models.ts     # `{ result, isAuthenticated }`; search + user, batched likes
β”‚   β”‚   β”‚   └── search-models.ts  # Unified listing/search (optional query + category)
β”‚   β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   β”‚   β”œβ”€β”€ get-all-model-slugs.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ get-model-by-slug.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ get-model-with-like-status.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ get-models-count.ts
β”‚   β”‚   β”‚   └── get-models-list.ts
β”‚   β”‚   └── types.ts
β”‚   └── pagination/
β”‚       β”œβ”€β”€ components/
β”‚       β”‚   β”œβ”€β”€ pagination-button.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-offset-transition.tsx
β”‚       β”‚   β”œβ”€β”€ pagination-skeleton.tsx
β”‚       β”‚   └── pagination.tsx
β”‚       β”œβ”€β”€ dal/
β”‚       β”‚   └── paginate-items.ts
β”‚       β”œβ”€β”€ utils/
β”‚       β”‚   └── to-paginated-result.ts
β”‚       β”œβ”€β”€ listing-canonical.ts
β”‚       β”œβ”€β”€ pagination-search-params.ts
β”‚       β”œβ”€β”€ constants.ts
β”‚       └── types.ts
β”œβ”€β”€ constants.ts                  # Shared constants (EMPTY_LIST_LENGTH)
β”œβ”€β”€ components/                   # Shared/generic components
β”‚   β”œβ”€β”€ form/
β”‚   β”‚   β”œβ”€β”€ field-errors.tsx
β”‚   β”‚   β”œβ”€β”€ form-error.tsx
β”‚   β”‚   β”œβ”€β”€ input.tsx
β”‚   β”‚   β”œβ”€β”€ label.tsx
β”‚   β”‚   β”œβ”€β”€ reset-button.tsx
β”‚   β”‚   └── submit-button.tsx
β”‚   β”œβ”€β”€ button.tsx
β”‚   β”œβ”€β”€ generic-component.tsx
β”‚   β”œβ”€β”€ nav-link.tsx
β”‚   β”œβ”€β”€ not-found/
β”‚   β”‚   β”œβ”€β”€ unsuccessful-state-list-item.tsx
β”‚   β”‚   └── unsuccessful-state.tsx
β”‚   β”œβ”€β”€ pill.tsx
β”‚   β”œβ”€β”€ scroll-progress.tsx
β”‚   β”œβ”€β”€ skeleton.tsx              # Shared loading skeleton primitive
β”‚   β”œβ”€β”€ 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
β”‚   β”‚   β”œβ”€β”€ relations.ts
β”‚   β”‚   └── index.ts
β”‚   β”œβ”€β”€ seed-data/
β”‚   β”‚   β”œβ”€β”€ categories.ts
β”‚   β”‚   └── models.ts
β”‚   β”œβ”€β”€ seed.ts
β”‚   β”œβ”€β”€ drop-tables.ts
β”‚   └── index.ts
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ api.ts
β”‚   β”œβ”€β”€ auth.ts
β”‚   β”œβ”€β”€ auth-client.ts
β”‚   β”œβ”€β”€ date.ts
β”‚   └── hero-image.ts
β”œβ”€β”€ types/
β”‚   └── index.ts
β”œβ”€β”€ utils/
β”‚   β”œβ”€β”€ cache-invalidation.ts
β”‚   β”œβ”€β”€ sanitise-name.ts
β”‚   β”œβ”€β”€ to-action-state.ts
β”‚   └── try-catch.ts
β”œβ”€β”€ 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/: All model-related components, actions, queries, and DAL
  • features/categories/: All category-related components and data queries
  • features/pagination/: Pagination utilities, types, 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/seed-data/: Explicitly named seed data files

Performance Optimizations

  • NuqsAdapter: Scoped to /3d-models layout only (not root layout) for reduced overhead on routes that don't use URL state management
  • Font Loading: Only required font weights are loaded (Albert Sans: 400,500,600,700; Montserrat Alternates: 400,600,700)
  • 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> type helper used consistently across all query functions for nullable return types; centralized type definitions in src/types/index.ts and feature-specific types.ts files for better organization and reusability
  • 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

πŸš€ Getting Started

Prerequisites

  • Bun (recommended) or a current Node.js LTS
  • Neon database account (or any PostgreSQL database)
  • 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 codegen to generate styled-system/, plus Husky). If codegen ever needs a manual rerun: bunx panda codegen.

  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"
    
    DATABASE_URL="your-neon-database-connection-string"

    Run bun run env:typegen after changing .env.schema to refresh src/env.d.ts. Typed access uses import { ENV } from "varlock/env". 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: URL-friendly identifier (unique)

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 provider accounts (GitHub)
  • session: User sessions with cookie caching
  • verification: Email verification tokens

πŸ—„οΈ Database Operations

Available Scripts

  • 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.2 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, including complex conditions with OR: [], AND: [], NOT: {}, and column filters like { column: { eq: value, ilike: pattern } } for better type safety and developer experience
  • Count queries: Count queries use db.$count() (RQBv2), with where conditions passed using SQL builder syntax (and(), or(), ilike(), etc.) since $count accepts SQL builder conditions
  • 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) and composed in higher-level DAL functions (get-models.ts, search-models.ts). Both helpers support optional searchPattern and category parameters for flexible querying
  • Note: Better Auth's drizzleAdapter currently has compatibility issues with RQBv2, showing errors about unknown relational filter fields (e.g., "decoder"). Experimental joins have been disabled for Drizzle v1 compatibility. Both email/password and GitHub OAuth authentication are fully functional. The application will continue using RQBv2 for queries as Better Auth is expected to update their adapter soon. Better Auth is mounted on ElysiaJS at /api/[[...slugs]]/route.ts with basePath /api/auth; OpenAPI documentation includes auth routes 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
  • 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, and listing. The function uses helper functions getModelsList and getModelsCount which support optional search and category parameters
  • Like Status: getHasLikedStatus uses "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: Centralized utilities in utils/cache-invalidation.ts with on-demand invalidation via invalidateModel()
  • 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 semantic tokens and preset utilities (panda.config.ts, @pandacss/preset-panda, typography preset); orange accent and shared patterns (e.g., navLink) live in config
  • 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)
  • 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/components/models-grid - Grid layout for model cards
  • features/models/components/models-not-found - Cached component for displaying no search results with helpful suggestions
  • features/models/components/models-view - Shared server shell: Suspense + async inner that awaits getModels; pagination uses PaginationOffsetTransition for directional View Transitions
  • features/pagination/components/pagination - Reusable pagination with nuqs integration and View Transition support
  • features/pagination/components/pagination-button - Page/limit control button used by pagination
  • features/models/components/heart-button/heart-button-client - Client component with form action, unified optimistic like/count state, View Transition types for count changes
  • features/models/components/heart-button/likes-count-transition - Wraps like count with ViewTransition update names for increase/decrease
  • features/models/components/heart-button/heart-button-server - Server component for detail pages (resolves like status server-side)
  • features/models/components/heart-button/heart-button-skeleton - Loading skeleton for heart button
  • components/search-input/search-input - Model search with nuqs URL state; 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 - Navbar parallel route with auth integration
  • app/@navbar/error.tsx - Error boundary for navbar with retry functionality
  • app/@footer/default - Footer parallel route with copyright
  • components/nav-link - NavLink (link with active state) and NavLinkListItem (li + NavLink); matching (includes or endsWith), border position (bottom or left) (client component)
  • components/top-link - Top-of-page control used in layouts
  • features/auth/components/auth-buttons - Authentication buttons with user avatar (GitHub image priority, icon fallback)
  • features/auth/components/auth-buttons-skeleton - Navbar auth slot loading state
  • 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/button - Shared button styled with Panda variants
  • components/form/input - Text input with consistent field styling
  • components/form/label - Accessible labels for form fields
  • 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 skeleton primitive for loading placeholders
  • components/suspend - Suspense helper component
  • components/generic-component - Generic wrapper for collections

Authentication & Data Access

  • lib/auth - Better Auth configuration with email/password and GitHub OAuth
  • lib/auth-client - Better Auth client instance for client-side usage
  • features/auth/actions - Sign-in, sign-up, and sign-out server actions with Valibot validation
  • features/auth/components/has-auth - Generic auth component with session provider and Suspense wrapper
  • features/auth/constants - Validation constants (password length, email length, name length limits)
  • features/auth/queries/get-user - User query with cache directives (returns user from session)
  • features/auth/components/sign-in-button - GitHub OAuth sign-in button
  • utils/to-action-state - Action state utilities for consistent server action responses
  • 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 .cursor/rules/ultracite.mdc)
  • React Doctor: React/Next.js diagnostics on pull requests; run locally with bun run react-doctor
  • tsgo: TypeScript type checking
  • TypeScript: Static type checking

Available Scripts

  • prepare (automatic on bun install) β€” Panda styled-system/ codegen and Husky setup
  • bun run dev - Start development server (Turbopack)
  • bun run dev:inspect - Start development server with Node.js inspector
  • 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 (Turbopack)
  • bun run build:debug - Build with debug prerender information
  • bun run start - Start production server
  • bun run test - Run tests (Bun test runner)
  • 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 β€” test preload (tests/setup/test-preload.ts) registers Happy DOM globals and stubs server-only for component tests
  • 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 tsgo type checking
  • bun run typegen - Generate Next.js routes and run tsgo (noEmit)
  • bun run env:typegen - Regenerate src/env.d.ts from .env.schema (Varlock)
  • 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 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 locally (react-doctor.config.json)

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. If you use Bun on Vercel, set bunVersion: "1.x" and buildCommand: "bun --bun run next build" in vercel.json. Ensure Bun runtime is >= 1.3.7 for Cache Components.

Environment Variables

Mirror .env.schema: NEXT_PUBLIC_SITE_URL, BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, DATABASE_URL, 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. Update src/db/seed-data/categories.ts with new category data
  2. Run bun run db:seed to update the database

Cache Management

  • Use centralized cache invalidation utilities in utils/cache-invalidation.ts
  • Functions: invalidateAllModels(), invalidateModel(slug), invalidateCategory(slug)
  • Cache tags provide granular control over what gets invalidated
  • Automatic cache invalidation on data mutations (e.g., toggleLike invalidates model cache)
  • Session cache uses "use cache: private" directive with cacheTag("session") for responsive auth state
  • Like status uses getHasLikedStatus 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, Tailwind CSS, and Drizzle ORM. Features category filtering, authentication, like/dislike, and view transitions.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors