diff --git a/src/app/api/programlar/route.ts b/src/app/api/programlar/route.ts index a0c87be..c5950cd 100644 --- a/src/app/api/programlar/route.ts +++ b/src/app/api/programlar/route.ts @@ -1,5 +1,6 @@ import { NextResponse, type NextRequest } from "next/server"; import { dilimAra } from "@/lib/db"; +import { programOzetle } from "@/features/liste/liste-queries"; import { PUAN_TURLERI, type DilimKey, @@ -39,5 +40,6 @@ export function GET(request: NextRequest) { limit: SAYFA_BOYU, offset, }); - return NextResponse.json({ programlar, toplam }); + // Tablo satırının kullanmadığı alanlar payload'a girmesin + return NextResponse.json({ programlar: programlar.map(programOzetle), toplam }); } diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts index 4806bbc..d77d610 100644 --- a/src/app/api/soru/route.ts +++ b/src/app/api/soru/route.ts @@ -106,17 +106,19 @@ export async function GET() { } export async function POST(request: NextRequest) { - const session = await getSession(); + // Oturum ve gövde ayrıştırma bağımsız — paralel başlar, hata önceliği aynı + // kalır (önce 401, sonra 400). + const [session, hamBody] = await Promise.all([ + getSession(), + request.json().catch(() => null), + ]); if (!session) { return Response.json({ error: "Oturum gerekli." }, { status: 401 }); } - - let body: { message?: unknown; clientMessageId?: unknown }; - try { - body = await request.json(); - } catch { + if (hamBody === null || typeof hamBody !== "object") { return Response.json({ error: "Geçersiz istek." }, { status: 400 }); } + const body = hamBody as { message?: unknown; clientMessageId?: unknown }; const mesaj = typeof body.message === "string" ? body.message.trim() : ""; const clientMessageId = typeof body.clientMessageId === "string" ? body.clientMessageId : ""; @@ -124,24 +126,30 @@ export async function POST(request: NextRequest) { return Response.json({ error: "Geçersiz mesaj." }, { status: 400 }); } - // Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz) - const rapor = await kullanicininRaporu(session.user.id); + // Üç okuma da yalnızca session.user.id'ye bağlı — tek round trip'te paralel. + // Geçmiş krediyi düşmeden ÖNCE okunur ki kullanıcı mesajı dahil olmasın; + // hasPaket'i spendCreditForMessage değiştirmez, erken okumak güvenli. + const [rapor, gecmisHam, kullanici] = await Promise.all([ + // Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz) + kullanicininRaporu(session.user.id), + appDb + .select() + .from(schema.chatMessages) + .where(eq(schema.chatMessages.userId, session.user.id)) + .orderBy(desc(schema.chatMessages.createdAt)) + .limit(MAX_GECMIS), + appDb.query.user.findFirst({ + where: eq(schema.user.id, session.user.id), + columns: { hasPaket: true }, + }), + ]); if (!rapor?.result) { return Response.json( { error: "Önce listeni oluşturmalısın.", code: "NO_REPORT" }, { status: 409 }, ); } - - // Geçmişi krediyi düşmeden ÖNCE oku ki kullanıcı mesajı dahil olmasın - const gecmis = ( - await appDb - .select() - .from(schema.chatMessages) - .where(eq(schema.chatMessages.userId, session.user.id)) - .orderBy(desc(schema.chatMessages.createdAt)) - .limit(MAX_GECMIS) - ).reverse(); + const gecmis = gecmisHam.reverse(); // Kredi düşümü stream'den önce — yetersizse hiç başlama const harcama = await spendCreditForMessage({ @@ -166,10 +174,6 @@ export async function POST(request: NextRequest) { // Paketsiz kullanıcının prompt'una yalnızca açık satırlar girer — aksi // halde danışman kilitli listeyi sözle sızdırabilir. - const kullanici = await appDb.query.user.findFirst({ - where: eq(schema.user.id, session.user.id), - columns: { hasPaket: true }, - }); const kilitli = !kullanici?.hasPaket; const tamOzet = listeOzetiCikar( rapor.result as RaporSonuc, diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 18c75dd..72c5fd3 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { KatalogArama } from "@/features/katalog/components/katalog-arama"; import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav"; -import { getAllRehberler } from "@/lib/rehber"; +import { getRehberAramaKayitlari } from "@/lib/rehber"; export function SiteHeader() { return ( @@ -32,7 +32,7 @@ export function SiteHeader() { - + {/* UserNav session+DB bekler; Suspense dışına taşarsa tüm kabuk bloklanır */} }> diff --git a/src/features/katalog/components/bolum-listesi.tsx b/src/features/katalog/components/bolum-listesi.tsx index e1d88a6..3a49747 100644 --- a/src/features/katalog/components/bolum-listesi.tsx +++ b/src/features/katalog/components/bolum-listesi.tsx @@ -34,15 +34,26 @@ function KategoriGruplari({ bolumler: BolumOzet[]; seviye: "lisans" | "onlisans"; }) { - const kalanlar = new Set(bolumler.map((b) => b.slug)); - const gruplar = KATEGORILER.map((kategori) => { - const uyeler = bolumler.filter((b) => - (b.kategoriler as readonly string[]).includes(kategori), - ); - for (const b of uyeler) kalanlar.delete(b.slug); - return { kategori: kategori as string, uyeler }; - }).filter((g) => g.uyeler.length > 0); - const diger = bolumler.filter((b) => kalanlar.has(b.slug)); + // Tek geçişte kategori index'i — kategori×bölüm çapraz filter yerine. + // Bir bölüm birden çok kategoride görünebilir; hiçbirinde yoksa "Diğer"e düşer. + const uyeMap = new Map(); + const diger: BolumOzet[] = []; + const gecerli = new Set(KATEGORILER); + for (const b of bolumler) { + let eslesti = false; + for (const kategori of b.kategoriler as readonly string[]) { + if (!gecerli.has(kategori)) continue; + const grup = uyeMap.get(kategori); + if (grup) grup.push(b); + else uyeMap.set(kategori, [b]); + eslesti = true; + } + if (!eslesti) diger.push(b); + } + const gruplar = KATEGORILER.filter((k) => uyeMap.has(k)).map((kategori) => ({ + kategori: kategori as string, + uyeler: uyeMap.get(kategori)!, + })); if (diger.length > 0) gruplar.push({ kategori: "Diğer", uyeler: diger }); return ( diff --git a/src/features/katalog/components/katalog-arama.tsx b/src/features/katalog/components/katalog-arama.tsx index a5765b7..a5b0968 100644 --- a/src/features/katalog/components/katalog-arama.tsx +++ b/src/features/katalog/components/katalog-arama.tsx @@ -1,13 +1,20 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { AramaFunnelKarti } from "@/components/arama-funnel-karti"; import { RehberKapak } from "@/features/rehber/components/rehber-kapak"; import { UniLogo } from "@/components/uni-logo"; import { aramaNormalize, eslesmePuani } from "@/lib/arama"; -import type { RehberOzet } from "@/lib/rehber"; +import type { RehberAramaKaydi } from "@/lib/rehber"; import { profilDuzenlemeyiAc, sonucaGitIste, @@ -60,6 +67,18 @@ const BOS_SONUCLAR: AramaSonuclari = { bolumler: [], universiteler: [] }; const REHBER_SONUC_SINIRI = 4; const sayi = (deger: number) => deger.toLocaleString("tr-TR"); +// Platform değişmez — abonelik gerektirmeyen dış değer (useSyncExternalStore) +const bosAbonelik = () => () => {}; +const macMiSunucu = () => true; +function macMiOku(): boolean { + const platform = + (navigator as { userAgentData?: { platform?: string } }).userAgentData + ?.platform ?? + navigator.platform ?? + ""; + return /mac|iphone|ipad/i.test(platform); +} + const KESFET_TONLARI = { slate: { pencere: "border-slate-200/80 bg-white", @@ -81,7 +100,7 @@ const KESFET_TONLARI = { }, } as const; -export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) { +export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) { const router = useRouter(); const profil = useTercihProfili(); const [acik, setAcik] = useState(false); @@ -95,7 +114,10 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) { const [sorgu, setSorgu] = useState(""); const [sonuclar, setSonuclar] = useState(BOS_SONUCLAR); const [yukleniyor, setYukleniyor] = useState(false); - const [macMi, setMacMi] = useState(true); + // Kısayol etiketi (⌘/Ctrl) platforma bağlı sabit bir dış değer: effect'te + // setState yerine useSyncExternalStore — SSR'da ⌘ (eski varsayılan), + // hydration sonrası gerçek platform. + const macMi = useSyncExternalStore(bosAbonelik, macMiOku, macMiSunucu); const inputRef = useRef(null); const kaydirmaRef = useRef(null); const [kenarBlur, setKenarBlur] = useState({ ust: false, alt: false }); @@ -139,15 +161,6 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) { sonuclar.universiteler.length > 0 || rehberSonuclari.length > 0; - useEffect(() => { - const platform = - (navigator as { userAgentData?: { platform?: string } }).userAgentData - ?.platform ?? - navigator.platform ?? - ""; - setMacMi(/mac|iphone|ipad/i.test(platform)); - }, []); - useEffect(() => { function kisayolDinle(event: KeyboardEvent) { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { @@ -722,7 +735,7 @@ function RehberSonucKarti({ yazi, onSec, }: { - yazi: RehberOzet; + yazi: RehberAramaKaydi; onSec: () => void; }) { return ( diff --git a/src/features/katalog/components/universite-listesi.tsx b/src/features/katalog/components/universite-listesi.tsx index 750ae6c..1fd4459 100644 --- a/src/features/katalog/components/universite-listesi.tsx +++ b/src/features/katalog/components/universite-listesi.tsx @@ -36,13 +36,15 @@ function UniLinkleri({ uniler }: { uniler: UniOzet[] }) { export function UniversiteKatalogu() { const hepsi = getAllUniversiteler(); - const devlet = hepsi.filter((u) => u.unitur === "DEVLET"); - const vakif = hepsi.filter( - (u) => u.unitur != null && ["VAKIF", "VAKIF MYO"].includes(u.unitur), - ); - const digerUniler = hepsi.filter( - (u) => !devlet.includes(u) && !vakif.includes(u), - ); + // Tek geçişte üç grup — filter×3 + includes'lı O(n²) tarama yerine + const devlet: UniOzet[] = []; + const vakif: UniOzet[] = []; + const digerUniler: UniOzet[] = []; + for (const u of hepsi) { + if (u.unitur === "DEVLET") devlet.push(u); + else if (u.unitur === "VAKIF" || u.unitur === "VAKIF MYO") vakif.push(u); + else digerUniler.push(u); + } return ( <> diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index 78042ea..f8b93fa 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -13,7 +13,12 @@ import { Scale, ShieldCheck, } from "lucide-react"; -import type { DilimKey, Program, PuanTuruKey, RankResults } from "@/types/yokatlas"; +import type { + DilimKey, + ProgramListeSatiri, + PuanTuruKey, + RankResultsListe, +} from "@/types/yokatlas"; import { riskHesapla, type RiskSeviyesi } from "@/lib/risk"; import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug"; import { @@ -70,7 +75,7 @@ const RISK_NOKTA: Record = { riskli: "bg-red-500", }; -const BOS_EKSTRA: Record = { +const BOS_EKSTRA: Record = { hayal: [], dengeli: [], garanti: [], @@ -78,7 +83,7 @@ const BOS_EKSTRA: Record = { // Dengeli ve hayal dilimleri tamamen boşsa (sıralama çok gerideyse) boş bir // "dengeli" sekmesi yerine dolu olan "garanti" ile açılır. -function varsayilanDilim(sonuclar: RankResults): DilimKey { +function varsayilanDilim(sonuclar: RankResultsListe): DilimKey { return sonuclar.toplam.dengeli === 0 && sonuclar.toplam.hayal === 0 ? "garanti" : "dengeli"; @@ -89,7 +94,7 @@ export function ProgramTablosu({ adaySira, tur, }: { - sonuclar: RankResults; + sonuclar: RankResultsListe; adaySira: number; tur: PuanTuruKey; }) { @@ -120,7 +125,7 @@ export function ProgramTablosu({ `/api/programlar?sira=${adaySira}&tur=${tur}&dilim=${dilim}&offset=${offset}`, ); if (!res.ok) return; - const veri = (await res.json()) as { programlar: Program[] }; + const veri = (await res.json()) as { programlar: ProgramListeSatiri[] }; // Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir setEkstra((mevcut) => { const eldekiler = new Set( @@ -232,7 +237,7 @@ function ProgramSatiri({ adaySira, tur, }: { - program: Program; + program: ProgramListeSatiri; adaySira: number; tur: PuanTuruKey; }) { diff --git a/src/features/liste/liste-queries.ts b/src/features/liste/liste-queries.ts index 027f78d..597344b 100644 --- a/src/features/liste/liste-queries.ts +++ b/src/features/liste/liste-queries.ts @@ -1,12 +1,45 @@ import "server-only"; import { searchByRank } from "@/lib/db"; -import type { PuanTuruKey, RankResults } from "@/types/yokatlas"; +import type { + Program, + ProgramListeSatiri, + PuanTuruKey, + RankResultsListe, +} from "@/types/yokatlas"; + +// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin. +// Alanlar tek tek seçilir: Program'a alan eklenirse ProgramListeSatiri +// üzerinden derleyici burada da karar vermeye zorlar. +export function programOzetle(p: Program): ProgramListeSatiri { + return { + id: p.id, + isim: p.isim, + universite: p.universite, + unitur: p.unitur, + il: p.il, + tur: p.tur, + sure: p.sure, + sira2025: p.sira2025, + sira2024: p.sira2024, + sira2023: p.sira2023, + sira2022: p.sira2022, + sira2021: p.sira2021, + kontenjan2025: p.kontenjan2025, + yerlesen2025: p.yerlesen2025, + }; +} // /sonuc'un ham tablosu: yalnızca sıralamayla ulaşılabilen programlar, // hayal/dengeli/garanti dilimli, dilim başına ilk 20 satır. export function siraylaProgramAra( sira: number, tur: PuanTuruKey, -): RankResults { - return searchByRank(sira, tur, { limitPerBucket: 20 }); +): RankResultsListe { + const ham = searchByRank(sira, tur, { limitPerBucket: 20 }); + return { + hayal: ham.hayal.map(programOzetle), + dengeli: ham.dengeli.map(programOzetle), + garanti: ham.garanti.map(programOzetle), + toplam: ham.toplam, + }; } diff --git a/src/features/odeme/components/odeme-sonuc-karti.tsx b/src/features/odeme/components/odeme-sonuc-karti.tsx index 1abdd78..53f2eb3 100644 --- a/src/features/odeme/components/odeme-sonuc-karti.tsx +++ b/src/features/odeme/components/odeme-sonuc-karti.tsx @@ -14,9 +14,11 @@ export async function OdemeSonucKarti({ siparis }: { siparis?: string }) { const geriDonus = siparis ? `/odeme/sonuc?siparis=${encodeURIComponent(siparis)}` : "/paket"; - const session = await verifySession(geriDonus); - - const order = siparis ? await siparisGetir(siparis) : null; + // Oturum ve sipariş sorgusu bağımsız; sahiplik kontrolü aşağıda yapılıyor + const [session, order] = await Promise.all([ + verifySession(geriDonus), + siparis ? siparisGetir(siparis) : Promise.resolve(null), + ]); // Yalnızca kendi siparişi görüntülenebilir const sahibiMi = order?.userId === session.user.id; diff --git a/src/features/odeme/odeme-actions.ts b/src/features/odeme/odeme-actions.ts index 68d881f..1610684 100644 --- a/src/features/odeme/odeme-actions.ts +++ b/src/features/odeme/odeme-actions.ts @@ -14,11 +14,12 @@ export async function baslatOdeme( _prev: OdemeBaslatDurum, formData: FormData, ): Promise { - const session = await verifySession("/paket"); + // Ucuz senkron doğrulama pahalı oturum turundan önce const product = formData.get("urun"); if (product !== "paket" && product !== "topup") { return { error: "Geçersiz ürün." }; } + const session = await verifySession("/paket"); const urun = URUNLER[product]; const orderId = crypto.randomUUID(); diff --git a/src/features/rehber/rehber-queries.ts b/src/features/rehber/rehber-queries.ts index cf584fb..6d40285 100644 --- a/src/features/rehber/rehber-queries.ts +++ b/src/features/rehber/rehber-queries.ts @@ -7,5 +7,7 @@ export { getAllRehberler, getAllRehberSlugs, getRehber, + getRehberAramaKayitlari, type RehberOzet, + type RehberAramaKaydi, } from "@/lib/rehber"; diff --git a/src/features/sihirbaz/components/sira-gerekli.tsx b/src/features/sihirbaz/components/sira-gerekli.tsx index 10073ae..b4da9f5 100644 --- a/src/features/sihirbaz/components/sira-gerekli.tsx +++ b/src/features/sihirbaz/components/sira-gerekli.tsx @@ -3,7 +3,7 @@ // /sonuc?sira= yokken: kayıtlı profil varsa oraya taşı; yoksa kapıyı aç. // Sıra bilgisi localStorage'daki TercihProfili ile tüm uygulamada ortak. -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { ArrowLeft, Trophy } from "lucide-react"; @@ -18,17 +18,14 @@ import { export function SiraGerekli() { const router = useRouter(); const { profil, profilYuklendi } = useProfilKapisi(); - const [yonleniyor, setYonleniyor] = useState(false); + // "Yönleniyor" durumu ayrı state gerektirmez: profil varken aşağıdaki koşul + // zaten bekleme ekranını gösteriyor; effect yalnızca yönlendirmeyi yapar. useEffect(() => { - if (!profilYuklendi) return; - if (profil) { - setYonleniyor(true); - router.replace(sonucHref(profil)); - } + if (profilYuklendi && profil) router.replace(sonucHref(profil)); }, [profil, profilYuklendi, router]); - if (!profilYuklendi || yonleniyor || profil) { + if (!profilYuklendi || profil) { return (

