refactor: /sonuc senkron kompozisyona geçti (Faz 9)

- liste-queries.ts (siraylaProgramAra) ve sihirbaz-queries.ts
  (getSihirbazFacetleri) eklendi — /sonuc'un SQLite okumaları feature
  sorgularına indi
- Yeni sunucu sarmalayıcıları: SonucProgramTablosu (+skeleton),
  TadimlikBolumu (girişliyse null — gruplu sınırda skeleton bilerek yok),
  SonucSihirbazi (session + rapor-varlığı + facetler içeride; client
  orkestratöre çözülmüş değerler iner)
- Sayfa senkron: searchParams.then() içinde sıra doğrulaması; geçersiz sıra
  SiraGerekli'ye düşer; geçerli akışta tablo+tadımlık+sihirbaz+dipnot+footer
  tek gruplu Suspense'te (bugünkü tek await'li akışla aynı anda belirir)
- sonuc/loading.tsx silindi; geometrisi sayfadaki fallback'te yaşıyor
- Davranış değişikliği yok (getSession React cache ile tek okuma)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:42:32 +03:00
parent fbd184e2be
commit 22e2fde058
7 changed files with 187 additions and 94 deletions

View File

@@ -1,19 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
// /sonuc senkron SQLite sorgularıyla açılır; yavaş bağlantıda beyaz ekran
// yerine sayfanın iskeletini göster.
export default function SonucLoading() {
return (
<main className="mx-auto w-full max-w-6xl px-4 py-10">
<Skeleton className="h-6 w-40 rounded-full" />
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
<Skeleton className="mt-8 h-36 w-full rounded-2xl" />
<div className="mt-8 flex flex-col gap-2">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full rounded-xl" />
))}
</div>
</main>
);
}

View File

