Build TercihAI MVP: landing page, YÖK Atlas data pipeline, rank-based results
- Add product vision/market research doc (VISION.md) - Set up shadcn/ui (radix + nova preset) with blue/orange theme, Outfit + Work Sans fonts - Landing page: hero with rank input, problem/steps sections, comparison table, FAQ - Data pipeline: CSV archive ingest (2021-2024) + live YÖK Atlas API refresh (2025) into SQLite (npm run ingest / refresh); zeros normalized to NULL - /sonuc page: hayal/dengeli/garanti buckets by COALESCE(sira2025, sira2024), score-type switcher, 2024→2025 trend indicators - Add ui-ux-pro-max and shadcn agent skills, shadcn MCP config, launch.json Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
298
src/app/sonuc/page.tsx
Normal file
298
src/app/sonuc/page.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Minus,
|
||||
Rocket,
|
||||
Scale,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
PUAN_TURLERI,
|
||||
searchByRank,
|
||||
type Program,
|
||||
type PuanTuruKey,
|
||||
} from "@/lib/db";
|
||||
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 — TercihAI",
|
||||
};
|
||||
|
||||
const TUR_LABELS: Record<PuanTuruKey, string> = {
|
||||
say: "Sayısal",
|
||||
ea: "Eşit Ağırlık",
|
||||
soz: "Sözel",
|
||||
dil: "Dil",
|
||||
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 }>;
|
||||
}) {
|
||||
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 (
|
||||
<main className="mx-auto flex max-w-2xl flex-col items-center px-4 py-24 text-center">
|
||||
<h1 className="font-heading text-2xl font-bold">
|
||||
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.
|
||||
</p>
|
||||
<Button asChild className="mt-6 cursor-pointer">
|
||||
<Link href="/">
|
||||
<ArrowLeft className="size-4" aria-hidden />
|
||||
Ana sayfaya dön
|
||||
</Link>
|
||||
</Button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const results = searchByRank(sira, turKey);
|
||||
|
||||
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">
|
||||
<header className="sticky top-4 z-50 mx-4">
|
||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between rounded-2xl border border-slate-200 bg-white/80 px-5 shadow-sm backdrop-blur">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 font-heading text-lg font-bold"
|
||||
>
|
||||
<Sparkles className="size-5 text-primary" aria-hidden />
|
||||
TercihAI
|
||||
</Link>
|
||||
<Button asChild size="sm" variant="outline" className="cursor-pointer">
|
||||
<Link href="/">
|
||||
<ArrowLeft className="size-4" aria-hidden />
|
||||
Yeni arama
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-6xl px-4 py-12">
|
||||
<Badge variant="secondary" className="bg-primary/10 text-primary">
|
||||
Ücretsiz ön izleme
|
||||
</Badge>
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
|
||||
{/* Puan türü seçimi */}
|
||||
<nav className="mt-6 flex flex-wrap gap-2" aria-label="Puan türü">
|
||||
{(Object.keys(PUAN_TURLERI) as PuanTuruKey[]).map((key) => (
|
||||
<Button
|
||||
key={key}
|
||||
asChild
|
||||
size="sm"
|
||||
variant={key === turKey ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Link href={`/sonuc?sira=${sira}&tur=${key}`}>
|
||||
{TUR_LABELS[key]}
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<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 CTA */}
|
||||
<section className="mt-16 rounded-2xl bg-primary p-8 text-center text-white sm:p-12">
|
||||
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
|
||||
Bu listeyi 24 tercihlik plana çevirelim mi?
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-white/90">
|
||||
Yapay zekâ danışman; şehir, bölüm ve kariyer hedeflerine göre bu
|
||||
dilimlerden gerekçeli, dengeli bir tercih listesi kurar. Çok
|
||||
yakında.
|
||||
</p>
|
||||
<Button
|
||||
size="lg"
|
||||
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
disabled
|
||||
>
|
||||
AI danışman yakında
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
|
||||
Veriler resmî YÖK Atlas kaynağından derlenmiştir. TercihAI bir karar
|
||||
destek aracıdır; yerleşme garantisi vermez, tercih sorumluluğu adaya
|
||||
aittir.
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user