diff --git a/.env.schema b/.env.schema index e53e45b..96f9514 100644 --- a/.env.schema +++ b/.env.schema @@ -1,5 +1,6 @@ -# @generateTypes(lang='ts', path='src/env.d.ts') -# @encryptInjectedEnv +# @generateTsTypes(path='src/env.d.ts') +# @currentEnv=$VARLOCK_ENV +# @encryptInjectedEnv=forEnv(production, preview) # @plugin(@varlock/bitwarden-plugin) # @initBitwarden(accessToken=$BITWARDEN_ACCESS_TOKEN) # @defaultSensitive=inferFromPrefix('NEXT_PUBLIC_') @@ -42,4 +43,4 @@ TURSO_DATABASE_AUTH=bitwarden("50b36813-35ae-4b68-bcfb-b4900011b5aa") # @docs(https://docs.turso.tech/quickstart) # @docs(https://orm.drizzle.team/docs/connect-turso) # @sensitive -TURSO_DATABASE_URL=bitwarden("c71a565f-a617-4dda-a3a1-b49000113ac5") \ No newline at end of file +TURSO_DATABASE_URL=bitwarden("c71a565f-a617-4dda-a3a1-b49000113ac5") diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..9088b18 --- /dev/null +++ b/.env.test @@ -0,0 +1,9 @@ +# Static test fixtures โ€” no Bitwarden/network. Loaded when VARLOCK_ENV=test. +# @disable=not(forEnv(test)) +# --- +GITHUB_CLIENT_ID=test-github-client-id +GITHUB_CLIENT_SECRET=test-github-client-secret +BETTER_AUTH_SECRET=test-better-auth-secret-at-least-32-chars-long +NEXT_PUBLIC_SITE_URL=http://localhost:3000 +TURSO_DATABASE_AUTH=test-turso-auth-token +TURSO_DATABASE_URL=libsql://test.turso.io diff --git a/.gitignore b/.gitignore index 55ed96b..facd160 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules .env* !.env.schema +!.env.test .next .vercel .fallow @@ -15,3 +16,7 @@ tsconfig.tsbuildinfo styled-system styled-system-studio .fallow/cache.bin + + +# kombai +.kombai/canvas diff --git a/README.md b/README.md index d29d5d8..fdf61b7 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,29 @@ A modern web application for browsing and discovering 3D models, built with Next ## ๐Ÿ› ๏ธ Tech Stack -![Next.js](https://img.shields.io/badge/Next.js-16.3.0-black?logo=next.js) +![Next.js](https://img.shields.io/badge/Next.js-16.4.0--canary.2-black?logo=next.js) ![React](https://img.shields.io/badge/React-19.3_canary-61DAFB?logo=react) ![TypeScript](https://img.shields.io/badge/TypeScript-7.0.2-3178C6?logo=typescript) -![Panda CSS](https://img.shields.io/badge/Panda_CSS-2.0.0--beta.12-000000) +![Panda CSS](https://img.shields.io/badge/Panda_CSS-2.0.0--beta.14-000000) ![Drizzle ORM](https://img.shields.io/badge/Drizzle-1.0.0--rc.4-FFE66D?logo=sqlite) -[![Better Auth](https://img.shields.io/badge/Better%20Auth-1.7.0--rc.4-000000?logo=better-auth&logoColor=white)](https://better-auth.com/) -![Biome](https://img.shields.io/badge/Biome-2.5.6-60A5FA?logo=biome) -[![Ultracite](https://img.shields.io/badge/Ultracite-7.10.1-000000?logo=biome&logoColor=60A5FA)](https://github.com/ultracite/ultracite) +[![Better Auth](https://img.shields.io/badge/Better%20Auth-1.7.1-000000?logo=better-auth&logoColor=white)](https://better-auth.com/) +![Biome](https://img.shields.io/badge/Biome-2.5.9-60A5FA?logo=biome) +[![Ultracite](https://img.shields.io/badge/Ultracite-7.10.6-000000?logo=biome&logoColor=60A5FA)](https://github.com/ultracite/ultracite) [![Formatted with Biome](https://img.shields.io/badge/Formatted_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev/) [![Linted with Biome](https://img.shields.io/badge/Linted_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev) -- **Framework**: Next.js 16.3.0 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-96fcba90-20260728`, aged for `bunfig` `minimumReleaseAge`) -- **Styling**: Panda CSS 2.0.0-beta.12 (`@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) +- **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.0-rc.4 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.9.5 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.6 with Ultracite 7.10.1 presets (`ultracite/biome/core`, `react`, `next`); [React Doctor](https://github.com/millionco/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`) +- **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](https://github.com/millionco/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](https://github.com/vercel/next.js/issues/87630), [oven-sh/bun#26508](https://github.com/oven-sh/bun/issues/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](https://varlock.dev/) 1.16.0 with `.env.schema` (`@encryptInjectedEnv`), `@varlock/nextjs-integration` plugin in `next.config.ts`, optional Bitwarden Secrets Manager via `@varlock/bitwarden-plugin` (see `docs/VARLOCK.md`) +- **Environment**: [Varlock](https://varlock.dev/) 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 @@ -37,7 +37,8 @@ A modern web application for browsing and discovering 3D models, built with Next - **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` (`components/offline-indicator.tsx`, `experimental.useOffline` in `next.config.ts`) +- **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 ` +); + +export { MobileMenuButton }; diff --git a/src/components/navbar/mobile-nav-auth.tsx b/src/components/navbar/mobile-nav-auth.tsx new file mode 100644 index 0000000..b261273 --- /dev/null +++ b/src/components/navbar/mobile-nav-auth.tsx @@ -0,0 +1,67 @@ +import { css, cx } from "@styled-system/css"; +import { circle, hstack, square } from "@styled-system/patterns"; +import { FaSignInAlt } from "react-icons/fa"; +import { AuthButtons } from "@/features/auth/components/auth-buttons"; +import { Avatar } from "@/features/auth/components/avatar"; +import { HasAuthSuspense } from "@/features/auth/components/has-auth"; +import { MOBILE_SIGN_IN_LINK } from "./constants"; +import { MobileNavLink } from "./mobile-nav-link"; + +const mobileSignInIconContainerClassName = cx( + circle({ + backgroundColor: "bg.muted", + size: 7, + }), + css({ + alignItems: "center", + display: "inline-flex", + flexShrink: 0, + justifyContent: "center", + }), +); + +const mobileAccountRowClassName = hstack({ + justifyContent: "space-between", + paddingBlock: 3, + paddingInline: 3, +}); + +const mobileAccountLabelClassName = css({ + color: "text.secondary", + fontSize: "sm", + fontWeight: "semibold", +}); + +const MobileSignInLink = () => ( + +