@@ -1,15 +1,16 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { rankWindowFacets, searchByRank } from "@/lib/db";
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
import { getSession } from "@/lib/session";
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
import { tadimlikSec } from "@/lib/tadimlik-havuzu";
import { SihirbazBolumu } from "@/features/sihirbaz/components/sihirbaz-cagri-karti";
import { TadimlikSatiri } from "@/features/rapor/components/tadimlik-satiri";
import { ProgramTablosu } from "@/features/liste/components/program-tablosu";
import {
SonucProgramTablosu,
SonucProgramTablosuSkeleton,
} from "@/features/liste/components/sonuc-program-tablosu";
import { ManuelHarita } from "@/features/liste/components/manuel-harita";
import { SiteFooter } from "@/components/site-footer";
import { TadimlikBolumu } from "@/features/rapor/components/tadimlik-bolumu";
import { SonucSihirbazi } from "@/features/sihirbaz/components/sihirbaz-bolumu";
import { SiraGerekli } from "@/features/sihirbaz/components/sira-gerekli";
import { SiteFooter } from "@/components/site-footer";
import { Skeleton } from "@/components/ui/skeleton";
export const metadata: Metadata = {
title: "Tercih Listeni Kur",
@@ -19,7 +20,7 @@ export const metadata: Metadata = {
robots: { index: false, follow: true },
};
export default async function SonucPage({
export default function SonucPage({
searchParams,
}: {
searchParams: Promise<{
@@ -29,76 +30,73 @@ export default async function SonucPage({
hazir?: string;
}>;
}) {
const params = await searchParams;
const sira = Number.parseInt(params.sira ?? "", 10);
const turKey: PuanTuruKey =
params.tur && params.tur in PUAN_TURLERI
? (params.tur as PuanTuruKey)
: "say";
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
return (
<>
<SiraGerekli />
<SiteFooter />
</>
);
}
// Sihirbaz için: kullanıcının kayıtlı raporu var mı? Listenin kendisi
// /listem'de yaşar; burada yalnızca CTA'nın hangi hâlde render edileceği
// için varlık bilgisi gerekir.
const session = await getSession();
const raporSatiri = session
? await kullanicininRaporu(session.user.id)
: null;
const facetler = rankWindowFacets(sira, turKey);
// Yapay Zeka'sız ham liste: yalnızca sıralamayla ulaşılabilen programlar (dilimli)
const hamSonuclar = searchByRank(sira, turKey, { limitPerBucket: 20 });
// Girişsiz tadımlık (İP-B): SSR'da kovaya uyan "genel" satır seçilir;
// sihirbaz kategorisi client'ta okunup /api/tadimlik ile kişiselleştirilir.
// Girişli kullanıcı gerçek ürünü zaten görebildiği için tadımlık almaz.
const tadimlik = session ? null : await tadimlikSec(sira, turKey);
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<main className="mx-auto max-w-4xl px-4 py-12">
{/* Sayfa hiyerarşisi: önce Türkiye haritası, sonra liste. Manuel
liste store'una abone olduğundan tablodan "+" ile eklenenler
buraya canlı yansır. */}
<ManuelHarita />
// Tablo değişken boylu olduğundan altındaki tadımlık, sihirbaz, dipnot ve
// footer aynı sınırda gruplu — bugünkü tek await'li akışla birebir aynı
// anda beliriyorlar, ara durumda zıplama yok.
<Suspense
fallback={
<main className="mx-auto w-full max-w-6xl px-4 py-10">
<Skeleton className="h-6 w-40 rounded-full" />
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
<Skeleton className="mt-8 h-36 w-full rounded-2xl" />
<SonucProgramTablosuSkeleton />
</main>
}
>
{searchParams.then((params) => {
const sira = Number.parseInt(params.sira ?? "", 10);
const turKey: PuanTuruKey =
params.tur && params.tur in PUAN_TURLERI
? (params.tur as PuanTuruKey)
: "say";
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında, sayfanın ana
gövdesi; puan türü navbar'daki sıralama rozetinden değiştirilir */}
<ProgramTablosu sonuclar={hamSonuclar} adaySira={sira} tur={turKey} />
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
return (
<>
<SiraGerekli />
<SiteFooter />
</>
);
}
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
Havuzda içerik yoksa bileşen hiç render edilmez. */}
{!session ? (
<TadimlikSatiri sira={sira} tur={turKey} ilk={tadimlik} />
) : null}
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<main className="mx-auto max-w-4xl px-4 py-12">
{/* Sayfa hiyerarşisi: önce Türkiye haritası, sonra liste. Manuel
liste store'una abone olduğundan tablodan "+" ile eklenenler
buraya canlı yansır. */}
<ManuelHarita />
{/* Sihirbaz akışı (CTA + modal) — üretim ve liste /listem'de yaşar,
burada yalnızca funnel kartları render edilir */}
<SihirbazBolumu
sira={sira}
tur={turKey}
facetler={facetler}
girisliMi={Boolean(session)}
raporVar={Boolean(raporSatiri?.result)}
otomatikAc={params.sihirbaz === "1"}
secimlerHazir={params.hazir === "1"}
/>
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
sayfanın ana gövdesi; puan türü navbar'daki sıralama
rozetinden değiştirilir */}
<SonucProgramTablosu sira={sira} tur={turKey} />
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
Veriler resmî YÖK Atlas kaynağından derlenmiştir. KolayTercih bir karar
destek aracıdır; yerleşme garantisi vermez, tercih sorumluluğu adaya
aittir.
</p>
</main>
<SiteFooter />
</div>
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
Havuzda içerik yoksa bileşen hiç render edilmez. */}
<TadimlikBolumu sira={sira} tur={turKey} />
{/* Sihirbaz akışı (CTA + modal) — üretim ve liste /listem'de
yaşar, burada yalnızca funnel kartları render edilir */}
<SonucSihirbazi
sira={sira}
tur={turKey}
otomatikAc={params.sihirbaz === "1"}
secimlerHazir={params.hazir === "1"}
/>
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
Veriler resmî YÖK Atlas kaynağından derlenmiştir. KolayTercih
bir karar destek aracıdır; yerleşme garantisi vermez, tercih
sorumluluğu adaya aittir.
</p>
</main>
<SiteFooter />
</div>
);
})}
</Suspense>
);
}