Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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.4.0--canary.2-black?logo=next.js)
![Next.js](https://img.shields.io/badge/Next.js-16.4.0--canary.8-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.14-000000)
Expand All @@ -15,7 +15,7 @@ A modern web application for browsing and discovering 3D models, built with Next
[![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.4.0-canary.2 with App Router, Cache Components, React Compiler, typed routes (`typedRoutes`), experimental `useOffline`, and root `maxDuration = 45` (platform hard kill ceiling)
- **Framework**: Next.js 16.4.0-canary.8 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`)
Expand Down Expand Up @@ -51,7 +51,7 @@ A modern web application for browsing and discovering 3D models, built with Next

## πŸ“ 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.
Static assets are served from `public/` at the **repository root** (not under `src/`), including logos, hero images, and `public/img/models/*.avif` 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/
Expand Down Expand Up @@ -104,8 +104,6 @@ src/
β”‚ β”œβ”€β”€ 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
Expand Down Expand Up @@ -143,8 +141,10 @@ src/
β”‚ β”‚ β”‚ └── model-back-link-skeleton.tsx
β”‚ β”‚ β”œβ”€β”€ components/
β”‚ β”‚ β”‚ β”œβ”€β”€ model-card.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ model-card.styles.ts # Container-query + subgrid listing card
β”‚ β”‚ β”‚ β”œβ”€β”€ model-card-skeleton.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ model-detail.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ model-detail.styles.ts # Container-query detail layout
β”‚ β”‚ β”‚ β”œβ”€β”€ models-grid.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ models-grid-header.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ models-grid-header-skeleton.tsx
Expand Down Expand Up @@ -176,7 +176,6 @@ src/
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ heart-button-count.tsx
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ heart-button-server.tsx
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ heart-button-skeleton.tsx
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ heart-icon.tsx
β”‚ β”‚ β”‚ β”‚ └── likes-count-transition.tsx
β”‚ β”‚ β”‚ β”œβ”€β”€ dal/
β”‚ β”‚ β”‚ β”‚ └── toggle-like.ts
Expand Down Expand Up @@ -281,6 +280,7 @@ src/
β”‚ β”œβ”€β”€ auth-client.ts
β”‚ β”œβ”€β”€ date.ts
β”‚ β”œβ”€β”€ hero-image.ts
β”‚ β”œβ”€β”€ placeholder-image.ts
β”‚ β”œβ”€β”€ slugify.ts # MODEL_SLUGIFY_OPTIONS + SlugifyOptions
β”‚ └── url.ts # Shared URL helpers (nuqs defaultOptions)
β”œβ”€β”€ types/
Expand Down Expand Up @@ -491,9 +491,9 @@ The application uses Next.js Cache Components with granular cache tags for effic
### Key Components

#### Feature Components
- `features/models/components/model-card` - Individual model display card
- `features/models/components/model-card` - Individual model display card (container-query split + subgrid via `model-card.styles`)
- `features/models/components/model-card-skeleton` - Loading skeleton for model cards
- `features/models/components/model-detail` - Detailed model view page
- `features/models/components/model-detail` - Detailed model view (`model-detail.styles` container queries)
- `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)
Expand All @@ -515,8 +515,7 @@ The application uses Next.js Cache Components with granular cache tags for effic
- `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/heart-button-client` - Client like button with `useHeartLike`, optimistic state, and `_icon` colors 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
Expand Down
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions docs/REACT_STINKY.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,14 @@ Name means opposite of value; `disabled={hasPreviousPage}` only works because of
**Files:**
- `src/features/models/likes/types.ts` (`HeartVisualState`)
- `src/features/models/likes/hooks/use-heart-like.ts`
- `src/features/models/likes/components/heart-icon.tsx`
- `src/features/models/likes/components/heart-button-client.tsx`

`isLiked` + `isNotLiked` + `isPending` were a redundant pair; impossible combos possible.

**Applied:** `HeartVisualState = "liked" | "unliked" | "pending"`. Hook derives one `visualState` (pending wins). `HeartIcon` maps via `Record<HeartVisualState, …>`. `isPending` / `isDisabled` stay on hook for form/button, not paint.
**Applied:** `HeartVisualState = "liked" | "unliked" | "pending"`. Hook derives one `visualState` (pending wins). Button maps via `Record<HeartVisualState, …>` with Panda `_icon` (`& :where(svg)`). `isPending` / `isDisabled` stay on hook for form/button; paint lives on button, not a separate icon component.

- [x] Introduce discriminated `visualState`
- [x] Update hook + icon + client button
- [x] Update hook + client button (`_icon` colors; no `heart-icon`)

---

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "3d-models",
"version": "0.1.0",
"version": "0.9.0",
"private": true,
"type": "module",
"scripts": {
Expand Down Expand Up @@ -66,7 +66,7 @@
"drizzle-orm": "1.0.0-rc.4",
"elysia": "1.4.29",
"file-type": "22.0.1",
"next": "16.4.0-canary.6",
"next": "16.4.0-canary.8",
"nuqs": "2.10.0",
"react": "19.3.0-canary-eb8feb71-20260814",
"react-dom": "19.3.0-canary-eb8feb71-20260814",
Expand Down
19 changes: 19 additions & 0 deletions panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ export default defineConfig({
xs: "30rem", // 480px
xxs: "20rem", // 320px
},
containerNames: ["models-grid", "model-detail", "navbar"],
containers: {
"card-copy": "48rem",
"card-split": "28rem",
"detail-split": "26rem",
"offline-indicator-full": "50ch",
"offline-indicator-small": "20ch",
},
keyframes: {
/** View-timeline enter/exit on narrow viewports (see `ModelCard`) */
animateModelIn: {
Expand Down Expand Up @@ -301,4 +309,15 @@ export default defineConfig({
},
},
},
utilities: {
extend: {
cornerShape: {
group: "Border Radius",
transform(value) {
return { cornerShape: value };
},
values: ["squircle", "bevel"],
},
},
},
});
Binary file added public/hero-image-square.avif
Binary file not shown.
Binary file removed public/hero-image-square.png
Binary file not shown.
Binary file removed public/img/hero-image-square.png
Binary file not shown.
Binary file removed public/img/hero-image.png
Binary file not shown.
Binary file added public/img/models/1.avif
Binary file not shown.
Binary file removed public/img/models/1.jpg
Binary file not shown.
Binary file added public/img/models/10.avif
Binary file not shown.
Binary file removed public/img/models/10.jpg
Binary file not shown.
Binary file added public/img/models/11.avif
Binary file not shown.
Binary file removed public/img/models/11.jpg
Binary file not shown.
Binary file added public/img/models/12.avif
Binary file not shown.
Binary file removed public/img/models/12.jpg
Binary file not shown.
Binary file added public/img/models/13.avif
Binary file not shown.
Binary file removed public/img/models/13.jpg
Binary file not shown.
Binary file added public/img/models/14.avif
Binary file not shown.
Binary file removed public/img/models/14.jpg
Binary file not shown.
Binary file added public/img/models/15.avif
Binary file not shown.
Binary file removed public/img/models/15.jpg
Binary file not shown.
Binary file added public/img/models/16.avif
Binary file not shown.
Binary file removed public/img/models/16.jpg
Binary file not shown.
Binary file added public/img/models/17.avif
Binary file not shown.
Binary file removed public/img/models/17.jpg
Binary file not shown.
Binary file added public/img/models/18.avif
Binary file not shown.
Binary file removed public/img/models/18.jpg
Binary file not shown.
Binary file added public/img/models/19.avif
Binary file not shown.
Binary file removed public/img/models/19.jpg
Binary file not shown.
Binary file added public/img/models/2.avif
Binary file not shown.
Binary file removed public/img/models/2.jpg
Binary file not shown.
Binary file added public/img/models/20.avif
Binary file not shown.
Binary file removed public/img/models/20.jpg
Binary file not shown.
Binary file added public/img/models/21.avif
Binary file not shown.
Binary file removed public/img/models/21.jpg
Binary file not shown.
Binary file added public/img/models/22.avif
Binary file not shown.
Binary file removed public/img/models/22.jpg
Binary file not shown.
Binary file added public/img/models/23.avif
Binary file not shown.
Binary file removed public/img/models/23.jpg
Binary file not shown.
Binary file added public/img/models/24.avif
Binary file not shown.
Binary file removed public/img/models/24.jpg
Binary file not shown.
Binary file added public/img/models/25.avif
Binary file not shown.
Binary file removed public/img/models/25.jpg
Binary file not shown.
Binary file added public/img/models/26.avif
Binary file not shown.
Binary file removed public/img/models/26.jpg
Binary file not shown.
Binary file added public/img/models/27.avif
Binary file not shown.
Binary file removed public/img/models/27.jpg
Binary file not shown.
Binary file added public/img/models/28.avif
Binary file not shown.
Binary file removed public/img/models/28.jpg
Binary file not shown.
Binary file added public/img/models/29.avif
Binary file not shown.
Binary file removed public/img/models/29.jpg
Binary file not shown.
Binary file added public/img/models/3.avif
Binary file not shown.
Binary file removed public/img/models/3.jpg
Diff not rendered.
Binary file added public/img/models/30.avif
Binary file not shown.
Binary file removed public/img/models/30.jpg
Diff not rendered.
Binary file added public/img/models/31.avif
Binary file not shown.
Binary file removed public/img/models/31.jpg
Diff not rendered.
Binary file added public/img/models/32.avif
Binary file not shown.
Binary file removed public/img/models/32.jpg
Diff not rendered.
Binary file added public/img/models/33.avif
Binary file not shown.
Binary file removed public/img/models/33.jpg
Diff not rendered.
Binary file added public/img/models/34.avif
Binary file not shown.
Binary file removed public/img/models/34.jpg
Diff not rendered.
Binary file added public/img/models/35.avif
Binary file not shown.
Binary file removed public/img/models/35.jpg
Diff not rendered.
Binary file added public/img/models/36.avif
Binary file not shown.
Binary file removed public/img/models/36.jpg
Diff not rendered.
Binary file added public/img/models/37.avif
Binary file not shown.
Binary file removed public/img/models/37.jpg
Diff not rendered.
Binary file added public/img/models/38.avif
Binary file not shown.
Binary file removed public/img/models/38.jpg
Diff not rendered.
Binary file added public/img/models/39.avif
Binary file not shown.
Binary file removed public/img/models/39.jpg
Diff not rendered.
Binary file added public/img/models/4.avif
Binary file not shown.
Binary file removed public/img/models/4.jpg
Diff not rendered.
Binary file added public/img/models/40.avif
Binary file not shown.
Binary file removed public/img/models/40.jpg
Diff not rendered.
Binary file added public/img/models/41.avif
Binary file not shown.
Binary file removed public/img/models/41.jpg
Diff not rendered.
Binary file added public/img/models/42.avif
Binary file not shown.
Binary file removed public/img/models/42.jpg
Diff not rendered.
Binary file added public/img/models/43.avif
Binary file not shown.
Binary file removed public/img/models/43.jpg
Diff not rendered.
Binary file added public/img/models/44.avif
Binary file not shown.
Binary file removed public/img/models/44.jpg
Diff not rendered.
Binary file added public/img/models/45.avif
Binary file not shown.
Binary file removed public/img/models/45.jpg
Diff not rendered.
Binary file added public/img/models/46.avif
Binary file not shown.
Binary file removed public/img/models/46.jpg
Diff not rendered.
Binary file added public/img/models/47.avif
Binary file not shown.
Binary file removed public/img/models/47.jpg
Diff not rendered.
Binary file added public/img/models/48.avif
Binary file not shown.
Binary file removed public/img/models/48.jpg
Diff not rendered.
Binary file added public/img/models/49.avif
Binary file not shown.
Binary file removed public/img/models/49.jpg
Diff not rendered.
Binary file added public/img/models/5.avif
Binary file not shown.
Binary file removed public/img/models/5.jpg
Diff not rendered.
Binary file added public/img/models/50.avif
Binary file not shown.
Binary file removed public/img/models/50.jpg
Diff not rendered.
Binary file added public/img/models/51.avif
Binary file not shown.
Binary file removed public/img/models/51.jpg
Diff not rendered.
Binary file added public/img/models/52.avif
Binary file not shown.
Binary file removed public/img/models/52.jpg
Diff not rendered.
Binary file added public/img/models/6.avif
Binary file not shown.
Binary file removed public/img/models/6.jpg
Diff not rendered.
Binary file added public/img/models/7.avif
Binary file not shown.
Binary file removed public/img/models/7.jpg
Diff not rendered.
Binary file added public/img/models/8.avif
Binary file not shown.
Binary file removed public/img/models/8.jpg
Diff not rendered.
Binary file added public/img/models/9.avif
Binary file not shown.
Binary file removed public/img/models/9.jpg
Diff not rendered.
Binary file added public/img/placeholder.avif
Binary file not shown.
Binary file removed public/img/placeholder.png
Diff not rendered.
6 changes: 5 additions & 1 deletion src/app/3d-models/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { toggleLike } from "@/features/models/likes/actions/toggle-like";
import { HeartButtonServer } from "@/features/models/likes/components/heart-button-server";
import { getAllModelSlugs } from "@/features/models/queries/get-all-model-slugs";
import { getModelBySlug } from "@/features/models/queries/get-model-by-slug";
import { PLACEHOLDER_IMAGE_SRC } from "@/lib/placeholder-image";

// fallow-ignore-next-line
export const instant = false;

export const generateStaticParams = async () => await getAllModelSlugs();

Expand All @@ -32,7 +36,7 @@ export const generateMetadata = async ({
{
alt: `3D model of ${title}`,
height: 630,
url: image || "/img/placeholder.png",
url: image || PLACEHOLDER_IMAGE_SRC,
width: 1200,
},
],
Expand Down
16 changes: 10 additions & 6 deletions src/app/styles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { css } from "@styled-system/css";
import { grid } from "@styled-system/patterns";
import { css, cx } from "@styled-system/css";
import { cq, grid } from "@styled-system/patterns";

export const skeletonEnter = css({
_starting: {
Expand All @@ -11,7 +11,11 @@ export const skeletonEnter = css({
transitionProperty: "filter",
});

export const modelsGrid = grid({
gap: 6,
gridTemplateColumns: "repeat(auto-fill,minmax(min(100%,250px),1fr))",
});
export const modelsGrid = cx(
cq({ name: "models-grid" }),
grid({
columnGap: 6,
gridTemplateColumns: "repeat(auto-fill,minmax(min(100%,20rem),1fr))",
rowGap: 0,
}),
);
Binary file removed src/assets/images/hero-image.png
Diff not rendered.
Binary file removed src/assets/images/placeholder.png
Diff not rendered.
4 changes: 2 additions & 2 deletions src/components/offline-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const offlineIconContainerClassName = cx(
const offlineIconClassName = cx(square({ size: 4 }), css({ color: "brand" }));

const offlineLabelClassName = css({
"@container navbar (min-width: 20ch)": {
"@navbar/offline-indicator-small": {
clip: "auto",
height: "auto",
margin: 0,
Expand All @@ -54,7 +54,7 @@ const offlineLabelClassName = css({
});

const offlineSupportingTextClassName = css({
"@container navbar (min-width: 50ch)": {
"@navbar/offline-indicator-full": {
display: "inline",
},
color: "text.muted",
Expand Down
Loading
Loading