Add "detay" script to package.json and enhance ListemSekmeleri component functionality
All checks were successful
Deploy / deploy (push) Successful in 16m29s
All checks were successful
Deploy / deploy (push) Successful in 16m29s
- Introduced a new "detay" script in package.json for additional processing. - Refactored ListemSekmeleri component to improve tab functionality and state management. - Updated UI elements for better user interaction and clarity, including renaming "Seçtiklerim" to "Kendi Listem" for consistency across the application. - Made adjustments to related components for improved integration and user experience.
This commit is contained in:
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
BIN
data/yokatlas.db
BIN
data/yokatlas.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"ingest": "tsx scripts/ingest.ts",
|
"ingest": "tsx scripts/ingest.ts",
|
||||||
"refresh": "tsx scripts/refresh.ts",
|
"refresh": "tsx scripts/refresh.ts",
|
||||||
|
"detay": "tsx scripts/detay.ts",
|
||||||
"logolar": "tsx scripts/logo-indir.ts"
|
"logolar": "tsx scripts/logo-indir.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
267
scripts/detay.ts
Normal file
267
scripts/detay.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* Canlı YÖK Atlas API'sinden program detay verilerini çekip DB'ye işler:
|
||||||
|
*
|
||||||
|
* 1. Kontenjan türü kırılımı (son yerleştirme yılı, ör. 2025):
|
||||||
|
* Genel / Okul Birincisi / Şehit-Gazi / Deprem / 34 Yaş Üstü Kadın
|
||||||
|
* için kontenjan + yerleşen. Kaynak: POST /api/tercih-kilavuz/search
|
||||||
|
* (gk1/gkY1, obk1/obkY1, sgy1/sgyY1, dprm1/dprmY1, y34_1/y34Y1 —
|
||||||
|
* "1" son yerleştirme yılı demek; kayıttaki `yil` kılavuz yılıdır,
|
||||||
|
* veri yılı = yil - 1).
|
||||||
|
*
|
||||||
|
* 2. Yerleşen son kişinin netleri (yıl bazında, tüm puan türleri):
|
||||||
|
* Kaynak: POST /api/netler/search → `netler` tablosu.
|
||||||
|
*
|
||||||
|
* Not: YÖK Atlas 2026'da SPA'ya geçti; eski content/lisans-dynamic/*.php
|
||||||
|
* panelleri (ve onlarla birlikte kadın-erkek dağılımı) yayından kalktı.
|
||||||
|
* Cinsiyet verisi yeni API'lerin hiçbirinde yok.
|
||||||
|
*
|
||||||
|
* Kullanım: npx tsx scripts/detay.ts
|
||||||
|
*/
|
||||||
|
import path from "node:path";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const KILAVUZ_API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||||
|
const NETLER_API = "https://yokatlas.yok.gov.tr/api/netler/search";
|
||||||
|
const PAGE_SIZE = 500;
|
||||||
|
const DELAY_MS = 300;
|
||||||
|
|
||||||
|
type KilavuzRecord = {
|
||||||
|
kilavuzKodu: number;
|
||||||
|
eskiKilavuzKodu?: number | null;
|
||||||
|
yil: number;
|
||||||
|
gk1?: number | null;
|
||||||
|
gkY1?: number | null;
|
||||||
|
obk1?: number | null;
|
||||||
|
obkY1?: number | null;
|
||||||
|
sgy1?: number | null;
|
||||||
|
sgyY1?: number | null;
|
||||||
|
dprm1?: number | null;
|
||||||
|
dprmY1?: number | null;
|
||||||
|
y34_1?: number | null;
|
||||||
|
y34Y1?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NetlerRecord = {
|
||||||
|
kilavuzKodu: number;
|
||||||
|
yil: number;
|
||||||
|
puanTuru: string | null;
|
||||||
|
tabanPuan?: number | null;
|
||||||
|
obp?: number | null;
|
||||||
|
katsayi?: number | null;
|
||||||
|
tytTrkNet?: number | null;
|
||||||
|
tytSosNet?: number | null;
|
||||||
|
tytMatNet?: number | null;
|
||||||
|
tytFenNet?: number | null;
|
||||||
|
aytMatNet?: number | null;
|
||||||
|
aytFizNet?: number | null;
|
||||||
|
aytKimNet?: number | null;
|
||||||
|
aytBioNet?: number | null;
|
||||||
|
aytTdeNet?: number | null;
|
||||||
|
aytTrh1Net?: number | null;
|
||||||
|
aytCog1Net?: number | null;
|
||||||
|
aytTrh2Net?: number | null;
|
||||||
|
aytCog2Net?: number | null;
|
||||||
|
aytDinNet?: number | null;
|
||||||
|
aytFelNet?: number | null;
|
||||||
|
ydtYdilNet?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiPage<T> = { content: T[]; totalPages: number; totalElements: number };
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
async function fetchPage<T>(api: string, page: number): Promise<ApiPage<T>> {
|
||||||
|
const res = await fetch(api, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
filters: {},
|
||||||
|
page,
|
||||||
|
size: PAGE_SIZE,
|
||||||
|
sortBy: "kilavuzKodu",
|
||||||
|
direction: "ASC",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`${api} ${res.status} (sayfa ${page})`);
|
||||||
|
return (await res.json()) as ApiPage<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function num(v: unknown): number | null {
|
||||||
|
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NET_COLS = [
|
||||||
|
"puanTuru",
|
||||||
|
"tabanPuan",
|
||||||
|
"obp",
|
||||||
|
"katsayi",
|
||||||
|
"tytTrkNet",
|
||||||
|
"tytSosNet",
|
||||||
|
"tytMatNet",
|
||||||
|
"tytFenNet",
|
||||||
|
"aytMatNet",
|
||||||
|
"aytFizNet",
|
||||||
|
"aytKimNet",
|
||||||
|
"aytBioNet",
|
||||||
|
"aytTdeNet",
|
||||||
|
"aytTrh1Net",
|
||||||
|
"aytCog1Net",
|
||||||
|
"aytTrh2Net",
|
||||||
|
"aytCog2Net",
|
||||||
|
"aytDinNet",
|
||||||
|
"aytFelNet",
|
||||||
|
"ydtYdilNet",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
|
||||||
|
// ── 1. Kontenjan türü kırılımı ─────────────────────────────────────────
|
||||||
|
const first = await fetchPage<KilavuzRecord>(KILAVUZ_API, 0);
|
||||||
|
// API kaydındaki yil kılavuz yılı; "1" ekli alanlar bir önceki
|
||||||
|
// yerleştirme dönemine ait (2026 kılavuzu → 2025 verisi)
|
||||||
|
const veriYili = first.content[0].yil - 1;
|
||||||
|
|
||||||
|
const kontCols = [
|
||||||
|
`gk${veriYili}`,
|
||||||
|
`gkY${veriYili}`,
|
||||||
|
`obk${veriYili}`,
|
||||||
|
`obkY${veriYili}`,
|
||||||
|
`sgy${veriYili}`,
|
||||||
|
`sgyY${veriYili}`,
|
||||||
|
`dprm${veriYili}`,
|
||||||
|
`dprmY${veriYili}`,
|
||||||
|
`y34k${veriYili}`,
|
||||||
|
`y34kY${veriYili}`,
|
||||||
|
];
|
||||||
|
const mevcut = new Set(
|
||||||
|
(db.prepare("PRAGMA table_info(programs)").all() as { name: string }[]).map(
|
||||||
|
(c) => c.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const col of kontCols) {
|
||||||
|
if (!mevcut.has(col)) {
|
||||||
|
db.exec(`ALTER TABLE programs ADD COLUMN ${col} INTEGER`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateKont = db.prepare(`
|
||||||
|
UPDATE programs SET ${kontCols.map((c) => `${c} = ?`).join(", ")}
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
|
||||||
|
|
||||||
|
// Kılavuz kodu yıllar arasında değişebiliyor (eskiKilavuzKodu); netler
|
||||||
|
// API'si yeni kodla döndüğü için eşleşen DB id'sini haritada tutuyoruz.
|
||||||
|
const dbId = new Map<number, string>();
|
||||||
|
let kontUpdated = 0;
|
||||||
|
let kontMissing = 0;
|
||||||
|
|
||||||
|
const processKilavuz = db.transaction((records: KilavuzRecord[]) => {
|
||||||
|
for (const r of records) {
|
||||||
|
if (!r.kilavuzKodu) continue;
|
||||||
|
let id = String(r.kilavuzKodu);
|
||||||
|
if (!exists.get(id)) {
|
||||||
|
const eski = r.eskiKilavuzKodu ? String(r.eskiKilavuzKodu) : null;
|
||||||
|
if (eski && exists.get(eski)) {
|
||||||
|
id = eski;
|
||||||
|
} else {
|
||||||
|
kontMissing++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dbId.set(r.kilavuzKodu, id);
|
||||||
|
updateKont.run(
|
||||||
|
num(r.gk1),
|
||||||
|
num(r.gkY1),
|
||||||
|
num(r.obk1),
|
||||||
|
num(r.obkY1),
|
||||||
|
num(r.sgy1),
|
||||||
|
num(r.sgyY1),
|
||||||
|
num(r.dprm1),
|
||||||
|
num(r.dprmY1),
|
||||||
|
num(r.y34_1),
|
||||||
|
num(r.y34Y1),
|
||||||
|
id
|
||||||
|
);
|
||||||
|
kontUpdated++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Kontenjan kırılımı (${veriYili}): ${first.totalElements} kayıt, ${first.totalPages} sayfa…`
|
||||||
|
);
|
||||||
|
processKilavuz(first.content);
|
||||||
|
for (let p = 1; p < first.totalPages; p++) {
|
||||||
|
await sleep(DELAY_MS);
|
||||||
|
processKilavuz((await fetchPage<KilavuzRecord>(KILAVUZ_API, p)).content);
|
||||||
|
process.stdout.write(`\rkontenjan sayfa ${p + 1}/${first.totalPages}`);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`\nKontenjan kırılımı: ${kontUpdated} güncellendi, ${kontMissing} DB'de yok`
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 2. Yerleşen son kişinin netleri ────────────────────────────────────
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS netler (
|
||||||
|
program_id TEXT NOT NULL,
|
||||||
|
yil INTEGER NOT NULL,
|
||||||
|
${NET_COLS.map((c) => `${c} ${c === "puanTuru" ? "TEXT" : "REAL"}`).join(
|
||||||
|
",\n "
|
||||||
|
)},
|
||||||
|
PRIMARY KEY (program_id, yil)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertNet = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO netler (program_id, yil, ${NET_COLS.join(", ")})
|
||||||
|
VALUES (${["?", "?", ...NET_COLS.map(() => "?")].join(", ")})
|
||||||
|
`);
|
||||||
|
|
||||||
|
let netRows = 0;
|
||||||
|
const processNetler = db.transaction((records: NetlerRecord[]) => {
|
||||||
|
for (const r of records) {
|
||||||
|
if (!r.kilavuzKodu || !r.yil) continue;
|
||||||
|
const id = dbId.get(r.kilavuzKodu) ?? String(r.kilavuzKodu);
|
||||||
|
insertNet.run(
|
||||||
|
id,
|
||||||
|
r.yil,
|
||||||
|
r.puanTuru?.trim() ?? null,
|
||||||
|
...NET_COLS.slice(1).map((c) => num(r[c as keyof NetlerRecord]))
|
||||||
|
);
|
||||||
|
netRows++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const netFirst = await fetchPage<NetlerRecord>(NETLER_API, 0);
|
||||||
|
console.log(
|
||||||
|
`Netler: ${netFirst.totalElements} kayıt, ${netFirst.totalPages} sayfa…`
|
||||||
|
);
|
||||||
|
processNetler(netFirst.content);
|
||||||
|
for (let p = 1; p < netFirst.totalPages; p++) {
|
||||||
|
await sleep(DELAY_MS);
|
||||||
|
processNetler((await fetchPage<NetlerRecord>(NETLER_API, p)).content);
|
||||||
|
process.stdout.write(`\rnetler sayfa ${p + 1}/${netFirst.totalPages}`);
|
||||||
|
}
|
||||||
|
console.log(`\nNetler: ${netRows} satır yazıldı`);
|
||||||
|
|
||||||
|
const ozet = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT
|
||||||
|
(SELECT COUNT(*) FROM programs WHERE gkY${veriYili} IS NOT NULL) AS kontlu,
|
||||||
|
(SELECT COUNT(*) FROM netler) AS net,
|
||||||
|
(SELECT COUNT(DISTINCT program_id) FROM netler) AS netli_program`
|
||||||
|
)
|
||||||
|
.get() as { kontlu: number; net: number; netli_program: number };
|
||||||
|
console.log(
|
||||||
|
`Özet → kontenjan kırılımı olan program: ${ozet.kontlu}, ` +
|
||||||
|
`net satırı: ${ozet.net} (${ozet.netli_program} program)`
|
||||||
|
);
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,36 +1,85 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { ListChecks, Sparkles } from "lucide-react";
|
import { ListChecks, Sparkles } from "lucide-react";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs as TabsPrimitive } from "radix-ui";
|
||||||
import { SecimlerimPaneli } from "@/components/manuel-liste/secimlerim-paneli";
|
import { SecimlerimPaneli } from "@/components/manuel-liste/secimlerim-paneli";
|
||||||
|
import { useManuelListe } from "@/components/manuel-liste/store";
|
||||||
|
|
||||||
export type ListemSekme = "secimlerim" | "ai";
|
export type ListemSekme = "secimlerim" | "ai";
|
||||||
|
|
||||||
export function ListemSekmeleri({
|
export function ListemSekmeleri({
|
||||||
varsayilan,
|
varsayilan,
|
||||||
aiIcerik,
|
aiIcerik,
|
||||||
|
aiHazir,
|
||||||
}: {
|
}: {
|
||||||
varsayilan: ListemSekme;
|
varsayilan: ListemSekme;
|
||||||
aiIcerik: React.ReactNode;
|
aiIcerik: React.ReactNode;
|
||||||
|
aiHazir?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const [sekme, setSekme] = useState<ListemSekme>(varsayilan);
|
||||||
|
const liste = useManuelListe();
|
||||||
|
|
||||||
|
function degistir(deger: string) {
|
||||||
|
const yeni = deger as ListemSekme;
|
||||||
|
setSekme(yeni);
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("sekme", yeni);
|
||||||
|
window.history.replaceState(null, "", url);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue={varsayilan} className="mt-2 gap-6">
|
<TabsPrimitive.Root value={sekme} onValueChange={degistir} className="mt-6">
|
||||||
<TabsList variant="line" className="h-auto w-full justify-start gap-1">
|
<TabsPrimitive.List
|
||||||
<TabsTrigger value="secimlerim" className="cursor-pointer px-3 py-2.5">
|
aria-label="Liste görünümü"
|
||||||
|
className="relative mx-auto grid w-full max-w-md grid-cols-2 rounded-full border border-slate-200 bg-slate-100 p-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-white shadow-sm transition-transform duration-200 [transition-timing-function:cubic-bezier(0.23,1,0.32,1)]"
|
||||||
|
style={{
|
||||||
|
transform: sekme === "ai" ? "translateX(100%)" : "translateX(0)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
value="secimlerim"
|
||||||
|
className="relative z-10 flex h-10 cursor-pointer items-center justify-center gap-2 rounded-full text-sm font-semibold text-slate-500 transition-colors duration-200 hover:text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[state=active]:text-slate-900"
|
||||||
|
>
|
||||||
<ListChecks className="size-4" aria-hidden />
|
<ListChecks className="size-4" aria-hidden />
|
||||||
Seçtiklerim
|
Kendi Listem
|
||||||
</TabsTrigger>
|
{liste.length > 0 ? (
|
||||||
<TabsTrigger value="ai" className="cursor-pointer px-3 py-2.5">
|
<span className="min-w-5 rounded-full bg-primary/10 px-1.5 py-0.5 text-center text-[11px] font-bold tabular-nums text-primary">
|
||||||
|
{liste.length}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TabsPrimitive.Trigger>
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
value="ai"
|
||||||
|
className="relative z-10 flex h-10 cursor-pointer items-center justify-center gap-2 rounded-full text-sm font-semibold text-slate-500 transition-colors duration-200 hover:text-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-[state=active]:text-slate-900"
|
||||||
|
>
|
||||||
<Sparkles className="size-4" aria-hidden />
|
<Sparkles className="size-4" aria-hidden />
|
||||||
AI Listem
|
AI Listem
|
||||||
</TabsTrigger>
|
{aiHazir ? (
|
||||||
</TabsList>
|
<span
|
||||||
<TabsContent value="secimlerim" className="mt-0">
|
className="size-1.5 rounded-full bg-emerald-500"
|
||||||
|
aria-hidden
|
||||||
|
title="AI listen hazır"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TabsPrimitive.Trigger>
|
||||||
|
</TabsPrimitive.List>
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
value="secimlerim"
|
||||||
|
className="mt-6 outline-none data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-200"
|
||||||
|
>
|
||||||
<SecimlerimPaneli />
|
<SecimlerimPaneli />
|
||||||
</TabsContent>
|
</TabsPrimitive.Content>
|
||||||
<TabsContent value="ai" className="mt-0">
|
<TabsPrimitive.Content
|
||||||
|
value="ai"
|
||||||
|
className="mt-6 outline-none data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-200"
|
||||||
|
>
|
||||||
{aiIcerik}
|
{aiIcerik}
|
||||||
</TabsContent>
|
</TabsPrimitive.Content>
|
||||||
</Tabs>
|
</TabsPrimitive.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default async function ListemPage({
|
|||||||
<p className="mt-3 text-slate-600">
|
<p className="mt-3 text-slate-600">
|
||||||
Sıralamanı girip birkaç adımda 24 tercihlik risk analizli listeni kur;
|
Sıralamanı girip birkaç adımda 24 tercihlik risk analizli listeni kur;
|
||||||
sonra burada görüntüleyip AI danışmanla üzerinde konuşabilirsin.
|
sonra burada görüntüleyip AI danışmanla üzerinde konuşabilirsin.
|
||||||
Seçtiklerim sekmesinde ise tablolardan eklediğin programlar durur.
|
Kendi Listem sekmesinde ise tablolardan eklediğin programlar durur.
|
||||||
</p>
|
</p>
|
||||||
<PagePixelDivider seed={67} className="mx-auto mt-6" />
|
<PagePixelDivider seed={67} className="mx-auto mt-6" />
|
||||||
<Button
|
<Button
|
||||||
@@ -101,17 +101,17 @@ export default async function ListemPage({
|
|||||||
if (!raporVar) {
|
if (!raporVar) {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-7xl px-4 py-8 sm:py-10">
|
<main className="mx-auto w-full max-w-7xl px-4 py-8 sm:py-10">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight sm:text-3xl">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
Tercih karar merkezin
|
<h1 className="font-heading text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
</h1>
|
Tercih karar merkezin
|
||||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">
|
</h1>
|
||||||
Seçtiklerin ve AI listen burada yan yana — ikisi birbirinden bağımsız.
|
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||||
</p>
|
Kendi listen ve AI listen burada yan yana — ikisi birbirinden
|
||||||
<PagePixelDivider seed={71} className="mt-6" />
|
bağımsız.
|
||||||
<ListemSekmeleri
|
</p>
|
||||||
varsayilan={varsayilanSekme}
|
<PagePixelDivider seed={71} className="mx-auto mt-6" />
|
||||||
aiIcerik={aiBos}
|
</div>
|
||||||
/>
|
<ListemSekmeleri varsayilan={varsayilanSekme} aiIcerik={aiBos} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -375,14 +375,21 @@ export default async function ListemPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-7xl px-4 py-8 sm:py-10">
|
<main className="mx-auto w-full max-w-7xl px-4 py-8 sm:py-10">
|
||||||
<h1 className="font-heading text-2xl font-bold tracking-tight sm:text-3xl">
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
Tercih karar merkezin
|
<h1 className="font-heading text-2xl font-bold tracking-tight sm:text-3xl">
|
||||||
</h1>
|
Tercih karar merkezin
|
||||||
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">
|
</h1>
|
||||||
Seçtiklerin ve AI listen burada yan yana — ikisi birbirinden bağımsız.
|
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||||
</p>
|
Kendi listen ve AI listen burada yan yana — ikisi birbirinden
|
||||||
<PagePixelDivider seed={71} className="mt-6" />
|
bağımsız.
|
||||||
<ListemSekmeleri varsayilan={varsayilanSekme} aiIcerik={aiIcerik} />
|
</p>
|
||||||
|
<PagePixelDivider seed={71} className="mx-auto mt-6" />
|
||||||
|
</div>
|
||||||
|
<ListemSekmeleri
|
||||||
|
varsayilan={varsayilanSekme}
|
||||||
|
aiIcerik={aiIcerik}
|
||||||
|
aiHazir
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import type { SihirbazFacetleri } from "@/lib/db";
|
|||||||
import {
|
import {
|
||||||
SIHIRBAZ_STORAGE_KEY,
|
SIHIRBAZ_STORAGE_KEY,
|
||||||
sihirbazDogrula,
|
sihirbazDogrula,
|
||||||
|
tercihProfiliSil,
|
||||||
|
tercihProfiliYaz,
|
||||||
type SihirbazSecimleri,
|
type SihirbazSecimleri,
|
||||||
} from "@/lib/sihirbaz";
|
} from "@/lib/sihirbaz";
|
||||||
import { SihirbazModal } from "./sihirbaz-modal";
|
import { SihirbazModal } from "./sihirbaz-modal";
|
||||||
@@ -210,9 +212,7 @@ export function SihirbazBolumu({
|
|||||||
});
|
});
|
||||||
setKredi(sonuc.kredi);
|
setKredi(sonuc.kredi);
|
||||||
setPaketli(sonuc.hasPaket);
|
setPaketli(sonuc.hasPaket);
|
||||||
try {
|
tercihProfiliSil();
|
||||||
localStorage.removeItem(SIHIRBAZ_STORAGE_KEY);
|
|
||||||
} catch {}
|
|
||||||
// AI çıktısı bu sayfada gösterilmez: üretim animasyonu açık kalır,
|
// AI çıktısı bu sayfada gösterilmez: üretim animasyonu açık kalır,
|
||||||
// liste (paketsizse paywall'lı hâliyle) /listem'de karşılar.
|
// liste (paketsizse paywall'lı hâliyle) /listem'de karşılar.
|
||||||
router.push("/listem");
|
router.push("/listem");
|
||||||
@@ -240,12 +240,7 @@ export function SihirbazBolumu({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function secimleriKaydetVeGirise(secimler: SihirbazSecimleri) {
|
function secimleriKaydetVeGirise(secimler: SihirbazSecimleri) {
|
||||||
try {
|
tercihProfiliYaz({ sira, tur, secimler });
|
||||||
localStorage.setItem(
|
|
||||||
SIHIRBAZ_STORAGE_KEY,
|
|
||||||
JSON.stringify({ sira, tur, secimler }),
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
const geri = `/sonuc?sira=${sira}&tur=${tur}&sihirbaz=1`;
|
const geri = `/sonuc?sira=${sira}&tur=${tur}&sihirbaz=1`;
|
||||||
router.push(`/giris?callback=${encodeURIComponent(geri)}`);
|
router.push(`/giris?callback=${encodeURIComponent(geri)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "@/components/sihirbaz-adimlar-lazy";
|
} from "@/components/sihirbaz-adimlar-lazy";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import type { SihirbazFacetleri } from "@/lib/db";
|
import type { SihirbazFacetleri } from "@/lib/db";
|
||||||
import { SIHIRBAZ_STORAGE_KEY, type SihirbazSecimleri } from "@/lib/sihirbaz";
|
import { tercihProfiliYaz, type SihirbazSecimleri } from "@/lib/sihirbaz";
|
||||||
|
|
||||||
// lib/db sunucu tarafı; etiketler client bundle'a sızmasın diye burada.
|
// lib/db sunucu tarafı; etiketler client bundle'a sızmasın diye burada.
|
||||||
const PUAN_TURLERI_SECENEK = [
|
const PUAN_TURLERI_SECENEK = [
|
||||||
@@ -82,12 +82,7 @@ export function HeroForm() {
|
|||||||
// oradan gelir (değer önce — giriş duvarı sihirbazın hemen arkasında değil).
|
// oradan gelir (değer önce — giriş duvarı sihirbazın hemen arkasında değil).
|
||||||
function secimleriTamamla(secimler: SihirbazSecimleri) {
|
function secimleriTamamla(secimler: SihirbazSecimleri) {
|
||||||
if (!sira) return;
|
if (!sira) return;
|
||||||
try {
|
tercihProfiliYaz({ sira, tur, secimler });
|
||||||
localStorage.setItem(
|
|
||||||
SIHIRBAZ_STORAGE_KEY,
|
|
||||||
JSON.stringify({ sira, tur, secimler }),
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
setModalAcik(false);
|
setModalAcik(false);
|
||||||
router.push(
|
router.push(
|
||||||
girisli
|
girisli
|
||||||
|
|||||||
@@ -6,12 +6,19 @@ import { RehberKapak } from "@/components/rehber-kapak";
|
|||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
import { aramaNormalize, eslesmePuani } from "@/lib/arama";
|
import { aramaNormalize, eslesmePuani } from "@/lib/arama";
|
||||||
import type { RehberOzet } from "@/lib/rehber";
|
import type { RehberOzet } from "@/lib/rehber";
|
||||||
|
import { PUAN_TURU_ETIKET } from "@/lib/sihirbaz";
|
||||||
|
import {
|
||||||
|
profilDuzenlemeyiAc,
|
||||||
|
useTercihProfili,
|
||||||
|
} from "@/components/manuel-liste/profil-kapisi-store";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
Building2,
|
Building2,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
Search,
|
Search,
|
||||||
|
SquarePen,
|
||||||
|
Trophy,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +57,7 @@ const REHBER_SONUC_SINIRI = 4;
|
|||||||
const sayi = (deger: number) => deger.toLocaleString("tr-TR");
|
const sayi = (deger: number) => deger.toLocaleString("tr-TR");
|
||||||
|
|
||||||
export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
||||||
|
const profil = useTercihProfili();
|
||||||
const [acik, setAcik] = useState(false);
|
const [acik, setAcik] = useState(false);
|
||||||
const [sorgu, setSorgu] = useState("");
|
const [sorgu, setSorgu] = useState("");
|
||||||
const [sonuclar, setSonuclar] = useState<AramaSonuclari>(BOS_SONUCLAR);
|
const [sonuclar, setSonuclar] = useState<AramaSonuclari>(BOS_SONUCLAR);
|
||||||
@@ -170,18 +178,43 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
|||||||
<>
|
<>
|
||||||
{/* lg altında logo + sağ küme ile çakışır (absolute); o aralıkta gizli */}
|
{/* lg altında logo + sağ küme ile çakışır (absolute); o aralıkta gizli */}
|
||||||
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 lg:flex">
|
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 lg:flex">
|
||||||
<Link
|
{profil ? (
|
||||||
href="/#nasil-calisir"
|
<button
|
||||||
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
type="button"
|
||||||
>
|
onClick={profilDuzenlemeyiAc}
|
||||||
Nasıl çalışır?
|
aria-label={`Sıralaman ${profil.sira.toLocaleString("tr-TR")} (${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}) — tercihlerini düzenle`}
|
||||||
</Link>
|
className="group/profil flex cursor-pointer items-center gap-2.5 rounded-full py-1.5 pl-4 pr-1.5 transition-colors duration-200 hover:bg-slate-100"
|
||||||
<Link
|
>
|
||||||
href="/#karsilastirma"
|
<Trophy className="size-4 text-orange-500" aria-hidden />
|
||||||
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
<span className="flex items-baseline gap-1.5">
|
||||||
>
|
<span className="font-semibold tabular-nums tracking-tight text-slate-900">
|
||||||
Karşılaştır
|
{profil.sira.toLocaleString("tr-TR")}
|
||||||
</Link>
|
</span>
|
||||||
|
<span className="text-xs font-medium text-slate-400">
|
||||||
|
{PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-1.5 text-xs font-medium text-slate-500 transition-colors duration-200 group-hover/profil:bg-white group-hover/profil:text-slate-900">
|
||||||
|
<SquarePen className="size-3" aria-hidden />
|
||||||
|
Düzenle
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href="/#nasil-calisir"
|
||||||
|
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||||||
|
>
|
||||||
|
Nasıl çalışır?
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/#karsilastirma"
|
||||||
|
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||||||
|
>
|
||||||
|
Karşılaştır
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAcik(true)}
|
onClick={() => setAcik(true)}
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ import {
|
|||||||
} from "./store";
|
} from "./store";
|
||||||
|
|
||||||
// Risk renk dili ürünün geri kalanıyla birebir aynı (bkz. rapor-listesi)
|
// Risk renk dili ürünün geri kalanıyla birebir aynı (bkz. rapor-listesi)
|
||||||
const RISK_STIL: Record<RiskSeviyesi, { nokta: string; kenar: string }> = {
|
const RISK_NOKTA: Record<RiskSeviyesi, string> = {
|
||||||
guvenli: { nokta: "bg-emerald-500", kenar: "border-l-emerald-500" },
|
guvenli: "bg-emerald-500",
|
||||||
"az-riskli": { nokta: "bg-amber-500", kenar: "border-l-amber-500" },
|
"az-riskli": "bg-amber-500",
|
||||||
riskli: { nokta: "bg-red-500", kenar: "border-l-red-500" },
|
riskli: "bg-red-500",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ListeCekmecesi() {
|
export function ListeCekmecesi() {
|
||||||
@@ -137,7 +137,7 @@ function CekmeceSheet() {
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h2 className="flex items-center gap-2 font-heading text-lg font-bold">
|
<h2 className="flex items-center gap-2 font-heading text-lg font-bold">
|
||||||
<ListChecks className="size-5 text-primary" aria-hidden />
|
<ListChecks className="size-5 text-primary" aria-hidden />
|
||||||
Seçtiklerim
|
Kendi Listem
|
||||||
<span className="font-heading text-sm font-bold tabular-nums text-slate-400">
|
<span className="font-heading text-sm font-bold tabular-nums text-slate-400">
|
||||||
{liste.length}/{MANUEL_LISTE_MAX}
|
{liste.length}/{MANUEL_LISTE_MAX}
|
||||||
</span>
|
</span>
|
||||||
@@ -182,6 +182,26 @@ function CekmeceSheet() {
|
|||||||
Bölüm, üniversite veya sonuç tablolarından eklediğin programlar —
|
Bölüm, üniversite veya sonuç tablolarından eklediğin programlar —
|
||||||
tutamaktan sürükleyip sırala, sağa kaydırıp sil.
|
tutamaktan sürükleyip sırala, sağa kaydırıp sil.
|
||||||
</p>
|
</p>
|
||||||
|
<ul
|
||||||
|
className="mt-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-slate-500"
|
||||||
|
aria-label="Yerleşme riski renkleri"
|
||||||
|
>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["guvenli", "yeşil güvenli"],
|
||||||
|
["az-riskli", "turuncu az riskli"],
|
||||||
|
["riskli", "kırmızı riskli"],
|
||||||
|
] as const
|
||||||
|
).map(([seviye, aciklama]) => (
|
||||||
|
<li key={seviye} className="inline-flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className={`size-2 shrink-0 rounded-full ${RISK_NOKTA[seviye]}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span>{aciklama}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Gövde: sürüklenebilir satırlar */}
|
{/* Gövde: sürüklenebilir satırlar */}
|
||||||
@@ -235,7 +255,7 @@ function CekmeceSheet() {
|
|||||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary transition-colors duration-200 hover:text-primary/80"
|
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary transition-colors duration-200 hover:text-primary/80"
|
||||||
>
|
>
|
||||||
<ListChecks className="size-4" aria-hidden />
|
<ListChecks className="size-4" aria-hidden />
|
||||||
Seçtiklerimi ve uyum notlarını aç
|
Kendi listemi ve uyum notlarını aç
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/listem?sekme=ai"
|
href="/listem?sekme=ai"
|
||||||
@@ -263,7 +283,6 @@ function CekmeceSatiri({
|
|||||||
sira: number;
|
sira: number;
|
||||||
projeksiyonAcik: boolean;
|
projeksiyonAcik: boolean;
|
||||||
}) {
|
}) {
|
||||||
const stil = tercih.risk ? RISK_STIL[tercih.risk] : null;
|
|
||||||
const siraKontrol = useDragControls();
|
const siraKontrol = useDragControls();
|
||||||
|
|
||||||
// Kartın yatay konumu: sil bandının görünürlüğü buna bağlı
|
// Kartın yatay konumu: sil bandının görünürlüğü buna bağlı
|
||||||
@@ -291,11 +310,7 @@ function CekmeceSatiri({
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, x: 96, transition: { duration: 0.18 } }}
|
exit={{ opacity: 0, x: 96, transition: { duration: 0.18 } }}
|
||||||
transition={{ type: "spring", duration: 0.4, bounce: 0 }}
|
transition={{ type: "spring", duration: 0.4, bounce: 0 }}
|
||||||
whileDrag={{
|
whileDrag={{ scale: 1.03, zIndex: 10 }}
|
||||||
scale: 1.03,
|
|
||||||
boxShadow: "0 12px 32px rgba(15, 23, 42, 0.16)",
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
className="relative select-none"
|
className="relative select-none"
|
||||||
>
|
>
|
||||||
{/* Sil bandı: kart sağa kaydıkça altından görünür */}
|
{/* Sil bandı: kart sağa kaydıkça altından görünür */}
|
||||||
@@ -315,9 +330,7 @@ function CekmeceSatiri({
|
|||||||
dragElastic={{ left: 0, right: 0.7 }}
|
dragElastic={{ left: 0, right: 0.7 }}
|
||||||
dragSnapToOrigin
|
dragSnapToOrigin
|
||||||
onDragEnd={kaydirinca}
|
onDragEnd={kaydirinca}
|
||||||
className={`relative flex items-center gap-2.5 rounded-xl border border-l-4 border-slate-200 bg-white px-3 py-2.5 ${
|
className="relative flex items-center gap-2.5 rounded-xl border border-slate-200 bg-white px-3 py-2.5"
|
||||||
stil?.kenar ?? "border-l-slate-300"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -350,7 +363,7 @@ function CekmeceSatiri({
|
|||||||
) : null}
|
) : null}
|
||||||
{tercih.risk ? (
|
{tercih.risk ? (
|
||||||
<span
|
<span
|
||||||
className={`size-2.5 shrink-0 rounded-full ${RISK_STIL[tercih.risk].nokta}`}
|
className={`size-2.5 shrink-0 rounded-full ${RISK_NOKTA[tercih.risk]}`}
|
||||||
aria-label={RISK_ETIKET[tercih.risk]}
|
aria-label={RISK_ETIKET[tercih.risk]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ export function ListemButonu() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={cekmeceDegistir}
|
onClick={cekmeceDegistir}
|
||||||
aria-expanded={acik}
|
aria-expanded={acik}
|
||||||
aria-label={`Seçtiklerim — ${liste.length}/${MANUEL_LISTE_MAX} program`}
|
aria-label={`Kendi listem — ${liste.length}/${MANUEL_LISTE_MAX} program`}
|
||||||
title="Tablodan seçtiğin programlar"
|
title="Tablodan seçtiğin programlar"
|
||||||
className="relative inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-[color,background-color,border-color,transform] duration-200 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.97] sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
|
className="relative inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-[color,background-color,border-color,transform] duration-200 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.97] sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
|
||||||
>
|
>
|
||||||
<ListChecks className="size-3.5 text-primary sm:size-4" aria-hidden />
|
<ListChecks className="size-3.5 text-primary sm:size-4" aria-hidden />
|
||||||
{/* lg–xl: ortadaki nav ile yer dar — metin gizli, ikon+rozet kalır */}
|
{/* lg–xl: ortadaki nav ile yer dar — metin gizli, ikon+rozet kalır */}
|
||||||
<span className="hidden sm:inline lg:hidden xl:inline">Seçtiklerim</span>
|
<span className="hidden sm:inline lg:hidden xl:inline">Kendi Listem</span>
|
||||||
{liste.length > 0 ? (
|
{liste.length > 0 ? (
|
||||||
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
|
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
|
||||||
<motion.span
|
<motion.span
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import type { Program } from "@/lib/db";
|
import type { Program } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
|
PROFIL_DEGISTI_EVENT,
|
||||||
tercihProfiliOku,
|
tercihProfiliOku,
|
||||||
tercihProfiliTamamlandiMi,
|
tercihProfiliTamamlandiMi,
|
||||||
type TercihProfili,
|
type TercihProfili,
|
||||||
@@ -75,6 +76,12 @@ function yukle() {
|
|||||||
profiliTazele();
|
profiliTazele();
|
||||||
yayinla();
|
yayinla();
|
||||||
});
|
});
|
||||||
|
// Aynı sekmedeki yazmalar (hero formu, AI sihirbazı) storage olayı
|
||||||
|
// tetiklemez; tercihProfiliYaz/Sil bu olayı yayınlar.
|
||||||
|
window.addEventListener(PROFIL_DEGISTI_EVENT, () => {
|
||||||
|
profiliTazele();
|
||||||
|
yayinla();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function abone(cb: () => void) {
|
function abone(cb: () => void) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function SecimlerimPaneli() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="flex items-center gap-2 font-heading text-xl font-bold">
|
<h2 className="flex items-center gap-2 font-heading text-xl font-bold">
|
||||||
<ListChecks className="size-5 text-primary" aria-hidden />
|
<ListChecks className="size-5 text-primary" aria-hidden />
|
||||||
Seçtiklerim
|
Kendi Listem
|
||||||
<span className="text-sm font-bold tabular-nums text-slate-400">
|
<span className="text-sm font-bold tabular-nums text-slate-400">
|
||||||
{liste.length}/{MANUEL_LISTE_MAX}
|
{liste.length}/{MANUEL_LISTE_MAX}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu";
|
import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu";
|
||||||
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
|
|
||||||
const DILIMLER: {
|
const DILIMLER: {
|
||||||
key: DilimKey;
|
key: DilimKey;
|
||||||
@@ -337,31 +338,38 @@ function ProgramSatiri({
|
|||||||
) : null}
|
) : null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-0 w-full">
|
<TableCell className="max-w-0 w-full">
|
||||||
<span className="block truncate text-sm font-semibold">{p.isim}</span>
|
<span className="flex min-w-0 items-center gap-2.5">
|
||||||
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
|
<UniLogo ad={p.universite} boy="sm" />
|
||||||
<span
|
<span className="min-w-0 flex-1">
|
||||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
<span className="block truncate text-sm font-semibold">
|
||||||
devlet
|
{p.isim}
|
||||||
? "bg-slate-100 text-slate-600"
|
</span>
|
||||||
: "bg-violet-50 text-violet-600"
|
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
|
||||||
}`}
|
<span
|
||||||
>
|
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||||
{devlet ? "Devlet" : "Vakıf"}
|
devlet
|
||||||
|
? "bg-slate-100 text-slate-600"
|
||||||
|
: "bg-violet-50 text-violet-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{devlet ? "Devlet" : "Vakıf"}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">
|
||||||
|
<Link
|
||||||
|
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
||||||
|
className="hover:text-primary hover:underline"
|
||||||
|
title={`${p.universite} taban puanları sayfası`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{p.universite}
|
||||||
|
</Link>
|
||||||
|
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
||||||
|
</span>
|
||||||
|
{p.sure ? (
|
||||||
|
<span className="shrink-0 text-slate-400">· {p.sure} yıl</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">
|
|
||||||
<Link
|
|
||||||
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
|
||||||
className="hover:text-primary hover:underline"
|
|
||||||
title={`${p.universite} taban puanları sayfası`}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{p.universite}
|
|
||||||
</Link>
|
|
||||||
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
|
||||||
</span>
|
|
||||||
{p.sure ? (
|
|
||||||
<span className="shrink-0 text-slate-400">· {p.sure} yıl</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
<TableCell className="hidden text-right text-xs md:table-cell">
|
||||||
|
|||||||
@@ -23,10 +23,26 @@ export const UNIVERSITE_TIPI_ETIKET: Record<UniturGrubu | "farketmez", string> =
|
|||||||
|
|
||||||
export type UniversiteTipi = UniturGrubu | "farketmez";
|
export type UniversiteTipi = UniturGrubu | "farketmez";
|
||||||
|
|
||||||
|
export const PUAN_TURU_ETIKET: Record<string, string> = {
|
||||||
|
say: "Sayısal",
|
||||||
|
ea: "Eşit Ağırlık",
|
||||||
|
soz: "Sözel",
|
||||||
|
dil: "Dil",
|
||||||
|
tyt: "TYT",
|
||||||
|
};
|
||||||
|
|
||||||
// Girişsiz kullanıcının modalda yaptığı seçimler, giriş sonrası /sonuc'a
|
// Girişsiz kullanıcının modalda yaptığı seçimler, giriş sonrası /sonuc'a
|
||||||
// dönene kadar burada bekler.
|
// dönene kadar burada bekler.
|
||||||
export const SIHIRBAZ_STORAGE_KEY = "kolaytercih.sihirbaz";
|
export const SIHIRBAZ_STORAGE_KEY = "kolaytercih.sihirbaz";
|
||||||
|
|
||||||
|
// storage olayı yalnızca diğer sekmelerde tetiklenir; aynı sekmedeki
|
||||||
|
// aboneler (ör. navbar sıralama rozeti) bu olayla haberdar edilir.
|
||||||
|
export const PROFIL_DEGISTI_EVENT = "kolaytercih:profil-degisti";
|
||||||
|
|
||||||
|
function profilDegistiginiYayinla(): void {
|
||||||
|
window.dispatchEvent(new Event(PROFIL_DEGISTI_EVENT));
|
||||||
|
}
|
||||||
|
|
||||||
export interface SihirbazSecimleri {
|
export interface SihirbazSecimleri {
|
||||||
kategoriler: string[];
|
kategoriler: string[];
|
||||||
iller: string[];
|
iller: string[];
|
||||||
@@ -128,6 +144,16 @@ export function tercihProfiliYaz(profil: TercihProfili): void {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(SIHIRBAZ_STORAGE_KEY, JSON.stringify(dogru));
|
localStorage.setItem(SIHIRBAZ_STORAGE_KEY, JSON.stringify(dogru));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
profilDegistiginiYayinla();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Profili siler (ör. AI listesi üretilip seçimler tüketildiğinde). */
|
||||||
|
export function tercihProfiliSil(): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(SIHIRBAZ_STORAGE_KEY);
|
||||||
|
} catch {}
|
||||||
|
profilDegistiginiYayinla();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tercihProfiliTamamlandiMi(): boolean {
|
export function tercihProfiliTamamlandiMi(): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user