feat(tasarim): üniversite sayfası "şişko ve sade" dile göre baştan, DESIGN.md know-how'ı

Üniversite sayfası (canlı referans ekran /universite/ankara-universitesi):
- Program listesi: ad kesilmez/sarar, telefonda taban sıra alt satıra iner,
  satır butonları 44 px, tür çipi okunur boyda
- Başlık bloğu lg altında ortalı (logo başlığın üstünde), kimlik hapları
  (Devlet/Ankara) navbar ölçüsünde, ikon + metin
- Harita yanında 4 özet kartı (OzetKart): sayı solda iri, ikon sağda;
  4. kart URAP Türkiye sırası + piksel sıra şeridi (PixelSira)
- Akademik göstergeler (URAP + YÖK İstatistik) sayfadan "Akademik
  göstergeler ›" satırının açtığı alt sayfaya taşındı: kartsız, ortalı,
  masonry gösterge kareleri, tek ekrana sığar
- Hover tek turuncu (orange-500); liste satırı adı mavi kalır
- Üst çubuk başlık geçişi (NavBaslik): h1 kaydırılınca üst çubukta logo +
  ad belirir; bölüm sayfasında da (yalnız ad)

Tasarım dili:
- DESIGN.md: temel his (§1.1), hover kuralı (§2.1), yazı alt sınırları,
  yeni tek kaynak parçalar (§4.8–4.15), lg altı ortala (§5.2), reddedilen
  denemeler (§7), ölç-tahmin-etme ve tuzaklar (§9)
- Denetim: gösterge karesi renk istisnası (bg-indigo-500, bg-green-600) ve
  site taraması için dört uyarı (minik-yazi, buyuk-harf-etiket, mavi-hover,
  iri-sayi-tabular)

Not: universite-icerik.tsx'teki akademik göstergeler bölümü (URAP/YÖK
kartları) Bilal'in commit'lenmemiş işiydi; tasarım değişiklikleriyle aynı
dosyada iç içe olduğu için bu commit'e girdi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 00:01:41 +03:00
parent 5df0af0d38
commit 6a9af19ef2
11 changed files with 1092 additions and 202 deletions

View File

@@ -0,0 +1,72 @@
"use client";
import type { ReactNode } from "react";
import { ChevronRight, GraduationCap, X } from "lucide-react";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
/**
* Üniversite sayfasında özet kartlarının altındaki "Akademik göstergeler ›"
* satırı (Apple Sağlık'taki "Tüm verileri göster" gibi): sık bakılan sayılar
* kartlarda, ayrıntı bir dokunuş derinde. İkinci satır içeride ne olduğunu
* önceden söyler. Ayrıntı alttan açılan çekmecede; çekmece ve kapatma
* çarpısı cta-sira-form.tsx'teki çekmeceyle birebir aynı sınıf setinde.
* Kartlar sunucuda üretilip `children` olarak gelir.
*/
export function AkademikAyrinti({
universite,
onizleme,
children,
}: {
universite: string;
onizleme: string;
children: ReactNode;
}) {
return (
<Drawer handleOnly>
<DrawerTrigger asChild>
<button
type="button"
className="mt-4 flex w-full cursor-pointer items-center gap-4 group rounded-2xl border border-slate-200 bg-white p-4 text-left transition-[background-color,border-color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:p-5"
>
<span className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary/10">
<GraduationCap className="size-5 text-primary" aria-hidden />
</span>
<span className="min-w-0 flex-1">
<span className="block font-heading text-lg font-bold text-slate-900">
Akademik göstergeler
</span>
<span className="block truncate text-sm text-slate-500">
{onizleme}
</span>
</span>
<ChevronRight
className="size-5 shrink-0 text-slate-400 transition-[color,transform] duration-150 group-hover:translate-x-0.5 group-hover:text-orange-500"
aria-hidden
/>
</button>
</DrawerTrigger>
<DrawerContent className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl">
<DrawerClose className="absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]">
<X className="size-4" aria-hidden />
<span className="sr-only">Kapat</span>
</DrawerClose>
<div className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6">
<DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
Akademik göstergeler
</DrawerTitle>
<DrawerDescription className="mt-1 px-10 text-center text-sm text-slate-500">
{universite}
</DrawerDescription>
<div className="mt-5">{children}</div>
</div>
</DrawerContent>
</Drawer>
);
}

View File