Sıralaman kontrol ediliyor…

diff --git a/src/lib/odeme.ts b/src/lib/odeme.ts index 8601acc..8acb67b 100644 --- a/src/lib/odeme.ts +++ b/src/lib/odeme.ts @@ -1,4 +1,5 @@ import "server-only"; +import { after } from "next/server"; import { and, eq } from "drizzle-orm"; import { appDb, schema } from "./appdb"; import { grantCredits, URUNLER } from "./credits"; @@ -62,15 +63,19 @@ export async function odemeyiSonuclandir( // Dönüşüm kaydı. Koşullu geçişin ardında olduğu için sipariş başına tam bir // kez düşer (self-healing fallback tekrar çağrılsa da rowsAffected 0 döner). - await sunucuOlayi("odeme_tamamlandi", { - kullaniciId: order.userId, - pathname: "/odeme/sonuc", - ozellikler: { - urun: order.product, - kredi: order.credits, - tutar: order.amountKurus / 100, - }, - }); + // after(): yanıt gönderildikten sonra koşar — callback redirect'ini ve + // /odeme/sonuc render'ını Rybbit'in 3 sn'lik timeout'u bloklayamaz. + after(() => + sunucuOlayi("odeme_tamamlandi", { + kullaniciId: order.userId, + pathname: "/odeme/sonuc", + ozellikler: { + urun: order.product, + kredi: order.credits, + tutar: order.amountKurus / 100, + }, + }), + ); return "paid"; } diff --git a/src/lib/rehber.ts b/src/lib/rehber.ts index 6bcd6e5..de62431 100644 --- a/src/lib/rehber.ts +++ b/src/lib/rehber.ts @@ -25,6 +25,13 @@ export type RehberOzet = { tarih: string; }; +/** + * Header'daki ⌘K aramasına giden asgari alan seti. `tarih` client'ta hiç + * okunmadığı için RSC payload'ına taşınmaz (root layout üzerinden her + * sayfaya serileşiyordu). + */ +export type RehberAramaKaydi = Pick; + export type RehberSema = { /** Mermaid tanımı (aria/altyazi yönergeleri ayıklanmış hâli) */ tanim: string; @@ -40,6 +47,16 @@ export type RehberYazi = RehberOzet & { parcalar: RehberParca[] }; const REHBER_DIZIN = path.join(process.cwd(), "content", "rehber"); +// SiteHeader root layout'ta olduğu için getAllRehberler her sayfa +// render/prerender'ında koşuyor; cache'siz hâli sayfa başına 24 dosya okuma + +// 24 marked.parse demekti. Yalnızca prod'da cache'lenir — dev'de içerik +// düzenlenirken taze fs okuması korunur (katalog.ts/db.ts globalThis deseni). +const globalRehber = globalThis as unknown as { + rehberYazilar?: Map; + rehberOzetler?: RehberOzet[]; +}; +const CACHE_AKTIF = process.env.NODE_ENV === "production"; + function parseFrontmatter(raw: string): { meta: Record; body: string; @@ -105,21 +122,62 @@ function parcala(slug: string, body: string): RehberParca[] { return parcalar; } -function oku(slug: string): RehberYazi | null { - const dosya = path.join(REHBER_DIZIN, `${slug}.md`); - if (!fs.existsSync(dosya)) return null; - const { meta, body } = parseFrontmatter(fs.readFileSync(dosya, "utf8")); +function metaDogrula( + slug: string, + meta: Record, +): asserts meta is { baslik: string; aciklama: string; tarih: string } { if (!meta.baslik || !meta.aciklama || !meta.tarih) { throw new Error( `rehber: ${slug}.md frontmatter eksik (baslik/aciklama/tarih zorunlu)`, ); } +} + +function oku(slug: string): RehberYazi | null { + if (CACHE_AKTIF) { + const onbellek = globalRehber.rehberYazilar?.get(slug); + if (onbellek !== undefined) return onbellek; + } + const dosya = path.join(REHBER_DIZIN, `${slug}.md`); + let yazi: RehberYazi | null = null; + if (fs.existsSync(dosya)) { + const { meta, body } = parseFrontmatter(fs.readFileSync(dosya, "utf8")); + metaDogrula(slug, meta); + yazi = { + slug, + baslik: meta.baslik, + aciklama: meta.aciklama, + tarih: meta.tarih, + parcalar: parcala(slug, body), + }; + } + if (CACHE_AKTIF) { + (globalRehber.rehberYazilar ??= new Map()).set(slug, yazi); + } + return yazi; +} + +/** + * Liste görünümleri için fast-path: yalnızca frontmatter parse edilir, + * `parcala()`/`marked.parse` hiç çağrılmaz (özetler `parcalar`ı kullanmıyor). + * Tam parse zaten cache'teyse dosyaya inmeden onu kırpar. + */ +function ozetOku(slug: string): RehberOzet | null { + const tam = CACHE_AKTIF ? globalRehber.rehberYazilar?.get(slug) : undefined; + if (tam !== undefined) { + if (tam === null) return null; + const { slug: s, baslik, aciklama, tarih } = tam; + return { slug: s, baslik, aciklama, tarih }; + } + const dosya = path.join(REHBER_DIZIN, `${slug}.md`); + if (!fs.existsSync(dosya)) return null; + const { meta } = parseFrontmatter(fs.readFileSync(dosya, "utf8")); + metaDogrula(slug, meta); return { slug, baslik: meta.baslik, aciklama: meta.aciklama, tarih: meta.tarih, - parcalar: parcala(slug, body), }; } @@ -139,14 +197,22 @@ export function getRehber(slug: string): RehberYazi | null { } export function getAllRehberler(): RehberOzet[] { - return getAllRehberSlugs() - .map((slug) => oku(slug)) - .filter((y): y is RehberYazi => y !== null) - .map(({ slug, baslik, aciklama, tarih }) => ({ - slug, - baslik, - aciklama, - tarih, - })) + if (CACHE_AKTIF && globalRehber.rehberOzetler) { + return globalRehber.rehberOzetler; + } + const ozetler = getAllRehberSlugs() + .map((slug) => ozetOku(slug)) + .filter((y): y is RehberOzet => y !== null) .sort((a, b) => b.tarih.localeCompare(a.tarih)); + if (CACHE_AKTIF) globalRehber.rehberOzetler = ozetler; + return ozetler; +} + +/** Header aramasının RSC payload'ına giden kırpılmış liste. */ +export function getRehberAramaKayitlari(): RehberAramaKaydi[] { + return getAllRehberler().map(({ slug, baslik, aciklama }) => ({ + slug, + baslik, + aciklama, + })); } \ No newline at end of file diff --git a/src/types/yokatlas.ts b/src/types/yokatlas.ts index f069b14..55b1c9e 100644 --- a/src/types/yokatlas.ts +++ b/src/types/yokatlas.ts @@ -41,6 +41,20 @@ export type RankResults = { toplam: Record; }; +/** + * /sonuc ham tablosunun satırı: tablo `fakulte` ve `puan2025`'i hiç + * kullanmadığı için bu alanlar client'a serileştirilmez (satır başına en + * uzun string alanlardan biri fakulte). Katalog sayfaları tam Program kullanır. + */ +export type ProgramListeSatiri = Omit; + +export type RankResultsListe = { + hayal: ProgramListeSatiri[]; + dengeli: ProgramListeSatiri[]; + garanti: ProgramListeSatiri[]; + toplam: Record; +}; + // Vakıf sayılan unitur değerleri (bkz. DISTINCT unitur: VAKIF, VAKIF MYO, // YURTDISI VAKIF). Devlet dışı her şey "vakıf" grubuna girer. export type UniturGrubu = "devlet" | "vakif";