Sihirbaz akışı ana sayfaya taşındı: sıralama girilince modal orada açılır
Some checks failed
Deploy / deploy (push) Failing after 1m30s

- Ortak SihirbazAdimlar bileşeni (3 adımlık seçim UI'ı) çıkarıldı;
  hem hero modalı hem /sonuc modalı kullanıyor
- /api/facetler: sıraya göre kategori/il/üniversite tipi facet'leri
- HeroForm: "Listemi oluştur" artık modalı açar; seçimler tamamlanınca
  localStorage'a (SIHIRBAZ_STORAGE_KEY) yazılıp /sonuc'a yönlendirilir
- /sonuc: modal yalnızca giriş dönüşünde (?sihirbaz=1) otomatik açılır;
  kayıtlı seçimler prefill olarak kullanılır

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-07-26 02:30:44 +03:00
parent fc146b0e18
commit 2c02e3d7e9
5 changed files with 397 additions and 498 deletions

View File

@@ -1,22 +1,11 @@
import Link from "next/link";
import type { Metadata } from "next";
import {
ArrowLeft,
MapPin,
Minus,
Rocket,
Scale,
ShieldCheck,
TrendingDown,
TrendingUp,
X,
} from "lucide-react";
import { ArrowLeft } from "lucide-react";
import { eq } from "drizzle-orm";
import {
PUAN_TURLERI,
searchByRank,
rankWindowFacets,
type Program,
type PuanTuruKey,
} from "@/lib/db";
import { getSession, getCurrentUser } from "@/lib/session";
@@ -24,24 +13,12 @@ import { appDb, schema } from "@/lib/appdb";
import type { RaporSonuc } from "@/lib/ai/rapor";
import type { RaporParams } from "@/lib/rapor-havuzu";
import { SihirbazBolumu } from "./sihirbaz-cagri-karti";
import type {
MevcutRapor,
OnizlemeSatir,
} from "./sihirbaz-modal";
import { BolgeHaritasi, type IlOzeti } from "@/components/bolge-haritasi";
import type { MevcutRapor, OnizlemeSatir } from "./sihirbaz-modal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export const metadata: Metadata = {
title: "Sıralamana Uygun Programlar — KolayTercih",
title: "Tercih Listeni Kur — KolayTercih",
};
const TUR_LABELS: Record<PuanTuruKey, string> = {
@@ -52,106 +29,12 @@ const TUR_LABELS: Record<PuanTuruKey, string> = {
tyt: "TYT (Önlisans)",
};
function TrendIcon({ p }: { p: Program }) {
if (p.sira2025 == null || p.sira2024 == null) {
return <Minus className="size-4 text-slate-400" aria-label="Veri yok" />;
}
const degisim = (p.sira2024 - p.sira2025) / p.sira2024;
// taban sıralaması küçülüyorsa bölüm zorlaşıyor demektir
if (degisim > 0.05) {
return (
<span className="inline-flex items-center gap-1 text-orange-600">
<TrendingUp className="size-4" aria-hidden />
<span className="text-xs">zorlaşıyor</span>
</span>
);
}
if (degisim < -0.05) {
return (
<span className="inline-flex items-center gap-1 text-emerald-600">
<TrendingDown className="size-4" aria-hidden />
<span className="text-xs">rahatlıyor</span>
</span>
);
}
return (
<span className="inline-flex items-center gap-1 text-slate-500">
<Minus className="size-4" aria-hidden />
<span className="text-xs">stabil</span>
</span>
);
}
function fmt(n: number | null): string {
return n == null ? "—" : n.toLocaleString("tr-TR");
}
function ProgramTable({ programs }: { programs: Program[] }) {
if (programs.length === 0) {
return (
<p className="rounded-xl border border-dashed border-slate-300 p-6 text-center text-sm text-slate-500">
Bu dilimde uygun program bulunamadı.
</p>
);
}
return (
<div className="overflow-x-auto rounded-2xl border border-slate-200 bg-white">
<Table>
<TableHeader>
<TableRow className="bg-slate-50">
<TableHead>Program</TableHead>
<TableHead>Üniversite</TableHead>
<TableHead className="text-right">2025 taban sırası</TableHead>
<TableHead className="text-right">2024</TableHead>
<TableHead>Trend</TableHead>
<TableHead className="text-right">Kontenjan</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{programs.map((p) => (
<TableRow key={p.id}>
<TableCell className="font-medium">
{p.isim}
{p.fakulte ? (
<span className="block text-xs text-slate-500">
{p.fakulte}
</span>
) : null}
</TableCell>
<TableCell>
{p.universite}
<span className="block text-xs text-slate-500">
{p.il}
{p.unitur ? ` · ${p.unitur.toLocaleLowerCase("tr-TR")}` : ""}
</span>
</TableCell>
<TableCell className="text-right font-medium tabular-nums">
{fmt(p.sira2025)}
</TableCell>
<TableCell className="text-right tabular-nums text-slate-500">
{fmt(p.sira2024)}
</TableCell>
<TableCell>
<TrendIcon p={p} />
</TableCell>
<TableCell className="text-right tabular-nums">
{fmt(p.kontenjan2025)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
export default async function SonucPage({
searchParams,
}: {
searchParams: Promise<{
sira?: string;
tur?: string;
il?: string;
sihirbaz?: string;
}>;
}) {
@@ -161,7 +44,6 @@ export default async function SonucPage({
params.tur && params.tur in PUAN_TURLERI
? (params.tur as PuanTuruKey)
: "say";
const seciliIl = params.il?.trim() || null;
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
return (
@@ -170,7 +52,7 @@ export default async function SonucPage({
Geçerli bir sıralama gerekli
</h1>
<p className="mt-3 text-slate-600">
Sonuçları görebilmek için ana sayfadan YKS başarı sıralamanı gir.
Listeni kurabilmek için ana sayfadan YKS başarı sıralamanı gir.
</p>
<Button asChild className="mt-6 cursor-pointer">
<Link href="/">
@@ -211,65 +93,12 @@ export default async function SonucPage({
il: p.il,
}));
const results = searchByRank(sira, turKey, {
il: seciliIl ?? undefined,
});
// Harita her zaman filtresiz dağılımı gösterir ki il değiştirilebilsin
const haritaSonuclari = seciliIl ? searchByRank(sira, turKey) : results;
// Harita için il bazında özet (81 il haritada; KKTC/yurtdışı ayrıca listelenir)
const iller: Record<string, IlOzeti> = {};
const dilimler = [
["hayal", haritaSonuclari.hayal],
["dengeli", haritaSonuclari.dengeli],
["garanti", haritaSonuclari.garanti],
] as const;
for (const [dilim, programs] of dilimler) {
for (const p of programs) {
if (!p.il) continue;
iller[p.il] ??= { hayal: 0, dengeli: 0, garanti: 0 };
iller[p.il][dilim]++;
}
}
const sections = [
{
key: "hayal",
icon: Rocket,
title: "Hayal",
tone: "text-orange-600",
description:
"Geçen yıl tabanı senin sıralamanın üzerinde kapanan programlar. Şansın düşük ama listenin başında birkaç tane bulunmalı.",
programs: results.hayal,
},
{
key: "dengeli",
icon: Scale,
title: "Dengeli",
tone: "text-primary",
description:
"Tabanı sıralamanın hemen civarında. Listenin bel kemiği bu dilimden kurulur.",
programs: results.dengeli,
},
{
key: "garanti",
icon: ShieldCheck,
title: "Garanti",
tone: "text-emerald-600",
description:
"Tabanı sıralamanın belirgin altında. Boşta kalmamak için listenin sonunda mutlaka yer almalı.",
programs: results.garanti,
},
] as const;
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<main
className={`mx-auto max-w-6xl px-4 py-12 ${mevcutRapor ? "" : "pb-56"}`}
>
<main className="mx-auto max-w-4xl px-4 py-12">
<div className="flex items-center justify-between gap-3">
<Badge variant="secondary" className="bg-primary/10 text-primary">
Ücretsiz ön izleme
{sira.toLocaleString("tr-TR")}. sıra · {TUR_LABELS[turKey]}
</Badge>
<Button
asChild
@@ -284,12 +113,15 @@ export default async function SonucPage({
</Button>
</div>
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
{sira.toLocaleString("tr-TR")}. sıradaki bir aday için görünüm
{sira.toLocaleString("tr-TR")}. sıradaki bir aday için tercih planı
</h1>
<p className="mt-2 max-w-2xl text-slate-600">
2025 YÖK Atlas taban sıralamalarına göre üç dilime ayrılmış öneriler.
Taban sıralamaları her yıl değişir; bu liste garanti değil, başlangıç
noktasıdır.
Birkaç adımda ilgi alanlarını ve şehir tercihini seç; yapay zekâ
gerçek YÖK Atlas verisiyle 24 tercihlik dengeli listeni kursun. Her
satır yerleşme riskine göre renklenir:{" "}
<span className="font-semibold text-emerald-600">yeşil güvenli</span>,{" "}
<span className="font-semibold text-amber-600">sarı az riskli</span>,{" "}
<span className="font-semibold text-red-600">kırmızı riskli</span>.
</p>
{/* Puan türü seçimi */}
@@ -302,82 +134,15 @@ export default async function SonucPage({
variant={key === turKey ? "default" : "outline"}
className="cursor-pointer"
>
<Link
href={`/sonuc?sira=${sira}&tur=${key}${
seciliIl ? `&il=${encodeURIComponent(seciliIl)}` : ""
}`}
>
<Link href={`/sonuc?sira=${sira}&tur=${key}`}>
{TUR_LABELS[key]}
</Link>
</Button>
))}
</nav>
{seciliIl ? (
<div className="mt-4 flex flex-wrap items-center gap-3 rounded-xl border border-primary/30 bg-primary/5 px-4 py-3">
<MapPin className="size-4 text-primary" aria-hidden />
<p className="text-sm font-medium">
Yalnızca <span className="font-bold">{seciliIl}</span>{" "}
programları gösteriliyor
</p>
<Button
asChild
size="sm"
variant="outline"
className="cursor-pointer"
>
<Link href={`/sonuc?sira=${sira}&tur=${turKey}`}>
<X className="size-3.5" aria-hidden />
Filtreyi kaldır
</Link>
</Button>
</div>
) : null}
{/* Bölge haritası */}
<section aria-labelledby="baslik-harita" className="mt-10">
<h2 id="baslik-harita" className="font-heading text-2xl font-bold">
Nerede okuyacaksın?
</h2>
<p className="mt-1 text-sm text-slate-600">
Aşağıdaki önerilerin illere dağılımı. Koyu renk, o ilde daha fazla
öneri olduğunu gösterir. Bir ile tıklayarak listeyi o ile
daraltabilir, tekrar tıklayarak filtreyi kaldırabilirsin.
</p>
<div className="mt-4 rounded-2xl border border-slate-200 bg-white p-4 sm:p-6">
<BolgeHaritasi
iller={iller}
sira={sira}
turKey={turKey}
seciliIl={seciliIl}
/>
</div>
</section>
<div className="mt-10 space-y-12">
{sections.map((s) => (
<section key={s.key} aria-labelledby={`baslik-${s.key}`}>
<div className="mb-4 flex items-start gap-3">
<s.icon className={`mt-1 size-6 ${s.tone}`} aria-hidden />
<div>
<h2
id={`baslik-${s.key}`}
className="font-heading text-2xl font-bold"
>
{s.title}
<span className="ml-2 text-base font-normal text-slate-500">
{s.programs.length} program
</span>
</h2>
<p className="text-sm text-slate-600">{s.description}</p>
</div>
</div>
<ProgramTable programs={s.programs} />
</section>
))}
</div>
{/* Ücretli katman — sihirbaz akışı (CTA + alt kart + modal) */}
{/* Sihirbaz akışı (CTA + modal) — seçimler ana sayfadaki modalda yapılır,
burada yalnızca giriş dönüşünde (?sihirbaz=1) otomatik açılır */}
<SihirbazBolumu
sira={sira}
tur={turKey}