refactor: ortak tipler @/types/yokatlas + features/rapor/types'a çıkarıldı, server-only korumaları eklendi (Faz 1)

- 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 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:06:06 +03:00
parent 7518baccfa
commit 820eea7eca
62 changed files with 1318 additions and 215 deletions

View File

@@ -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 <span aria-label={`${count} unread`}>{count}</span>;
}
```
The page (not this file) wraps it in `<Suspense fallback={<NotificationsBadgeSkeleton />}>` — see `references/pages-suspense.md`.
For parameterized routes, the page resolves `params` and the feature receives an ID:
```tsx
// app/post/[id]/page.tsx
<Suspense fallback={<PostDetailSkeleton />}>
{params.then(({ id }) => (
<PostDetail id={id} />
))}
</Suspense>
```
```tsx
// features/post/components/post-detail.tsx
export async function PostDetail({ id }: { id: string }) {
const post = await getPost(id);
return <article>{post.body}</article>;
}
```
## 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 (
<ul>
{posts.map((p) => (
<Post key={p.id} post={p} />
))}
</ul>
);
}
export function FeedSkeleton() {
return (
<ul>
{Array.from({ length: 3 }).map((_, i) => (
<li key={i}>
<Skeleton className="h-24" />
</li>
))}
</ul>
);
}
```
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 <GridSkeleton dense />;
}
```
Import the real skeleton and pass the prop inline at the `<Suspense>` boundary: `fallback={<GridSkeleton dense />}`.
### 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 25 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 (
<article>
<MetaStrip event={event} />
<Speaker speaker={event.speaker} />
<p>{event.description}</p>
</article>
);
}
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 (
<article>
<PostBody body={post.body} />
<PostActions userState={userState} /> {/* 'use client' leaf */}
</article>
);
}
```
### 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
avatar={
<Suspense fallback={<AvatarSkeleton />}>
<CurrentUserAvatar />
</Suspense>
}
/>
```
`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) => <Post key={post.id} post={post} />);
}
async function Post({ post }: { post: Post }) {
return <article>{post.body}</article>;
}
```
```tsx
// Wrong — child refetches what the parent already had
async function Post({ id }: { id: string }) {
const post = await getPost(id);
return <article>{post.body}</article>;
}
```
## 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 `<Suspense>`.
```tsx
// page: pass the unresolved promise, wrap in Suspense
<Suspense fallback={<TagListSkeleton />}>
<TagPicker itemsPromise={getTags()} />
</Suspense>
```
```tsx
"use client";
import { use } from "react";
export function TagPicker({ itemsPromise }: { itemsPromise: Promise<Tag[]> }) {
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`.