From 820eea7eca4f4fbd0dd43c43e9deb9d50dec5c09 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sat, 8 Aug 2026 01:06:06 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20ortak=20tipler=20@/types/yokatlas?= =?UTF-8?q?=20+=20features/rapor/types'a=20=C3=A7=C4=B1kar=C4=B1ld=C4=B1,?= =?UTF-8?q?=20server-only=20korumalar=C4=B1=20eklendi=20(Faz=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/ SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu fonksiyonları barındırıyor - RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/ rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi - 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client, ai/cagri, credits) bilinçli eklenmedi - Davranış değişikliği yok; pnpm build yeşil Co-Authored-By: Claude Fable 5 --- .../skills/nextjs-app-architecture/SKILL.md | 101 ++++++++ .../nextjs-app-architecture/metadata.json | 20 ++ .../references/cache-components.md | 80 +++++++ .../references/components.md | 214 +++++++++++++++++ .../references/example.md | 34 +++ .../references/feature-folders.md | 147 ++++++++++++ .../references/pages-suspense.md | 223 ++++++++++++++++++ .../references/queries-actions.md | 118 +++++++++ .../references/single-page-applications.md | 48 ++++ .../references/ux-patterns.md | 68 ++++++ data/app.db | Bin 315392 -> 315392 bytes package.json | 1 + pnpm-lock.yaml | 8 + scripts/tadimlik-uret.ts | 8 +- src/app/api/facetler/route.ts | 5 +- src/app/api/programlar/route.ts | 4 +- src/app/api/soru/route.ts | 7 +- src/app/api/tadimlik/route.ts | 2 +- src/app/listem/listem-govde.tsx | 2 +- src/app/listem/page.tsx | 7 +- src/app/meraklisina/demo.tsx | 2 +- src/app/meraklisina/page.tsx | 3 +- src/app/rapor/yazdir/page.tsx | 6 +- src/app/sonuc/actions.ts | 8 +- src/app/sonuc/page.tsx | 8 +- src/app/sonuc/sihirbaz-cagri-karti.tsx | 2 +- src/app/sonuc/sihirbaz-modal.tsx | 2 +- .../universite/[slug]/universite-icerik.tsx | 2 +- src/components/bolum-program-listesi.tsx | 2 +- src/components/cta-sira-form.tsx | 2 +- src/components/hero-form.tsx | 2 +- .../manuel-liste/profil-kapisi-store.ts | 2 +- .../manuel-liste/program-ekle-butonu.tsx | 2 +- src/components/manuel-liste/store.ts | 2 +- .../manuel-liste/tercih-profili-kapisi.tsx | 2 +- src/components/program-liste-verileri.tsx | 2 +- src/components/program-netleri.tsx | 2 +- src/components/program-tablosu.tsx | 2 +- src/components/rapor-listesi.tsx | 2 +- src/components/sihirbaz-adimlar.tsx | 2 +- src/components/universite-program-tablosu.tsx | 2 +- src/features/rapor/types/rapor.ts | 69 ++++++ src/lib/ai/rapor.ts | 56 +---- src/lib/analitik-sunucu.ts | 3 +- src/lib/auth.ts | 3 +- src/lib/db.ts | 87 +------ src/lib/eposta-gonder.ts | 3 +- src/lib/eposta.ts | 3 +- src/lib/harita-pinler.ts | 2 +- src/lib/iyzico.ts | 3 +- src/lib/katalog.ts | 9 +- src/lib/kredi-hatirlatma.ts | 3 +- src/lib/odeme.ts | 3 +- src/lib/rapor-havuzu.ts | 23 +- src/lib/rapor-kaydi.ts | 5 +- src/lib/rapor-maske.ts | 12 +- src/lib/rehber.ts | 3 +- src/lib/session.ts | 3 +- src/lib/sihirbaz.ts | 2 +- src/lib/sosyal-kanit.ts | 3 +- src/lib/tadimlik-havuzu.ts | 4 +- src/types/yokatlas.ts | 78 ++++++ 62 files changed, 1318 insertions(+), 215 deletions(-) create mode 100644 .claude/skills/nextjs-app-architecture/SKILL.md create mode 100644 .claude/skills/nextjs-app-architecture/metadata.json create mode 100644 .claude/skills/nextjs-app-architecture/references/cache-components.md create mode 100644 .claude/skills/nextjs-app-architecture/references/components.md create mode 100644 .claude/skills/nextjs-app-architecture/references/example.md create mode 100644 .claude/skills/nextjs-app-architecture/references/feature-folders.md create mode 100644 .claude/skills/nextjs-app-architecture/references/pages-suspense.md create mode 100644 .claude/skills/nextjs-app-architecture/references/queries-actions.md create mode 100644 .claude/skills/nextjs-app-architecture/references/single-page-applications.md create mode 100644 .claude/skills/nextjs-app-architecture/references/ux-patterns.md create mode 100644 src/features/rapor/types/rapor.ts create mode 100644 src/types/yokatlas.ts diff --git a/.claude/skills/nextjs-app-architecture/SKILL.md b/.claude/skills/nextjs-app-architecture/SKILL.md new file mode 100644 index 0000000..f14d8fa --- /dev/null +++ b/.claude/skills/nextjs-app-architecture/SKILL.md @@ -0,0 +1,101 @@ +--- +name: nextjs-app-architecture +description: Build or audit Next.js 16 App Router apps using a next-beats-style React Server Components architecture. Use when scaffolding a new app, adding a feature, reviewing an existing app, refactoring route-loader-shaped pages into feature-owned async server components, deciding where queries/actions/components live, keeping pages synchronous with `params.then()`, placing Suspense boundaries, choosing the client/server boundary, designing skeletons, preventing CLS, or enabling Cache Components. Also use when the user asks about RSC composition, components receiving IDs instead of route params, `'use cache'`, `cacheTag`, `updateTag`, static-shell prerendering, or making an app easier for AI agents to modify. +license: MIT +metadata: + author: aurorascharff + version: "1.3.7" +--- + +# Next.js App Architecture + +A workflow for building and auditing Next.js 16+ App Router apps so they follow one consistent, feature-sliced RSC architecture like `next-beats`. + +**Follow the workflow below step by step** — it produces the invariants by construction. Load the reference a step names for the decision it depends on. Get framework _mechanics_ (API signatures, config options, hook contracts) from the linked docs — don't restate or improvise them. + +## Prerequisite + +Before changing a Next.js app, make sure the project is set up for AI agents to read version-matched docs. Follow the [AI Coding Agents guide](https://preview.nextjs.org/docs/app/guides/ai-agents): prefer the project's `AGENTS.md` / bundled docs, and create or refresh them when missing. Then use this skill for architecture decisions. + +## Architecture target + +Build pages that describe the loading experience, not pages that act like route loaders: + +- `app/**/page.tsx` and `layout.tsx` are synchronous composition surfaces: static chrome, section headings, `` boundaries, error boundaries, and transition wrappers. +- Feature components own their reads on the server. They receive minimal stable inputs (`id`, `slug`, `handle`, parsed filter values) or already-fetched records, never raw `params` / `searchParams`. +- Queries and actions live in the feature folder. Components import queries; client leaves import actions directly. +- When server tags and client query keys describe the same feature data, a pure feature-local cache contract owns those identities. +- Skeletons mirror the component tree and live beside the component they represent. + +## Invariants (what every change must satisfy) + +The non-negotiables. The workflow produces them; the final check verifies them. + +1. **Pages compose, they never fetch.** A page/layout imports feature components and places ``. No queries, no domain logic, no route-specific components defined inline. +2. **Pages stay synchronous.** Use `params.then()` / `searchParams.then()`, never `await params` at the top — so chrome paints into the static shell and only data-dependent sections suspend. +3. **Feature components receive IDs, not route props.** Resolve `params` / `searchParams` at the page boundary and pass plain values (`id`, `slug`, `query`) into features. +4. **Async server component is the default.** `'use client'` only for hooks, event handlers, or browser APIs — and only on leaves, never on parents of server content. +5. **The page owns the Suspense boundary; the feature owns the skeleton.** Features never pre-wrap themselves in ``. +6. **Skeletons live in the same file as the component**, exported alongside it, defined at the end. `Feed` and `FeedSkeleton` are siblings. +7. **Queries live in `-queries.ts`** (`import 'server-only'`); **actions live in `-actions.ts`** (`'use server'`). The file name matches the folder, even for sub-concepts. +8. **One feature folder per real domain noun.** Sub-concepts (favorite, like, vote, bookmark, search) fold into the parent feature, never their own folder. +9. **Client components import actions directly** — never receive a server action as a prop just to call it. +10. **Feature-local cache coordination stays with its domain.** Put pure tags/keys in `-cache.ts`, client query definitions in `-query-options.ts`, hook wrappers in `hooks/use-*.ts`, and tiny client leaves in `components/`; promote support code only after real cross-feature reuse. + +## Workflow + +Run these in order for build-from-scratch, feature work, or audits. Each step names the reference to consult and the check it must pass. + +1. **Choose mode.** + - **Build from scratch:** sketch routes, real domain nouns, static shell, and expected loading groups before writing code. + - **Audit/refactor:** scan current `app/` pages first; list every async page, page-level query import, route prop leak, missing Suspense boundary, and feature folder mismatch. + → `references/example.md` for the target shape; `references/feature-folders.md` for placement. + ✓ You know whether you are creating the architecture or converting loader-shaped code into it. +2. **Place the work.** Decide the feature folder before writing anything. + → `references/feature-folders.md` (decision tree + merge rules). + ✓ A real domain, or folded into the right parent. +3. **Write the query and, when a client cache shares its data, the cache contract.** Put server reads in `-queries.ts` with `import 'server-only'`; keep shared tag/key identities in a pure `-cache.ts`. + → `references/queries-actions.md`; for SWR/TanStack Query → `references/single-page-applications.md`; with `cacheComponents: true`, also → `references/cache-components.md`. + ✓ Cache identities are defined once; server reads are server-only, cached/tagged/lifetimed under Cache Components, and return domain types rather than ORM rows. +4. **Write the action** (if there's a mutation). `features//-actions.ts`, `'use server'` at the top. + → `references/queries-actions.md`. + ✓ Re-checks auth, validates input, invalidates matching cache tags under Cache Components (`refresh()` only for justified dynamic reads), returns a discriminated union. +5. **Build the component + skeleton.** `features//components/.tsx`: an async server component that awaits its own query from minimal props; `'use client'` only on interactive leaves. + → `references/components.md`; for a client data library or strict-SPA/CSR feature → `references/single-page-applications.md`. + ✓ Component receives IDs/handles/parsed filters or already-resolved records, not `params`; skeleton is a sibling export at the end; no alias skeleton wrappers. +6. **Compose the page.** `app//page.tsx`: synchronous, `params.then()`, place `}>`, and wrap fallible sections in an error boundary. + → `references/pages-suspense.md`. + ✓ The page only composes; the boundary lives here, not in the feature; route props are resolved to plain values before reaching feature components. +7. **Add interaction** (if any): optimistic updates, pending state, toasts, confirmation. + → `references/ux-patterns.md`. + ✓ Feedback isn't doubled; destructive actions confirm; feature-owned client coordination stays with that feature instead of leaking into unrelated domains. +8. **Verify** against the checklist below before declaring done. + +## Verify before done + +Inspect the diff against every invariant — each is checkable by reading the changed files: + +- [ ] No page/layout imports a `*-queries` file or defines a route-specific component inline. +- [ ] Every page with params is synchronous and uses `params.then()` / `searchParams.then()`. +- [ ] Feature components receive plain IDs/handles/parsed filters or resolved records; no feature prop is named `params` or `searchParams`. +- [ ] Every `` for page data sits in the page; no feature pre-wraps itself. +- [ ] Every component has its real `*Skeleton` in the same file, at the end; no tiny skeleton aliases just to pass props. +- [ ] Every `*-queries.ts` starts with `import 'server-only'`; every `*-actions.ts` with `'use server'`. +- [ ] With `cacheComponents: true`, reusable reads use `'use cache'` / `cacheTag` / `cacheLife`, or `'use cache: private'` / `'use cache: remote'` when appropriate; any dynamic read is intentional and justified. +- [ ] Mutations touching cached reads call `updateTag()` / `revalidateTag(..., 'max')` for the matching tags; `refresh()` is not a substitute for tag invalidation. +- [ ] Action files are named `-actions.ts`; no sub-concept spawned its own folder. +- [ ] Features with both server tags and client query keys define them once in a pure `-cache.ts`; queries, actions, hydration, query options, and hooks import from it. +- [ ] Feature-local client-support files sit in the smallest fitting place: query options at the feature root, `use-*` hook wrappers in `hooks/`, leaf components in `components/`, and shared support only after real cross-feature reuse. +- [ ] `'use client'` components are leaves — they import actions/hooks/providers, not async server components. +- [ ] Mutations validate their input and invalidate the affected data. + +## Reference index + +- **`references/feature-folders.md`** — where code goes: folder layout, cache contracts, naming, and merging sub-concepts. +- **`references/queries-actions.md`** — query/action rules: server-only, dedup, validation, invalidation, return shape. +- **`references/components.md`** — server/client boundary, skeletons, `use()`, single-use helpers, live data. +- **`references/pages-suspense.md`** — page composition, `params.then()`, Suspense placement, CLS, error boundaries, prefetch. +- **`references/cache-components.md`** — the `cacheComponents` decisions: which reads to cache, which directive to use, how to invalidate. +- **`references/single-page-applications.md`** — client cache decisions: placement, server seeding, Cache Components coordination, hydration, and mutations. +- **`references/ux-patterns.md`** — interaction decisions: optimistic vs pending vs inline error, toasts, action-prop, confirmations. +- **`references/example.md`** — the next-beats reference app: invariant → file map, for seeing any rule in real code. diff --git a/.claude/skills/nextjs-app-architecture/metadata.json b/.claude/skills/nextjs-app-architecture/metadata.json new file mode 100644 index 0000000..64666f5 --- /dev/null +++ b/.claude/skills/nextjs-app-architecture/metadata.json @@ -0,0 +1,20 @@ +{ + "version": "1.3.7", + "organization": "Aurora Scharff", + "date": "July 2026", + "abstract": "Build and audit Next.js 16 App Router apps with a next-beats-style RSC architecture: synchronous pages that compose static shells, Suspense, and error boundaries; feature-owned async server components that receive IDs or parsed values; co-located skeletons; server-only queries; server actions; enforced Cache Components practice when enabled; and build/runtime verification of the resulting loading shape. References cover each topic in depth so the agent only loads what's needed for the task.", + "references": [ + "https://preview.nextjs.org/docs/app", + "https://preview.nextjs.org/docs/app/guides/ai-agents", + "https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents", + "https://preview.nextjs.org/docs/app/api-reference/directives/use-cache", + "https://preview.nextjs.org/docs/app/api-reference/functions/cacheTag", + "https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheLife", + "https://preview.nextjs.org/docs/app/api-reference/functions/updateTag", + "https://preview.nextjs.org/docs/app/guides/adopting-partial-prefetching", + "https://preview.nextjs.org/docs/app/guides/migrating-to-cache-components", + "https://preview.nextjs.org/docs/app/guides/interactive-apps", + "https://aurorascharff.no/posts/component-architecture-for-react-server-components/", + "https://github.com/vercel-labs/next-beats" + ] +} diff --git a/.claude/skills/nextjs-app-architecture/references/cache-components.md b/.claude/skills/nextjs-app-architecture/references/cache-components.md new file mode 100644 index 0000000..d6f5eda --- /dev/null +++ b/.claude/skills/nextjs-app-architecture/references/cache-components.md @@ -0,0 +1,80 @@ +# Cache Components + +Decisions for when [`cacheComponents: true`](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) is set in `next.config.ts`. This file is about *which reads to cache, which directive to use, and how to invalidate* — for the mechanics of each directive, follow the doc links. + +## When this reference applies + +Use this reference when an app already has `cacheComponents: true`, the user wants this architecture while enabling it, or you are reviewing/refactoring an app that targets Cache Components. + +If the project has not adopted Cache Components yet and the user asks to enable, migrate, or work through adoption blockers, use the `next-cache-components-adoption` skill first. It owns the route-by-route migration loop, opt-out strategy, and build/dev overlay workflow. Then return here for steady-state query/action/component architecture. + +If `cacheComponents` is not enabled and the task is ordinary feature work, follow the core references without adding cache directives. Do not recommend skipping Cache Components based on app category alone; adoption is a migration/project decision, not a per-feature shortcut. + +Adopting these in an existing app: follow [Migrating to Cache Components](https://preview.nextjs.org/docs/app/guides/migrating-to-cache-components) and [Adopting Partial Prefetching](https://preview.nextjs.org/docs/app/guides/adopting-partial-prefetching) — they cover the incremental path (per-route `prefetch = 'partial'`, fixing dynamic-usage build errors) rather than a big-bang switch. + +## The model + +```ts +// next.config.ts +const nextConfig: NextConfig = { + cacheComponents: true, + partialPrefetching: true, // prefetch the static shell of linked routes +}; +``` + +- **Static shell** — synchronous content, `'use cache'` output, and Suspense fallbacks prerender at build time. +- **Dynamic holes** — async work without `'use cache'` streams in behind `` at request time. +- **Build constraint** — any async work without `'use cache'` must sit inside ``, or the build fails (wrap it, or add `'use cache'`). + +`cacheComponents` implies Partial Prerendering — it replaced `experimental.ppr` / `dynamicIO` / `useCache`, so don't set those. See [caching](https://preview.nextjs.org/docs/app/getting-started/caching). + +With `cacheComponents: true`, the skill practice is **cache reusable reads**. Do not leave a database/API read dynamic just because Suspense makes the build pass. If a read has a stable key and a mutation can name what changed, give it a cache directive, tags, and a lifetime. + +Dynamic reads are the exception: use them for values that must be recomputed for every request or cannot be invalidated coherently. When you leave a read dynamic, note the reason in the surrounding code/review and invalidate its mutations with `refresh()` because there is no tag to update. + +## Decide what to cache + +| Data | Directive | Notes | +| ---- | --------- | ----- | +| Cacheable across users (public listings, computed pages) | [`'use cache'`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache) | Add [`cacheTag`](https://preview.nextjs.org/docs/app/api-reference/functions/cacheTag) (a global + a scoped tag) and a [`cacheLife`](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheLife) profile. | +| Per-user / reads cookies, headers, session | [`'use cache: private'`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache-private) | Cached in the browser only, doesn't persist across reloads; never stored on the server. | +| Remote service, safe across users, worth durable storage | [`'use cache: remote'`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache-remote) | Protects against rate-limited third-party APIs. | +| Genuinely dynamic per request | none | Must be justified. Read inside ``; mutations use `refresh()` because no tag exists. | + +Cache the **query** when its result should be reused across requests. Cache the **component** when rendering is expensive and props are stable (a nav, a trending sidebar). Don't `'use cache'` a component that already calls a `'use cache'` query — double-caching, no benefit. + +## Keep a synchronous value out of the shell + +You usually don't need this. A query that reads `cookies()`/`headers()` or awaits a DB/`fetch` inside `` already stays out of the shell on its own. Only a *synchronous* request-time read (`new Date()`, `Math.random()`, a sync sqlite read) needs help: `await` [`io()`](https://preview.nextjs.org/docs/app/api-reference/functions/io) before it, with the caller inside ``. + +Prefer `io()` over [`connection()`](https://preview.nextjs.org/docs/app/api-reference/functions/connection): both exclude what follows from the shell, but `connection()` blocks prefetches while `io()` stays prefetchable. Reach for `connection()` only when rendering must wait for a real user request. + +## Decide how to invalidate + +- [`updateTag(tag)`](https://preview.nextjs.org/docs/app/api-reference/functions/updateTag) — in **server actions**, when the user should see the result immediately (read-your-own-writes). Requires the query to carry a matching `cacheTag`. +- [`revalidateTag(tag, 'max')`](https://preview.nextjs.org/docs/app/api-reference/functions/revalidateTag) — in **route handlers** (webhooks, cron) for stale-while-revalidate. The single-arg `revalidateTag(tag)` form is deprecated. +- [`refresh()`](https://preview.nextjs.org/docs/app/api-reference/functions/refresh) — re-render the current route for the current user. Use it for deliberately dynamic reads with no tag; don't use it instead of `updateTag()` for cached reads. + +Tag, cache, invalidate: the `cacheTag` in the query and the `updateTag` in the action use the same string and live in the same feature folder. + +## Coordinate hydrated client data + +When cached server data seeds SWR, TanStack Query, or another browser cache, follow `references/single-page-applications.md`. Server and client freshness policies are independent; hydration adds library-specific constraints. + +## Build failure map + +When `next build` fails under Cache Components, map the error back to an architecture rule instead of patching locally: + +- Async work without `'use cache'` and without an ancestor `` → cache the reusable read, or wrap a justified dynamic read in a page-owned ``. +- Request data inside `'use cache'` → switch to `'use cache: private'` when it is per-user cacheable, or keep it dynamic with a documented reason. +- `await params` / `await searchParams` at the top of a page → keep the page synchronous and move the read into `params.then()` / `searchParams.then()`. +- Sync request-time values (`new Date()`, `Math.random()`, sync storage reads) captured in the shell → cache stable values, or use [`io()`](https://preview.nextjs.org/docs/app/api-reference/functions/io) for per-request values. + +For adoption-wide blocker triage, use `next-cache-components-adoption`. For API-specific recipes, follow the [Migrating to Cache Components guide](https://preview.nextjs.org/docs/app/guides/migrating-to-cache-components). + +## Without Cache Components + +- Don't use `'use cache'` / `cacheTag` / `cacheLife` — they require the flag. +- Use React `cache()` only for proven same-request dedup needs; plain `server-only` async queries are the default. +- Invalidate with `refresh()` from server actions. +- Pages still use `params.then()` in this architecture. Without Cache Components there is no build-time static shell to preserve, but keeping pages synchronous still lets chrome paint before route-specific data resolves and keeps the app consistent. diff --git a/.claude/skills/nextjs-app-architecture/references/components.md b/.claude/skills/nextjs-app-architecture/references/components.md new file mode 100644 index 0000000..d9fa5aa --- /dev/null +++ b/.claude/skills/nextjs-app-architecture/references/components.md @@ -0,0 +1,214 @@ +# Components + +How to build server and client components inside a feature folder. + +## Default: async server component + +Server components await their own queries directly — no `useEffect`, no client-side fetching, no manual loading state. See the [Server Components docs](https://preview.nextjs.org/docs/app/getting-started/server-and-client-components) for the model. + +Prefer minimal, stable props: IDs, slugs, handles, parsed filters, or records the parent already fetched. Do not pass raw route `params` or `searchParams` into feature components. Pages resolve those promises and pass plain values. + +```tsx +// features/notifications/components/notifications-badge.tsx +import { getUnreadNotificationCount } from "@/features/notifications/notifications-queries"; + +export async function NotificationsBadge() { + const count = await getUnreadNotificationCount(); + if (count === 0) return null; + return {count}; +} +``` + +The page (not this file) wraps it in `}>` — see `references/pages-suspense.md`. + +For parameterized routes, the page resolves `params` and the feature receives an ID: + +```tsx +// app/post/[id]/page.tsx +}> + {params.then(({ id }) => ( + + ))} + +``` + +```tsx +// features/post/components/post-detail.tsx +export async function PostDetail({ id }: { id: string }) { + const post = await getPost(id); + return
{post.body}
; +} +``` + +## Skeletons live in the same file + +Export the main component and its skeleton from the same file. Pages import both. Define the skeleton **at the end of the file**, below the real component(s) — never above. Function declarations are hoisted, so a skeleton referenced by a component earlier in the file still works when defined last. + +```tsx +export async function Feed({ userId }: { userId: string }) { + const posts = await getFeed(userId); + return ( +
    + {posts.map((p) => ( + + ))} +
+ ); +} + +export function FeedSkeleton() { + return ( +
    + {Array.from({ length: 3 }).map((_, i) => ( +
  • + +
  • + ))} +
+ ); +} +``` + +Don't export a second skeleton whose whole job is to rename or preconfigure another skeleton: + +```tsx +// Wrong — alias wrapper adds an import surface but no behavior +export function CompactGridSkeleton() { + return ; +} +``` + +Import the real skeleton and pass the prop inline at the `` boundary: `fallback={}`. + +### Skeleton design checklist + +1. Match the real component's layout: flex direction, gaps, padding, breakpoints. +2. Include all structural elements: avatar circles, action button placeholders, image squares. +3. Responsive visibility must match (`hidden sm:block` in the real component → same in the skeleton). +4. Show 2–5 placeholders for variable-length lists, not the real count. +5. Don't include skeletons for inner Suspense content — those have their own boundaries. +6. Reserve the right height. CLS comes from skeletons that are shorter than the real content. + +## Group related components in one file + +A card and its grid live in the same file. For example, `genre-card.tsx` exports `GenrePill`, `GenreCard`, `GenreGrid`, `GenreGridSkeleton`. Variants should reuse that skeleton inline instead of exporting alias skeletons. Don't split shared UI primitives prematurely — wait until three call sites need the same shape before extracting. + +Two sidebar widgets that happen to look similar but render different data shapes are **not** the same component. The visuals diverge as soon as one needs an extra slot. + +### Single-use sub-components stay inlined + +For a metadata strip inside one card, a header used only by one detail view, a list item only rendered by its list — inline them as **non-exported** functions in the same file: + +```tsx +export async function EventDetails({ slug }: { slug: string }) { + const event = await getEventBySlug(slug); + return ( +
+ + +

