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:
bilalgursen
2026-07-20 07:46:41 +03:00
parent 4c76bd9724
commit 87f265ec15
84 changed files with 16020 additions and 89 deletions

106
src/lib/db.ts Normal file
View File

@@ -0,0 +1,106 @@
import path from "node:path";
import Database from "better-sqlite3";
export type Program = {
id: string;
isim: string;
universite: string;
unitur: string | null;
il: string | null;
fakulte: string | null;
tur: string;
sure: number | null;
sira2025: number | null;
sira2024: number | null;
sira2023: number | null;
sira2022: number | null;
sira2021: number | null;
puan2025: number | null;
kontenjan2025: number | null;
yerlesen2025: number | null;
};
export const PUAN_TURLERI = {
say: "SAYISAL",
ea: "EŞİT AĞIRLIK",
soz: "SÖZEL",
dil: "DİL",
tyt: "TYT",
} as const;
export type PuanTuruKey = keyof typeof PUAN_TURLERI;
export type RankResults = {
hayal: Program[];
dengeli: Program[];
garanti: Program[];
};
// Dev'de hot-reload başına yeni bağlantıılmasın diye global cache
const globalForDb = globalThis as unknown as { yokatlasDb?: Database.Database };
function getDb(): Database.Database {
if (!globalForDb.yokatlasDb) {
globalForDb.yokatlasDb = new Database(
path.join(process.cwd(), "data", "yokatlas.db"),
{ readonly: true, fileMustExist: true }
);
}
return globalForDb.yokatlasDb;
}
const SELECT_COLS = `id, isim, universite, unitur, il, fakulte, tur, sure,
sira2025, sira2024, sira2023, sira2022, sira2021,
puan2025, kontenjan2025, yerlesen2025`;
// 2025 sıralaması yoksa (az yerleşen/yeni program) 2024'e düşer
const EFEKTIF_SIRA = "COALESCE(sira2025, sira2024)";
/**
* Kullanıcının başarı sıralamasına göre programları üç dilime ayırır.
* Sıralamada küçük sayı daha iyidir; program taban sıralaması kullanıcının
* sıralamasından küçükse geçen yıl oraya yerleşmek daha iyi dereceyle mümkündü
* demektir (hayal), büyükse daha güvenli demektir (garanti).
*/
export function searchByRank(
sira: number,
turKey: PuanTuruKey,
limitPerBucket = 12
): RankResults {
const db = getDb();
const tur = PUAN_TURLERI[turKey];
const onlisans = turKey === "tyt" ? 1 : 0;
const base = `FROM programs
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
AND ${EFEKTIF_SIRA} BETWEEN ? AND ?`;
// hayal: kullanıcıdan daha iyi taban sıralaması (sınıra en yakın olanlar önce)
const hayal = db
.prepare(
`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} DESC LIMIT ?`
)
.all(tur, onlisans, Math.round(sira * 0.5), sira - 1, limitPerBucket);
// dengeli: tabanı kullanıcının sıralaması civarında
const dengeli = db
.prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`)
.all(tur, onlisans, sira, Math.round(sira * 1.4), limitPerBucket);
// garanti: tabanı belirgin şekilde altında
const garanti = db
.prepare(`SELECT ${SELECT_COLS} ${base} ORDER BY ${EFEKTIF_SIRA} ASC LIMIT ?`)
.all(
tur,
onlisans,
Math.round(sira * 1.4) + 1,
Math.round(sira * 3),
limitPerBucket
);
return {
hayal: hayal as Program[],
dengeli: dengeli as Program[],
garanti: garanti as Program[],
};
}