@@ -22,6 +22,7 @@ import {
} from "./sayfalama";
import { BolumProgramListesi } from "./bolum-program-listesi";
import { VeriKaynagi } from "./veri-kaynagi";
import { NavBaslik } from "./nav-baslik";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
import { Skeleton } from "@/components/ui/skeleton";
@@ -224,8 +225,12 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
)}
</nav>
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir */}
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
<header className="border-b pb-7 sm:pb-9">
<h1 className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
<h1
id="bolum-baslik"
className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
{bolum.ad}
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
{!ilkSayfa && aralik ? (

View File

@@ -0,0 +1,116 @@
"use client";
import {
type ReactNode,
useEffect,
useState,
useSyncExternalStore,
} from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
/** SiteHeader'daki yuva; logo yazısının hemen sağında. */
export const NAV_BASLIK_YUVASI = "nav-baslik-yuvasi";
const subscribe = () => () => {};
/**
* iOS'taki büyük başlık davranışı: sayfa başlığı (h1) üst çubuğun altına
* girince üst çubukta "KolayTercih" yazısı yukarı kayıp söner (SiteHeader'da
* `in-data-[nav-baslik]` ile), yerine üniversitenin logosu ve adı gelir.
* Dokununca sayfanın başına döner. Tetik IntersectionObserver'la: kaydırma
* dinleyicisi yok, üst sınır üst çubuğun alt kenarı.
*
* Hareket Apple'ın varsayılanı: sekmesiz yay (bounce 0, 0,35 sn), giriş ve
* çıkış aynı yoldan. "Hareketi azalt" açıksa yalnız opaklık değişir.
* Logo sunucuda üretilip `logo` olarak gelir (logo listesi istemciye inmez);
* logosu olmayan sayfada verilmez, yalnız ad görünür.
*/
export function NavBaslik({
baslikId,
ad,
logo,
}: {
baslikId: string;
ad: string;
/** Verilmezse yalnız ad görünür (ör. bölüm sayfası). */
logo?: ReactNode;
}) {
const [goster, setGoster] = useState(false);
const azHareket = useReducedMotion();
const yuva = useSyncExternalStore(
subscribe,
() => document.getElementById(NAV_BASLIK_YUVASI),
() => null,
);
useEffect(() => {
const baslik = document.getElementById(baslikId);
// Sayfada başka <header> olabilir (bölüm sayfası): yuvanın kendi
// üst çubuğu alınır.
const ustCubuk = document
.getElementById(NAV_BASLIK_YUVASI)
?.closest("header");
if (!baslik || !ustCubuk) return;
let gozlemci: IntersectionObserver | null = null;
const kur = () => {
gozlemci?.disconnect();
const alt = Math.round(ustCubuk.getBoundingClientRect().bottom);
gozlemci = new IntersectionObserver(
([giris]) => {
// Görünmüyor ve üst çubuğun üstünde kaldıysa (aşağıda değil)
setGoster(
!giris.isIntersecting && giris.boundingClientRect.top < alt,
);
},
{ rootMargin: `-${alt}px 0px 0px 0px` },
);
gozlemci.observe(baslik);
};
kur();
window.addEventListener("resize", kur);
return () => {
window.removeEventListener("resize", kur);
gozlemci?.disconnect();
};
}, [baslikId]);
useEffect(() => {
const kok = document.documentElement;
if (goster) kok.dataset.navBaslik = "";
else delete kok.dataset.navBaslik;
return () => {
delete kok.dataset.navBaslik;
};
}, [goster]);
if (!yuva) return null;
const kayma = azHareket ? 0 : 10;
return createPortal(
<AnimatePresence>
{goster ? (
<motion.button
key="nav-baslik"
type="button"
onClick={() =>
window.scrollTo({ top: 0, behavior: azHareket ? "auto" : "smooth" })
}
aria-label={`${ad} — sayfanın başına dön`}
initial={{ opacity: 0, y: kayma }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: kayma }}
transition={{ type: "spring", bounce: 0, duration: 0.35 }}
className="pointer-events-auto flex min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-full py-1 pr-2 pl-1 transition-colors duration-150 hover:text-orange-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]"
>
{logo ?? null}
<span className="truncate font-bricolage text-base font-semibold tracking-tight sm:text-xl">
{ad}
</span>
</motion.button>
) : null}
</AnimatePresence>,
yuva,
);
}

View File

@@ -1,7 +1,19 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ChevronRight } from "lucide-react";
import {
ArrowRight,
Building2,
ChevronRight,
Landmark,
LayoutList,
Medal,
MapPin,
Target,
Users,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import {
getAllUniversiteler,
getAllUniversiteSlugs,
@@ -26,17 +38,23 @@ import {
sayfaDilimi,
siraYiliEtiketi,
} from "./sayfalama";
import { PagePixelDivider } from "@/components/pixel-decor";
import { PagePixelDivider, PixelSira } from "@/components/pixel-decor";
import { Button } from "@/components/ui/button";
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
import { VeriKaynagi } from "./veri-kaynagi";
import { URAP_DONEM, URAP_KAYNAK_URL, URAP_TOPLAM } from "@/lib/urap-siralama";
import {
URAP_DONEM,
URAP_KAYNAK_URL,
URAP_TOPLAM,
} from "@/lib/urap-siralama";
YOK_ISTATISTIK_DONEM,
YOK_ISTATISTIK_KAYNAK_URL,
OGRENCI_BASINA_UYE_MEDYAN,
ogrenciBasinaUye,
} from "@/lib/yok-istatistik";
import { UniLogo } from "@/components/uni-logo";
import { UrapLogo } from "./urap-logo";
import { AkademikAyrinti } from "./akademik-ayrinti";
import { NavBaslik } from "./nav-baslik";
import { uniLogoYolu } from "@/lib/uni-logolar";
import { Skeleton } from "@/components/ui/skeleton";
import type { Program } from "@/types/yokatlas";
@@ -124,8 +142,8 @@ function fakulteOzeti({
* Atıf pazarlık dışı: URAP'ın izni "urapcenter.org adresine aktif bağlantı"
* şartına bağlı, o yüzden gerçek <a href>, yeni sekme, rel="noopener" ve
* nofollow YOK. Sıra payda ("198 üniversite arasında") ve dönem olmadan
* gösterilmez. URAP listesinde olmayan üniversitede sayı yoktur; eksiklik
* gizlenmez, nedeni tek cümleyle yazılır.
* gösterilmez. Sıra ve atıf artık AkademikGostergeler kartında; bu satır
* yalnız URAP listesinde olmayan üniversitede nedeni tek cümleyle yazar.
*/
function UrapKaynakSatiri({ uni }: { uni: UniDetay }) {
if (!uni.urap) {
@@ -139,18 +157,288 @@ function UrapKaynakSatiri({ uni }: { uni: UniDetay }) {
</p>
);
}
// Sıra ve zorunlu atıf AkademikGostergeler kartında.
return null;
}
/**
* Akademik göstergeler: URAP sırası + YÖK İstatistik öğrenci/öğretim üyesi.
* Sayfada değil, özet kartlarının altındaki "Akademik göstergeler ›"
* satırının açtığı alt sayfada (AkademikAyrinti) durur; sıra sayısı ayrıca
* dördüncü özet kartında.
*
* Apple ayrıntı sayfası düzeni (Hava Durumu gibi): kart ve süs ayraç yok,
* her şey ortalı; iki dev sayı yan yana ince çizgiyle ayrılır, altında ham
* sayılar, tek satır not, buton ve en altta ince yazıyla kaynaklar. Alt
* sayfa ekranı doldurmasın diye dikey değil yan yana dizilir.
*
* URAP: sıra payda ve dönem olmadan gösterilmez; sıranın yeri piksel
* şeridinde (PixelSira) turuncu yanar. Atıf pazarlık dışı — URAP'ın izni
* urapcenter.org'a aktif bağlantı şartına bağlı: gerçek <a href>, yeni
* sekme, rel="noopener", nofollow YOK. Oran kıyası sayfası olan
* üniversitelerin medyanıyla yapılır; renk kullanılmaz.
*/
function AkademikGostergeler({ uni }: { uni: UniDetay }) {
const { urap, istatistik: i } = uni;
const oran = i ? ogrenciBasinaUye(i) : null;
const oranVar = i != null && oran != null;
if (!urap && !oranVar) return null;
const dilim = urap ? Math.ceil((urap.sira / URAP_TOPLAM) * 100) : 0;
const kaynakLinki =
"font-medium text-primary transition-colors hover:text-orange-500 hover:underline";
// Dört gösterge dört canlı karede, logodaki kareler gibi yerleşir: iki
// sütun, sağdaki aşağı kaymış, kare boyları farklı — ne tepeler ne
// bitişler hizalı (masonry). Renk sempati katar, anlamı etiket taşır;
// yazı beyaz (turuncu/mavi zeminde sitenin CTA'larıyla aynı ikili).
const kare =
"flex flex-col items-center justify-center rounded-3xl px-3 text-white";
const etiket = "text-sm font-bold text-white/90";
const devSayi =
"font-heading text-5xl font-bold leading-none tracking-tight";
const orta = "font-heading text-3xl font-bold leading-none";
const siraKaresi = urap ? (
<div className={`${kare} min-h-40 bg-orange-500 py-4`}>
<dt className={etiket}>Türkiye sırası</dt>
<dd className="mt-3 flex items-baseline justify-center gap-1">
<span className={devSayi}>{sayi(urap.sira)}.</span>
<span className="text-base font-bold text-white/80">
/ {sayi(URAP_TOPLAM)}
</span>
</dd>
<dd className="mt-2 text-base font-bold">
{dilim <= 50 ? `İlk %${dilim}` : `${URAP_DONEM} listesi`}
</dd>
<dd className="mt-3 text-white">
<PixelSira
sira={urap.sira}
toplam={URAP_TOPLAM}
hucre={10}
zemin="koyu"
className="h-3"
/>
</dd>
</div>
) : null;
const ogrenciKaresi = oranVar ? (
<div className={`${kare} min-h-24 bg-indigo-500 py-3`}>
<dd className={orta}>{sayi(i.ogrenci)}</dd>
<dt className={`mt-1.5 ${etiket}`}>Örgün öğrenci</dt>
</div>
) : null;
const oranKaresi = oranVar ? (
<div className={`${kare} min-h-32 bg-primary py-4`}>
<dt className={etiket}>Hoca başına öğrenci</dt>
<dd className={`mt-3 ${devSayi}`}>{sayi(oran)}</dd>
<dd className="mt-2 text-base font-bold text-white/90">
Türkiye: {sayi(OGRENCI_BASINA_UYE_MEDYAN)}
</dd>
</div>
) : null;
const uyeKaresi = oranVar ? (
<div className={`${kare} bg-green-600 py-5`}>
<dd className={orta}>{sayi(i.ogretimUyesi)}</dd>
<dt className={`mt-1.5 ${etiket}`}>Öğretim üyesi</dt>
</div>
) : null;
const ikiSutun = urap != null && oranVar;
return (
<div className="flex flex-col items-center text-center">
<dl className="flex w-full max-w-md items-start gap-3">
<div className="flex flex-1 flex-col gap-3">
{siraKaresi}
{ogrenciKaresi}
</div>
{ikiSutun || (!urap && oranVar) ? (
<div className={`flex flex-1 flex-col gap-3 ${ikiSutun ? "mt-12" : ""}`}>
{oranKaresi}
{uyeKaresi}
</div>
) : null}
</dl>
{urap ? (
<>
<p className="mt-4 max-w-md text-sm leading-relaxed text-slate-500">
Türkiye sırası araştırma performansını ölçer; eğitim kalitesini
ya da iş bulma oranını ölçmez.
</p>
<Button
asChild
size="lg"
variant="outline"
className="mt-4 cursor-pointer px-6 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
>
<Link href="/universite-siralamasi">
Tüm sıralamayı gör
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
</>
) : null}
<div className="mt-5 flex w-full max-w-md items-center justify-center gap-3 border-t border-slate-100 pt-4 text-left text-xs leading-5 text-slate-500">
{urap ? <UrapLogo className="h-7 shrink-0" /> : null}
<div>
{urap ? (
<p>
URAP {URAP_DONEM} ·{" "}
<a
href={URAP_KAYNAK_URL}
target="_blank"
rel="noopener"
className={kaynakLinki}
>
urapcenter.org
</a>
</p>
) : null}
{i ? (
<p>
YÖK {YOK_ISTATISTIK_DONEM} ·{" "}
<a
href={YOK_ISTATISTIK_KAYNAK_URL}
target="_blank"
rel="noopener"
className={kaynakLinki}
>
istatistik.yok.gov.tr
</a>
</p>
) : null}
</div>
</div>
</div>
);
}
/**
* Özet kartı (Apple Fitness özet kartlarının düzeni, sitenin ölçüleriyle):
* kartın en baskın öğesi iri sayı; ikonu sağında, küçük ikon kutusunda
* (DESIGN.md §4.4 ikon kutusunun küçük hali). Altında başlık ve tek satır
* açıklama. Telefonda kart iç genişliği ~131 px: en uzun sayı ("13.889",
* text-4xl ≈ 90 px) + size-8 ikon kutusu sığar; sm'den itibaren text-5xl.
* Harita yanındaki dört hücrenin tek kaynağı. `ek` verilirse açıklamanın
* altında durur (ör. Türkiye sırasındaki piksel şeridi). dl anlamı için dt
* DOM'da önce gelir, görsel sıra `order` ile kurulur.
*/
function OzetKart({
ikon: Ikon,
etiket,
deger,
birim,
alt,
ek,
className,
}: {
ikon: LucideIcon;
etiket: string;
deger: ReactNode;
birim?: string;
alt: ReactNode;
ek?: ReactNode;
className?: string;
}) {
return (
<div
className={`flex flex-col rounded-2xl border border-slate-200 bg-white p-4 sm:p-5 ${className ?? ""}`}
>
<dt className="order-2 mt-2 text-base font-semibold text-slate-900">
{etiket}
</dt>
<dd className="order-1 flex items-start justify-between gap-1.5">
<span className="flex min-w-0 flex-wrap items-baseline gap-x-1">
<span className="font-heading text-4xl font-bold leading-none tracking-tight text-slate-900 sm:text-5xl">
{deger}
</span>
{birim ? (
<span className="text-base font-semibold text-slate-400">
{birim}
</span>
) : null}
</span>
<span className="flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-10">
<Ikon className="size-4 text-primary sm:size-5" aria-hidden />
</span>
</dd>
<dd className="order-3 mt-0.5 text-sm text-slate-500">{alt}</dd>
{ek ? <dd className="order-4 mt-3">{ek}</dd> : null}
</div>
);
}
/**
* Dördüncü özet kartı: URAP Türkiye sırası. Açıklamanın altında sıranın
* yerini gösteren piksel şeridi (PixelSira) durur. Kartın tamamı sıralama
* sayfasına gider (after: ile gerilmiş link); hover kenarlığı bunu belli
* eder. Atıf pazarlık dışı: sıra payda ve dönem olmadan gösterilmez;
* urapcenter.org'a aktif bağlantı (yeni sekme, rel="noopener", nofollow YOK)
* z-10 ile kart linkinin üstünde kalır.
*/
function TurkiyeSirasiKarti({ sira }: { sira: number }) {
const dilim = Math.ceil((sira / URAP_TOPLAM) * 100);
return (
<OzetKart
className="group relative transition-[background-color,border-color] duration-150 hover:border-orange-500 hover:bg-orange-50/60"
ikon={Medal}
ek={<PixelSira sira={sira} toplam={URAP_TOPLAM} className="h-3" />}
etiket="Türkiye sırası"
deger={
<Link
href="/universite-siralamasi"
aria-label={`Türkiye sıralamasında ${sayi(sira)}. — tüm üniversite sıralamasını gör`}
className="transition-colors after:absolute after:inset-0 after:rounded-2xl group-hover:text-orange-500 focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-primary"
>
{sayi(sira)}.
</Link>
}
birim={`/ ${sayi(URAP_TOPLAM)}`}
alt={
<>
{dilim <= 50 ? (
<span className="block font-semibold text-slate-700">
İlk %{dilim}
</span>
) : null}
<a
href={URAP_KAYNAK_URL}
target="_blank"
rel="noopener"
className="relative z-10 font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
>
URAP {URAP_DONEM}
</a>
</>
}
/>
);
}
/** "Akademik göstergeler ›" satırının ikinci satırı: içeride ne var. */
function akademikOnizleme(uni: UniDetay): string {
const oran = uni.istatistik ? ogrenciBasinaUye(uni.istatistik) : null;
if (oran != null) return `Hoca başına ${sayi(oran)} öğrenci`;
return "Türkiye sıralaması ayrıntısı";
}
/** YÖK İstatistik'in ayrı kaynak satırı — dönemi YÖK Atlas'tan ve URAP'tan farklı. */
function IstatistikKaynakSatiri({ uni }: { uni: UniDetay }) {
if (!uni.istatistik) return null;
return (
<p className="mt-2 text-xs leading-5 text-slate-500">
URAP Türkiye Sıralaması {URAP_DONEM} ({sayi(URAP_TOPLAM)} üniversite
arasında) — akademik yayın ve araştırma performansını ölçer; eğitim
kalitesini ya da iş bulma oranını ölçmez. Kaynak:{" "}
Öğrenci ve öğretim üyesi sayıları: YÖK Yükseköğretim İstatistikleri{" "}
{YOK_ISTATISTIK_DONEM}. Örgün öğrenci birinci ve ikinci öğretimdeki tüm
düzeyleri (ön lisans–doktora) kapsar, uzaktan ve açık öğretim dahil
değildir; öğretim üyesi profesör, doçent ve doktor öğretim üyesidir.
Kaynak:{" "}
<a
href={URAP_KAYNAK_URL}
href={YOK_ISTATISTIK_KAYNAK_URL}
target="_blank"
rel="noopener"
className="font-medium text-primary hover:underline"
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
>
urapcenter.org
istatistik.yok.gov.tr
</a>
.
</p>
@@ -231,7 +519,7 @@ export function UniversiteIcerik({
// özet paragrafının hemen altında, tablonun (ve ilk sayfadaki istatistik
// ızgarasının) üstünde durur. Alttaki kart yerinde kalır.
const ustSiraSatiri = (
<div className="mt-6">
<div className="mt-8">
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
</div>
);
@@ -276,71 +564,88 @@ export function UniversiteIcerik({
<nav
aria-label="breadcrumb"
className="flex items-center gap-1 text-xs text-slate-500"
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
>
<Link href="/" className="hover:text-primary hover:underline">
<Link
href="/"
className="transition-colors hover:text-orange-500 hover:underline"
>
Ana Sayfa
</Link>
<ChevronRight className="size-3" aria-hidden />
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
<Link
href="/universiteler"
className="hover:text-primary hover:underline"
className="transition-colors hover:text-orange-500 hover:underline"
>
Üniversiteler
</Link>
<ChevronRight className="size-3" aria-hidden />
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
{ilkSayfa ? (
<span className="text-slate-700">{uni.ad}</span>
) : (
<>
<Link
href={`/universite/${slug}`}
className="hover:text-primary hover:underline"
className="transition-colors hover:text-orange-500 hover:underline"
>
{uni.ad}
</Link>
<ChevronRight className="size-3" aria-hidden />
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
<span className="text-slate-700">Sayfa {sayfa}</span>
</>
)}
</nav>
<div className="mt-4 flex items-center gap-4">
{/* Başlık üst çubuğun altına girince üst çubukta logo + ad belirir */}
<NavBaslik
baslikId="uni-baslik"
ad={uni.ad}
logo={<UniLogo slug={slug} ad={uni.ad} boy="xs" />}
/>
{/* Geniş ekran (lg) altında başlık bloğu ortalı: logo başlığın
üstünde, yol satırı, çipler ve özet paragraf ortada. */}
<div className="mt-6 flex flex-col items-center gap-4 text-center lg:flex-row lg:text-left">
<UniLogo slug={slug} ad={uni.ad} boy="lg" />
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
<h1
id="uni-baslik"
className="font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl"
>
{uni.ad} Taban Puanları {BASLIK_YILI}
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
</h1>
</div>
{/* Kurumun kimliği (tür + il): ızgaradaki kartlar sayısal ve
karşılaştırılabilir metriklerdir, tür ise kategoridir — sitenin
geri kalanında da başlığın altında çip olarak duruyor. Çipin sınıf
seti program-tablosu.tsx:329-333'ten birebir kopyadır (AGENTS.md
"ortak elemanlar tek kaynaktan"); yeni varyant üretilmez.
Sayfalama sayfalarında da yazar: ziyaretçi Google'dan doğrudan
/sayfa/N'ye inebiliyor, kimlik orada da aynı. `unitur` NULL ise
satırın tamamı düşer, kopuk boşluk kalmaz. */}
{turEtiketi ? (
<p className="mt-3 flex min-w-0 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 ${
uniturDevletMi(uni.unitur)
? "bg-slate-100 text-slate-600"
: "bg-violet-50 text-violet-600"
}`}
>
{turEtiketi.cip}
</span>
karşılaştırılabilir metriklerdir, tür ise kategoridir — başlığın
altında iki iri hap olarak durur (navbar'daki hap butonların
ölçüsü: h-10, tam yuvarlak, ikon + tek kelime). Tür renkle değil
ikon + metinle ayrılır. Sayfalama sayfalarında da yazar:
ziyaretçi Google'dan doğrudan /sayfa/N'ye inebiliyor. `unitur`
NULL ise tür hapı düşer, kopuk boşluk kalmaz. */}
{turEtiketi || uni.il ? (
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
{turEtiketi ? (
<span className="inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800">
{uniturDevletMi(uni.unitur) ? (
<Landmark className="size-4 text-primary" aria-hidden />
) : (
<Building2 className="size-4 text-primary" aria-hidden />
)}
{turEtiketi.cip}
</span>
) : null}
{uni.il ? (
<span className="truncate">{trBaslikDuzeni(uni.il)}</span>
<span className="inline-flex h-10 min-w-0 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800">
<MapPin className="size-4 shrink-0 text-primary" aria-hidden />
<span className="truncate">{trBaslikDuzeni(uni.il)}</span>
</span>
) : null}
</p>
) : null}
{ilkSayfa ? (
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
{uni.ad}
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}
{turCumlesi ? `, ${turCumlesi};` : ","} {SON_YIL} YKS yerleştirme
@@ -370,82 +675,68 @@ export function UniversiteIcerik({
{ustSiraSatiri}
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
<UniversiteKonumHaritasi universite={uni.ad} il={uni.il} />
{/* Şablon tam 4 hücre bırakıyor: masaüstünde harita 3. sütunu
iki satır boyunca kaplıyor, 5. kart ızgarayı bozar. Fakülte
sayısı Program kartının alt satırında; boşalan hücrede
Türkiye sırası. Geri kalan akademik ayrıntı alttaki
"Akademik göstergeler ›" penceresinde. */}
<dl className="contents">
<div className="rounded-xl border border-slate-200 bg-white p-4">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
Program
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{sayi(uni.programSayisi)}
</dd>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-4">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
Fakülte / Birim
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{sayi(uni.fakulteSayisi)}
</dd>
</div>
{/* Şablon tam 4 hücre bırakıyor: masaüstünde harita 3. sütunu
iki satır boyunca kaplıyor (lg:grid-rows-2 + row-span-2), 5.
kart ızgarayı bozar. Bu yüzden 4. hücre tek: URAP'ta olan
üniversitede URAP sırası, olmayan 31 sayfada toplam
kontenjan. Toplam kontenjanı 0 olan üniversite yok
(yokatlas.db: GROUP BY universite HAVING
SUM(COALESCE(kontenjan2026,0))=0 → 0 satır), yani hiçbir
sayfada "—" çıkmaz. URAP'ta payda kartın içindedir: "18."
tek başına yanıltır. */}
<div className="rounded-xl border border-slate-200 bg-white p-4">
{uni.urap ? (
<>
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
URAP Türkiye
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{sayi(uni.urap.sira)} / {sayi(URAP_TOPLAM)}
<span className="mt-0.5 block font-sans text-xs font-medium text-slate-500">
{URAP_DONEM}
</span>
</dd>
</>
) : (
<>
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
Toplam kontenjan
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{sayi(uni.stats.toplamKontenjan)}
</dd>
</>
)}
</div>
<div className="rounded-xl border border-slate-200 bg-white p-4">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
En iyi sıralama
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{uni.stats.minSira != null ? sayi(uni.stats.minSira) : "—"}
</dd>
</div>
<OzetKart
ikon={LayoutList}
etiket="Program"
deger={sayi(uni.programSayisi)}
alt={`${sayi(uni.fakulteSayisi)} birimde`}
/>
<OzetKart
ikon={Users}
etiket="Kontenjan"
deger={sayi(uni.stats.toplamKontenjan)}
alt={`${SON_YIL} toplamı`}
/>
<OzetKart
ikon={Target}
etiket="En iyi sıralama"
deger={
uni.stats.minSira != null ? sayi(uni.stats.minSira) : "—"
}
alt={`${SON_YIL} taban sırası`}
/>
{uni.urap ? (
<TurkiyeSirasiKarti sira={uni.urap.sira} />
) : (
<OzetKart
ikon={Building2}
etiket="Fakülte / birim"
deger={sayi(uni.fakulteSayisi)}
alt="programların dağıldığı"
/>
)}
</dl>
</div>
{uni.urap || uni.istatistik ? (
<AkademikAyrinti
universite={uni.ad}
onizleme={akademikOnizleme(uni)}
>
<AkademikGostergeler uni={uni} />
</AkademikAyrinti>
) : null}
</>
) : (
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
{uni.ad} taban puanları tablosunun {sayfa}. sayfası (
{sayi(tumProgramlar.length)} programın{" "}
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
satırları, fakülte sırasıyla).
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite
özeti ve istatistikler{" "}
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""}{" "}
Üniversite özeti ve istatistikler{" "}
<Link
href={`/universite/${slug}`}
className="font-medium text-primary hover:underline"
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
>
ilk sayfada
</Link>
@@ -455,13 +746,13 @@ export function UniversiteIcerik({
</>
)}
<PagePixelDivider seed={57} className="mt-8" />
<PagePixelDivider seed={57} className="mx-auto mt-12 lg:mx-0" />
<div id="programlar" className="mt-8">
<h2 className="font-heading text-xl font-bold text-slate-900">
<div id="programlar" className="mt-10 text-center lg:text-left">
<h2 className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl">
Üniversite programları
</h2>
<p className="mt-1 text-xs text-slate-500">
<p className="mt-2 text-base text-slate-500">
Fakülte ve birimlere göre
</p>
</div>
@@ -474,6 +765,7 @@ export function UniversiteIcerik({
</VeriKaynagi>
{ilkSayfa ? <UrapKaynakSatiri uni={uni} /> : null}
{ilkSayfa ? <IstatistikKaynakSatiri uni={uni} /> : null}
<Sayfalama
tabanYol={`/universite/${slug}`}
@@ -497,21 +789,21 @@ export function UniversiteIcerik({
export function UniversiteIcerikSkeleton() {
return (
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
<Skeleton className="h-4 w-64" />
<div className="mt-4 flex items-center gap-4">
<Skeleton className="h-5 w-64" />
<div className="mt-6 flex items-center gap-4">
<Skeleton className="size-14 rounded-lg" />
<Skeleton className="h-10 w-2/3 max-w-xl" />
</div>
<Skeleton className="mt-4 h-5 w-full max-w-3xl" />
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
{Array.from({ length: 5 }, (_, i) => (
<Skeleton key={i} className="h-24 w-full rounded-xl" />
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
))}
</div>
<div className="mt-16 space-y-3">
<Skeleton className="h-7 w-64" />
{Array.from({ length: 5 }, (_, i) => (
<Skeleton key={i} className="h-12 w-full" />
<Skeleton key={i} className="h-20 w-full rounded-2xl" />
))}
</div>
</main>

View File

@@ -27,7 +27,7 @@ export function UniversiteKonumHaritasi({
return (
<KonumBolumu universite={universite}>
<UlkeHaritasi universite={universite} konum={yurtdisi} />
<p className="pointer-events-none absolute bottom-0 left-0 text-[11px] font-medium text-slate-500">
<p className="pointer-events-none absolute bottom-0 left-0 text-xs font-medium text-slate-500">
{yurtdisi.ulkeAd}
{yurtdisi.sehir ? ` · ${yurtdisi.sehir}` : ""}
</p>

View File

@@ -36,29 +36,36 @@ export function UniversiteProgramTablosu({
}: {
gruplar: { fakulte: string; programlar: UniProgramSatiri[] }[];
}) {
// Şişko ve sade: program adı kesilmez (sarar), sıra ve butonlar iri.
// 375 px'te yalnız ad + butonlar sütun olarak kalır; taban sıra adın
// altındaki satıra iner, böylece ad bütün genişliği kullanır.
return (
<>
{gruplar.map(({ fakulte, programlar }, i) => (
<section key={`${fakulte}-${i}`} className="mt-6">
<h2 className="mb-3 font-heading text-xl font-bold text-slate-900">
<section key={`${fakulte}-${i}`} className="mt-10">
<h2 className="mb-4 text-center font-heading text-xl font-bold text-slate-900 sm:text-2xl lg:text-left">
{fakulte}
</h2>
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<Table>
<TableHeader>
<TableRow>
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
<TableRow className="hover:bg-transparent">
<TableHead className="h-12 pl-4 text-sm font-semibold text-slate-500 sm:pl-5">
Program
</TableHead>
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 lg:table-cell">
Son {VERI_YIL_SAYISI} yıl
</TableHead>
<TableHead className="hidden text-right lg:table-cell">
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 lg:table-cell">
{SON_YIL} puan
</TableHead>
<TableHead className="hidden text-right sm:table-cell">
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 md:table-cell">
Kontenjan
</TableHead>
<TableHead className="text-right">Taban sıra</TableHead>
<TableHead className="w-24 text-right">
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 sm:table-cell">
Taban sıra
</TableHead>
<TableHead className="h-12 w-28 text-right">
<span className="sr-only">Netler ve listeye ekle</span>
</TableHead>
</TableRow>
@@ -66,61 +73,64 @@ export function UniversiteProgramTablosu({
<TableBody>
{programlar.map((p) => {
const taban = programEtkinSira(p);
const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—";
return (
<TableRow key={p.id} className="kt-cv-satir">
<TableCell className="max-w-0 w-full">
<TableCell className="w-full whitespace-normal py-4 pl-4 sm:pl-5">
{p.bolumSlug ? (
<Link
href={`/bolum/${p.bolumSlug}`}
className="block truncate text-sm font-semibold text-slate-900 hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="block text-base font-semibold leading-snug text-slate-900 transition-colors hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{p.isim.trim()}
</Link>
) : (
<span className="block truncate text-sm font-semibold">
<span className="block text-base font-semibold leading-snug text-slate-900">
{p.isim.trim()}
</span>
)}
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
<span className="shrink-0 rounded bg-slate-100 px-1 py-px text-[10px] font-medium leading-4 text-slate-600">
<span className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500">
<span className="shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700">
{TUR_ETIKET[p.tur] ?? p.tur}
</span>
{p.sure ? (
<span className="shrink-0">{p.sure} yıl</span>
) : null}
</span>
<span className="mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden">
Taban sıra
<span className="font-semibold tabular-nums text-slate-900">
{tabanMetni}
</span>
<ProgramTabanTrendi program={p} />
</span>
</TableCell>
<TableCell className="hidden text-right text-xs md:table-cell">
<TableCell className="hidden text-right text-sm lg:table-cell">
<ProgramSiraGecmisi program={p} />
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
<TableCell className="hidden text-right text-sm tabular-nums text-slate-600 lg:table-cell">
{p.puanSon != null
? p.puanSon.toFixed(2).replace(".", ",")
: "—"}
</TableCell>
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
<TableCell className="hidden text-right text-sm tabular-nums text-slate-600 md:table-cell">
{p.kontenjanSon != null
? `${sayi(p.kontenjanSon)} kişi`
: "—"}
</TableCell>
<TableCell className="text-right text-xs tabular-nums text-slate-500">
<span className="inline-flex items-center gap-1">
<TableCell className="hidden text-right text-base font-semibold tabular-nums text-slate-900 sm:table-cell">
<span className="inline-flex items-center gap-1.5">
<ProgramTabanTrendi program={p} />
{taban != null ? `~${sayi(taban)}.` : "—"}
{tabanMetni}
</span>
</TableCell>
<TableCell className="pr-3 text-right">
<span className="inline-flex items-center gap-1.5">
<TableCell className="py-4 pr-4 text-right sm:pr-5">
<span className="inline-flex items-center gap-2">
<ProgramNetleriButonu
programId={p.id}
isim={p.isim.trim()}
className="size-8 min-h-8 min-w-8"
/>
<ProgramEkleButonu
program={p}
kaynak="universite"
className="size-8 min-h-8 min-w-8"
/>
<ProgramEkleButonu program={p} kaynak="universite" />
</span>
</TableCell>
</TableRow>