{event.description}

+
+ ); +} + +function MetaStrip({ event }: { event: Event }) { ... } +function Speaker({ speaker }: { speaker: string }) { ... } +``` + +Exports are for things other files will import. Internal structure is for readability inside one file. + +## The server/client boundary + +`'use client'` only when you need: + +- Hooks (`useState`, `useReducer`, `useOptimistic`, `useTransition`, `useEffect`) +- Event handlers (`onClick`, `onChange`, `onSubmit`) +- Browser APIs (`window`, `localStorage`, refs to DOM) + +If the component needs interactive pieces, keep the server component as the parent and render client leaves: + +```tsx +async function PostDetail({ id }: { id: string }) { + const [post, userState] = await Promise.all([ + getPost(id), + getPostUserState(id), + ]); + return ( +
+ + {/* 'use client' leaf */} +
+ ); +} +``` + +### Server content as children of client components + +Composition crosses the boundary. A client component can accept server-rendered JSX as children or props: + +```tsx +}> + +
+ } +/> +``` + +`ComposerForm` is `'use client'`. It doesn't know where the avatar JSX came from. The Suspense boundary streams the avatar in without the form re-rendering. + +### Pass server children resolved values, not promises + +Prefer passing plain values (strings, IDs, resolved data) to a server child. A server component _can_ `await` a promise prop, but resolve route promises in the page instead — pass an unresolved promise down only to a _client_ component that reads it with `use()` (see below). When a parent already has the data from its own query, pass it as a prop instead of having the child refetch. + +```tsx +// Right — parent fetches the list, passes each item +async function Feed({ userId }: { userId: string }) { + const posts = await getFeed(userId); + return posts.map((post) => ); +} + +async function Post({ post }: { post: Post }) { + return
{post.body}
; +} +``` + +```tsx +// Wrong — child refetches what the parent already had +async function Post({ id }: { id: string }) { + const post = await getPost(id); + return
{post.body}
; +} +``` + +## Client components that own their loading state + +When a client component needs server data but should manage its own loading (a sidebar badge, a popover that opens on hover), pass an **unresolved promise** from the server and resolve it with [`use()`](https://react.dev/reference/react/use) on the client. Wrap the consumer in ``. + +```tsx +// page: pass the unresolved promise, wrap in Suspense +}> + + +``` + +```tsx +"use client"; +import { use } from "react"; + +export function TagPicker({ itemsPromise }: { itemsPromise: Promise }) { + const items = use(itemsPromise); + // render interactive UI from items +} +``` + +The opinionated bit: name promise props with a `Promise` suffix (`itemsPromise`, `userPromise`) so the contract is obvious at the call site. + +### Client data libraries (SWR, TanStack Query) + +Follow `references/single-page-applications.md` when a feature uses a browser data cache or needs externally authored updates. It covers when to use a library, where its files live, server seeding, Cache Components coordination, hydration, and mutations. + +## Mutations + +For client-side reactions to a server mutation (instant feedback, pending state, success/error toasts), see `references/ux-patterns.md`. To cache rendered output across requests, see `references/cache-components.md`. diff --git a/.claude/skills/nextjs-app-architecture/references/example.md b/.claude/skills/nextjs-app-architecture/references/example.md new file mode 100644 index 0000000..ec4f801 --- /dev/null +++ b/.claude/skills/nextjs-app-architecture/references/example.md @@ -0,0 +1,34 @@ +# Reference app: next-beats + +A working app that follows this architecture: **** (Next.js 16, `cacheComponents` + `partialPrefetching`, a music player). Use it to see any invariant in real code rather than restating the code here. Paths are as of this writing — verify against the current repo. + +For the reasoning behind this architecture, read [Component Architecture for React Server Components](https://aurorascharff.no/posts/component-architecture-for-react-server-components/). The skill's target is the same shape: pages describe layout and loading; feature components own server reads; route params become IDs before they reach components. + +## Invariant → where to see it + +| Invariant | File(s) | +| --------- | ------- | +| 1. Pages compose, never fetch | `app/(app)/search/page.tsx`, `app/(app)/genre/[genre]/page.tsx` — import feature components, place ``, no queries. | +| 2. Pages stay synchronous (`params.then` / `searchParams.then`) | `app/(app)/track/[id]/page.tsx`, `app/(app)/genre/[genre]/page.tsx`, `app/(app)/search/page.tsx`. | +| 3. Feature components receive IDs, not route props | Track/genre pages resolve `params` / `searchParams` and pass `id`, `genre`, or parsed values into feature components. | +| 4. Async server component default; `'use client'` on leaves | Server: `features/track/components/discover.tsx`, `most-played.tsx`. Client leaves: `features/track/components/track-interactions.tsx`, `play-button.tsx`. | +| 5. Page owns Suspense; feature owns skeleton | Pages place the boundary (e.g. `app/(app)/genre/[genre]/page.tsx`); features export the skeleton (below). | +| 6. Skeleton in the same file, at the end | `features/track/components/track-row.tsx` (`TrackRow` … `TrackListSkeleton`), `features/genre/components/genre-card.tsx`. | +| 7. `-queries.ts` (`server-only`) / `-actions.ts` (`'use server'`) | `features/track/track-queries.ts`, `features/playlist/playlist-actions.ts`. | +| 8. One folder per domain; sub-concepts folded in | `toggleFavorite` in `features/track/track-actions.ts` (no `favorite` folder); `searchTracks` in `features/track/track-queries.ts` (no `search` folder). | +| 9. Client components import actions directly | `features/track/components/track-interactions.tsx` imports `toggleFavorite` directly. | + +## Supporting patterns + +| Pattern | File | +| ------- | ---- | +| Error boundary on `catchError` (`ErrorInfo` `retry`) | `components/ui/error-boundary.tsx` | +| `useOptimistic` for an unlikely-to-fail toggle | `features/track/components/track-interactions.tsx` | +| Action-prop / `*Action` convention + confirm dialog | `features/playlist/components/playlist-interactions.tsx`, `components/ui/confirm-dialog.tsx` | +| `useFormStatus` submit button | `components/ui/button.tsx` | +| `useActionState` inline field errors | `features/user/components/sign-in-form.tsx` | +| Client-owned live data via a provider (not ``) | `providers/player-provider.tsx` → `components/now-playing-bar.tsx` | +| `use()` on an unresolved promise prop | `features/playlist/components/add-to-playlist-menu.tsx` | +| Purpose-named `components/scripts/` subfolder | `components/scripts/` | + +> The repo is a live app, not a golden reference — spots may drift from the invariants (a page may fetch inline, a route `error.tsx` may lag an API rename). When the app and the invariants disagree, the invariants win; treat the mismatch as a fix for the app. diff --git a/.claude/skills/nextjs-app-architecture/references/feature-folders.md b/.claude/skills/nextjs-app-architecture/references/feature-folders.md new file mode 100644 index 0000000..907a874 --- /dev/null +++ b/.claude/skills/nextjs-app-architecture/references/feature-folders.md @@ -0,0 +1,147 @@ +# Feature folders + +How to organize code under `features/` and `app/`. + +## Folder layout + +``` +features// + -cache.ts # Pure server tags + client query keys, when shared + -queries.ts # Server-only queries + -actions.ts # Server actions + -query-options.ts # Client data-library query definitions, when needed + components/ # Server + client components, each with its skeleton + types/ # Feature-local public types, when needed by multiple files + hooks/ # Actual feature-local React hooks and hook wrappers + providers/ # Feature-local providers, only when the provider belongs to this domain +``` + +The folder name **is** the domain. The query and action filenames match the folder. + +## How many features? + +Keep the feature list short. One folder per **domain noun a user would recognize**, not per database table or technical concern. + +A new folder is justified when **all three** are true: + +1. The concept has its own queries. +2. The concept has its own pages or routes. +3. The concept is referenced from at least two other features. + +If you find yourself making a feature folder with one query, one action, and one button, fold it into the parent feature instead. + +### Merge aggressively + +Concepts that exist only in service of a parent entity belong inside the parent's feature folder: + +- A `favorite` or `bookmark` concept that only attaches to one parent entity (events, posts) → inside that parent's folder. +- A `like`, `repost`, `vote`, or `reaction` concept on a piece of content → with that content's feature. +- `auth` / `session` / `current user` → a single `user` folder, not split. +- A cross-cutting concern like `search` folds into the primary content feature it queries (`searchTracks` in `features/track/`), not a `features/search/` folder — the page composes it. + +Concrete example: `toggleFavorite` is a mutation about events ("I favorite an event"), not its own domain. It lives in `features/event/event-actions.ts`, not `features/favorite/favorite-actions.ts`. + +## File naming + +Filenames inside the folder always start with the folder name: + +``` +features/event/ + event-queries.ts + event-actions.ts + components/ + event-grid.tsx + event-details.tsx + favorite-button.tsx ← OK: a component, not a "favorite" feature +``` + +- `-queries.ts` — even if the file has only one query. +- `-actions.ts` — even if a mutation is about a sub-concept. +- Other `-*.ts` files are fine when the folder needs them (`playlist-constants.ts`, `-schema.ts`), as long as they keep the folder-name prefix. Don't put reusable domain types in a root `*-types.ts` file; use `features//types/` once a type is imported by multiple files. +- Component files use any descriptive name. The component (not the feature) is the unit here. + +## Local vs shared support folders + +Use a local support folder when the code belongs to one feature: + +``` +features/message/ + message-cache.ts + message-query-options.ts + types/ + message.ts + hooks/ + use-message-mutations.ts + use-message-draft.ts + providers/ + message-draft-provider.tsx +``` + +Feature-owned client coordination stays with the feature. Place each file by the shape it exports and the domain it belongs to: + +- Client data-library cache contracts and query definitions follow `references/single-page-applications.md`. +- Mutation wrappers that export hooks live in `hooks/use-*.ts` (`use-message-mutations.ts` exporting `useSendMessage`). +- Browser-only state helpers live in `hooks/` when their public API is a hook (`use-thread.ts`, `use-message-draft.ts`). +- Client leaf components that coordinate a server write live in `components/` next to the UI they support (`mark-activity-read.tsx` posts read activity in the background while the current `/activity` tree stays stable). + +Keep the file prefix aligned with the feature folder when a file exports a grouped feature contract (`workspace-cache.ts` and `workspace-query-options.ts`, not `activity-cache.ts` in `features/workspace/`). Support code for a sub-concept still lives with the parent feature: reactions on messages belong in `features/message/`; unread activity chrome belongs in `features/workspace/`. + +Promote only when there are real cross-feature consumers: + +- `types/` at the project root — shared domain/application types imported across features. +- `hooks/` at the project root — shared client hooks used across features. +- `app/providers.tsx` or `components/*-provider.tsx` — app-shell providers that wrap the whole app. + +Avoid root-level miscellany like `message-types.ts`, `shared-hooks.ts`, or `common-provider.tsx`; the folder name should explain the scope. + +## What goes in `components/` + +Each component file exports the main component **plus its skeleton**: + +```tsx +// features/event/components/event-grid.tsx +export async function EventGrid(...) { ... } +export function EventGridSkeleton() { ... } +``` + +Group related components in one file when they're always used together or one is a natural building block for another. A card and its grid live together. For example, `genre-card.tsx` exports `GenrePill`, `GenreCard`, `GenreGrid`, `GenreGridSkeleton`. + +Split into separate files only when: + +- A component is consumed by multiple sibling components (one shared use is not enough — wait until three call sites need it). +- A component is `'use client'` and a sibling is a server component (the server/client boundary forbids sharing a file). + +See `references/components.md` for inlining rules and the skeleton design checklist. + +## What pages do + +Pages in `app/` compose feature components with Suspense and transition wrappers. They never: + +- Contain domain logic +- Define new components except thin transition wrappers (e.g. ``) +- Fetch data directly +- Inline route-specific components — extract them into the feature folder + +See `references/pages-suspense.md` for page composition details. + +## Top-level layout + +``` +app/ # Pages and layouts +features/ # Domain folders +components/ # UI primitives, theme, and app-shell singletons +hooks/ # Shared client hooks used across features +types/ # Shared cross-feature types only +lib/ # Utilities and cohesive non-domain subsystems +``` + +`lib/` holds flat helpers (`db.ts`, `utils.ts`) but may also group a cohesive non-domain subsystem in its own subfolder (e.g. `lib/audio/` for an audio engine). Cross-feature client hooks live in top-level `hooks/`; a hook used by a single feature co-locates in that feature's `hooks/`. Types follow the same rule: shared types at top-level `types/`, feature-only exported types in `features//types/`. + +`components/` holds: + +- **`components/ui/`** — primitives. Low-level building blocks and action-prop components. +- **`components/theme/`** — theme provider and toggle, paired. +- **Top-level files** (`site-header.tsx`, `auth-gate.tsx`, `poller.tsx`) — app-shell singletons used once each. No `common/` folder — "common" is not a category. If a component is used everywhere it's a primitive (→ `ui/`); if it's used once it lives at the top level. +- **Purpose-named subfolders** are fine when several files share a clear technical role — e.g. `components/scripts/` for pre-hydration inline `