diff --git a/src/features/katalog/components/bolum-icerik.tsx b/src/features/katalog/components/bolum-icerik.tsx index d9b5bbf..6197998 100644 --- a/src/features/katalog/components/bolum-icerik.tsx +++ b/src/features/katalog/components/bolum-icerik.tsx @@ -14,7 +14,12 @@ import { sayfaMetadata, ustSegmentOgGorseli, } from "@/lib/seo"; -import { KATALOG_CTA_ACIKLAMA, SAYFA_BOYU, sayfaDilimi } from "./sayfalama"; +import { + KATALOG_CTA_ACIKLAMA, + SAYFA_BOYU, + sayfaDilimi, + siraYiliEtiketi, +} from "./sayfalama"; import { BolumProgramListesi } from "./bolum-program-listesi"; import { VeriKaynagi } from "./veri-kaynagi"; import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form"; @@ -60,16 +65,28 @@ function bolumSayfaDilimi(bolum: BolumDetay, sayfa: number) { let minSira: number | null = null; let maxSira: number | null = null; let verisiz = 0; + // Sırası 2025'te olmayıp 2024'ten gelen satırlar: aralık metni yılı dürüstçe + // söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir). + let yalniz2024 = 0; for (const p of dilim) { const efektif = p.sira2025 ?? p.sira2024; if (efektif == null) { verisiz += 1; continue; } + if (p.sira2025 == null) yalniz2024 += 1; if (minSira == null || efektif < minSira) minSira = efektif; if (maxSira == null || efektif > maxSira) maxSira = efektif; } - return { tumProgramlar, dilim, minSira, maxSira, verisiz }; + return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalniz2024 }; +} + +/** için kısa sıra: 2.425.061 → "2,43 mn", 95.400 → "95 bin". */ +function kisaSira(n: number): string { + if (n >= 1_000_000) { + return `${(n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 })} mn`; + } + return n >= 10_000 ? `${Math.round(n / 1000)} bin` : sayi(n); } /** @@ -82,15 +99,17 @@ function siraAraligiMetni({ minSira, maxSira, verisiz, + yalniz2024, }: ReturnType<typeof bolumSayfaDilimi>): string | null { if (dilim.length === 0) return null; if (minSira == null || maxSira == null) { return "taban verisi olmayan programlar"; } + const yil = siraYiliEtiketi(yalniz2024, dilim.length - verisiz); const aralik = minSira === maxSira - ? `taban sırası ${sayi(minSira)} olan programlar` - : `taban sırası ${sayi(minSira)}–${sayi(maxSira)} arası programlar`; + ? `${yil} taban sırası ${sayi(minSira)} olan programlar` + : `${yil} taban sırası ${sayi(minSira)}–${sayi(maxSira)} arası programlar`; return verisiz > 0 ? `${aralik} ile taban verisi olmayanlar` : aralik; } @@ -102,17 +121,20 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata { const sayfaDilim = bolumSayfaDilimi(bolum, sayfa); const aralik = siraAraligiMetni(sayfaDilim); const { minSira, maxSira } = sayfaDilim; - // <title> kısa kalsın: aralığın yalnız rakamları; açıklama tam cümleyi taşır. + // <title> kısa kalsın (arama sonucunda ayırt edici kısım kesilmesin): + // yuvarlanmış rakamlar, kök şablonun " — KolayTercih" eki yok; açıklama tam + // cümleyi taşır. "s. N" yuvarlamada çakışan iki sayfayı yine ayırır. const baslikEki = minSira != null && maxSira != null ? minSira === maxSira - ? `Sıra ${sayi(minSira)}, Sayfa ${sayfa}` - : `Sıra ${sayi(minSira)}–${sayi(maxSira)}, Sayfa ${sayfa}` + ? `Sıra ${kisaSira(minSira)}, s. ${sayfa}` + : `Sıra ${kisaSira(minSira)}–${kisaSira(maxSira)}, s. ${sayfa}` : `Sayfa ${sayfa}`; - return sayfaMetadata({ - title: `${bolum.ad} Taban Puanları 2026 (${baslikEki})`, + const baslik = `${bolum.ad} Taban Puanları (${baslikEki})`; + const meta = sayfaMetadata({ + title: baslik, description: aralik - ? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın 2025 yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.` + ? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (2025; 2025'te verisi olmayanlarda 2024) — taban puanı, başarı sıralaması ve kontenjan.` : `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`, path: `/bolum/${slug}/sayfa/${sayfa}`, openGraph: { @@ -122,6 +144,7 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata { ), }, }); + return { ...meta, title: { absolute: baslik } }; } const enIyi = stats.minSira != null diff --git a/src/features/katalog/components/sayfalama.tsx b/src/features/katalog/components/sayfalama.tsx index f86685f..a7e56d3 100644 --- a/src/features/katalog/components/sayfalama.tsx +++ b/src/features/katalog/components/sayfalama.tsx @@ -13,6 +13,12 @@ export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] { return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU); } +/** Aralıktaki sıraların hangi yıldan geldiği: hepsi 2025 / hepsi 2024 / karışık. */ +export function siraYiliEtiketi(yalniz2024: number, siraliSayi: number): string { + if (yalniz2024 === 0) return "2025"; + return yalniz2024 === siraliSayi ? "2024" : "2024–2025"; +} + /** * Üniversite ve bölüm tablo sayfalarının altındaki CtaSiraForm kartının köprü * cümlesi — iki sayfa birebir aynı metni kullansın diye tek kaynak. Sunucu diff --git a/src/features/katalog/components/universite-icerik.tsx b/src/features/katalog/components/universite-icerik.tsx index 207f9b7..50821ab 100644 --- a/src/features/katalog/components/universite-icerik.tsx +++ b/src/features/katalog/components/universite-icerik.tsx @@ -23,6 +23,7 @@ import { Sayfalama, SAYFA_BOYU, sayfaDilimi, + siraYiliEtiketi, } from "./sayfalama"; import { PagePixelDivider } from "@/components/pixel-decor"; import { UniversiteKonumHaritasi } from "./universite-konum-haritasi"; @@ -95,9 +96,13 @@ function fakulteOzeti({ if (siralar.length === 0) return adlar[0]; const min = Math.min(...siralar); const max = Math.max(...siralar); + const yil = siraYiliEtiketi( + dilim.filter((p) => p.sira2025 == null && p.sira2024 != null).length, + siralar.length, + ); return min === max - ? `${adlar[0]} (taban sırası ${sayi(min)})` - : `${adlar[0]} (taban sırası ${sayi(min)}–${sayi(max)} arası)`; + ? `${adlar[0]} (${yil} taban sırası ${sayi(min)})` + : `${adlar[0]} (${yil} taban sırası ${sayi(min)}–${sayi(max)} arası)`; } if (adlar.length === 2) return `${adlar[0]} ve ${adlar[1]}`; return `${adlar[0]}, ${adlar[1]} ve ${adlar.length - 2} birim daha`; diff --git a/src/features/kullanici/components/giris-form.tsx b/src/features/kullanici/components/giris-form.tsx index 6b51fd0..3ec62e1 100644 --- a/src/features/kullanici/components/giris-form.tsx +++ b/src/features/kullanici/components/giris-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Mail } from "lucide-react"; import { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; @@ -40,18 +40,28 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) { // 60 sn: sunucunun adres başına gönderim soğumasıyla hizalı; daha kısa // sayaçta "Yeniden gönder" sessizce yutulur, kullanıcı gelmeyen maili bekler. + // Tek sayaç: "E-postayı düzelt" → yeniden gönderimde eski interval durmazsa + // iki sayaç birlikte sayar, buton sunucu soğuması bitmeden açılır. + const sayacRef = useRef<ReturnType<typeof setInterval> | null>(null); function geriSayimBaslat() { + if (sayacRef.current) clearInterval(sayacRef.current); setBekleme(60); - const sayac = setInterval(() => { + sayacRef.current = setInterval(() => { setBekleme((s) => { if (s <= 1) { - clearInterval(sayac); + if (sayacRef.current) clearInterval(sayacRef.current); + sayacRef.current = null; return 0; } return s - 1; }); }, 1000); } + useEffect(() => { + return () => { + if (sayacRef.current) clearInterval(sayacRef.current); + }; + }, []); async function googleIleGiris() { setBusy("google"); diff --git a/src/lib/analitik-sunucu.ts b/src/lib/analitik-sunucu.ts index 3e2b8cf..be212a2 100644 --- a/src/lib/analitik-sunucu.ts +++ b/src/lib/analitik-sunucu.ts @@ -10,7 +10,9 @@ import "server-only"; * Env yoksa (dev, self-host anahtarı tanımsız) hiçbir istek yapılmaz. */ -const HOST = process.env.RYBBIT_HOST; +// Sondaki "/" atılır: secret "https://…/" girilirse `//api/track` 404 olup +// olaylar sessizce kaybolmasın. +const HOST = process.env.RYBBIT_HOST?.replace(/\/+$/, ""); const SITE_ID = process.env.RYBBIT_SITE_ID; const API_KEY = process.env.RYBBIT_API_KEY;