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

190
scripts/ingest.ts Normal file
View File

@@ -0,0 +1,190 @@
/**
* YÖK Atlas verisini SQLite'a yükler.
*
* Kaynak: yokatlas-dataset-2025 (github.com/MorphaxTheDeveloper/yokatlas-dataset-2025)
* — YÖK Atlas'ın halka açık verisinin CSV dökümü. Canlı API'den tazeleme
* (yokatlas-py'nin kullandığı JSON API) v2'de eklenecek.
*
* Kullanım: npx tsx scripts/ingest.ts <tum_bolumler.csv yolu>
*/
import { createReadStream, existsSync, mkdirSync, rmSync } from "node:fs";
import { createInterface } from "node:readline";
import path from "node:path";
import Database from "better-sqlite3";
const YEARS = [2021, 2022, 2023, 2024] as const;
const csvPath = process.argv[2];
if (!csvPath || !existsSync(csvPath)) {
console.error("Kullanım: npx tsx scripts/ingest.ts <tum_bolumler.csv yolu>");
process.exit(1);
}
const dbDir = path.join(process.cwd(), "data");
mkdirSync(dbDir, { recursive: true });
const dbPath = path.join(dbDir, "yokatlas.db");
rmSync(dbPath, { force: true });
const db = new Database(dbPath);
db.pragma("journal_mode = WAL");
const yearCols = YEARS.flatMap((y) => [
`sira${y} INTEGER`,
`puan${y} REAL`,
`kontenjan${y} INTEGER`,
`yerlesen${y} INTEGER`,
]);
db.exec(`
CREATE TABLE programs (
id TEXT PRIMARY KEY,
isim TEXT NOT NULL,
universite TEXT NOT NULL,
unitur TEXT,
il TEXT,
fakulte TEXT,
tur TEXT NOT NULL,
sure INTEGER,
onlisans INTEGER NOT NULL DEFAULT 0,
${yearCols.join(",\n ")}
);
CREATE INDEX idx_programs_tur_sira ON programs (tur, sira2024);
CREATE INDEX idx_programs_il ON programs (il);
`);
// CSV satırlarını RFC-4180 tırnak kurallarıyla böler (alan içi virgül/tırnak destekli)
function splitCsvLine(line: string): string[] {
const out: string[] = [];
let cur = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (inQuotes) {
if (ch === '"') {
if (line[i + 1] === '"') {
cur += '"';
i++;
} else {
inQuotes = false;
}
} else {
cur += ch;
}
} else if (ch === '"') {
inQuotes = true;
} else if (ch === ",") {
out.push(cur);
cur = "";
} else {
cur += ch;
}
}
out.push(cur);
return out;
}
function toInt(v: string | undefined): number | null {
if (!v) return null;
const n = Number.parseFloat(v);
return Number.isFinite(n) ? Math.round(n) : null;
}
function toReal(v: string | undefined): number | null {
if (!v) return null;
const n = Number.parseFloat(v);
return Number.isFinite(n) ? n : null;
}
// Kaynak CSV'de 0, "veri yok" anlamına gelir (sıralama/puan 0 olamaz)
function positiveOrNull(n: number | null): number | null {
return n != null && n > 0 ? n : null;
}
async function main() {
const columns = [
"id",
"isim",
"universite",
"unitur",
"il",
"fakulte",
"tur",
"sure",
"onlisans",
...YEARS.flatMap((y) => [
`sira${y}`,
`puan${y}`,
`kontenjan${y}`,
`yerlesen${y}`,
]),
];
const insert = db.prepare(
`INSERT OR REPLACE INTO programs (${columns.join(",")})
VALUES (${columns.map(() => "?").join(",")})`
);
const rl = createInterface({
input: createReadStream(csvPath, "utf8"),
crlfDelay: Infinity,
});
let header: string[] | null = null;
let idx: Record<string, number> = {};
const rows: unknown[][] = [];
let skipped = 0;
for await (const line of rl) {
if (!header) {
header = splitCsvLine(line);
idx = Object.fromEntries(header.map((h, i) => [h, i]));
continue;
}
if (!line.trim()) continue;
const f = splitCsvLine(line);
const get = (col: string) => f[idx[col]]?.trim() ?? "";
const id = get("id");
const isim = get("isim");
const universite = get("universite");
const tur = get("tur");
if (!id || !isim || !universite || !tur) {
skipped++;
continue;
}
rows.push([
id,
isim,
universite,
get("unitur") || null,
get("il") || null,
get("fakulte") || null,
tur,
toInt(get("sure")),
toInt(get("onlisans")) ?? 0,
...YEARS.flatMap((y) => [
positiveOrNull(toInt(get(`sira${y}`))),
positiveOrNull(toReal(get(`puan${y}`))),
toInt(get(`kontenjan${y}`)),
toInt(get(`yerlesen${y}`)),
]),
]);
}
const insertAll = db.transaction((all: unknown[][]) => {
for (const r of all) insert.run(...r);
});
insertAll(rows);
const count = db
.prepare("SELECT COUNT(*) AS c FROM programs")
.get() as { c: number };
const withSira = db
.prepare("SELECT COUNT(*) AS c FROM programs WHERE sira2024 IS NOT NULL")
.get() as { c: number };
console.log(`Yüklendi: ${count.c} program (${skipped} satır atlandı)`);
console.log(`2024 sıralaması olan: ${withSira.c}`);
console.log(`DB: ${dbPath}`);
}
main().then(() => db.close());