Ü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>
812 lines
31 KiB
TypeScript
812 lines
31 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
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,
|
||
getUniversiteBySlug,
|
||
bolumSlugFromIsim,
|
||
} from "../katalog-queries";
|
||
import { trBaslikDuzeni } from "@/lib/slug";
|
||
import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur";
|
||
import {
|
||
JsonLd,
|
||
breadcrumbJsonLd,
|
||
sayfaMetadata,
|
||
SITE_URL,
|
||
ustSegmentOgGorseli,
|
||
} from "@/lib/seo";
|
||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||
import { SiteFooter } from "@/components/site-footer";
|
||
import {
|
||
KATALOG_CTA_ACIKLAMA,
|
||
Sayfalama,
|
||
SAYFA_BOYU,
|
||
sayfaDilimi,
|
||
siraYiliEtiketi,
|
||
} from "./sayfalama";
|
||
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 {
|
||
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";
|
||
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||
|
||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||
|
||
export function uniToplamSayfa(programSayisi: number): number {
|
||
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
|
||
}
|
||
|
||
// Sayfaların generateStaticParams'ı için — sayfalar *-queries import etmez.
|
||
export function uniStaticSlugs(): string[] {
|
||
return getAllUniversiteSlugs();
|
||
}
|
||
|
||
// 2..N sayfaları; 1. sayfa taban rota (/universite/[slug]) olduğundan üretilmez.
|
||
export function uniSayfaStaticParams(): { slug: string; no: string }[] {
|
||
return getAllUniversiteler().flatMap((u) => {
|
||
const toplam = uniToplamSayfa(u.programSayisi);
|
||
return Array.from({ length: Math.max(0, toplam - 1) }, (_, i) => ({
|
||
slug: u.slug,
|
||
no: String(i + 2),
|
||
}));
|
||
});
|
||
}
|
||
|
||
type UniDetay = NonNullable<ReturnType<typeof getUniversiteBySlug>>;
|
||
|
||
/**
|
||
* Sayfanın satır dilimi + o dilimde geçen fakülte/birim adları (tablo
|
||
* sırasıyla, tekrarsız). Liste fakülteye göre dizili olduğundan her derin
|
||
* sayfa belirli birimleri kapsar. Metadata ve gövde aynı hesabı buradan alır.
|
||
* Fakültesi boş satırlar tabloda "Diğer" grubuna düşer; ad listesine girmez.
|
||
*/
|
||
function uniSayfaDilimi(uni: UniDetay, sayfa: number) {
|
||
// Sayfalama fakülte sıralı düz liste üzerinden; dilim içi yeniden gruplanır
|
||
const tumProgramlar = uni.fakulteler.flatMap((f) => f.programlar);
|
||
const dilim = sayfaDilimi(tumProgramlar, sayfa);
|
||
const fakulteAdlari: string[] = [];
|
||
for (const p of dilim) {
|
||
if (p.fakulte && !fakulteAdlari.includes(p.fakulte)) {
|
||
fakulteAdlari.push(p.fakulte);
|
||
}
|
||
}
|
||
return { tumProgramlar, dilim, fakulteAdlari };
|
||
}
|
||
|
||
/**
|
||
* İlk 2 ad + "ve N birim daha"; ad yoksa null (çağıran eski metne düşer).
|
||
* Dilim tek birimden ibaretse (50'den çok programlı MYO'lar art arda iki
|
||
* sayfayı doldurabiliyor) sayfalar ayırt edilsin diye birim içi taban sırası
|
||
* aralığı eklenir — birim içinde satırlar zaten taban sırasına göre dizili.
|
||
*/
|
||
function fakulteOzeti({
|
||
dilim,
|
||
fakulteAdlari: adlar,
|
||
}: ReturnType<typeof uniSayfaDilimi>): string | null {
|
||
if (adlar.length === 0) return null;
|
||
if (adlar.length === 1) {
|
||
const siralar = dilim
|
||
.map((p) => p.efektifSira)
|
||
.filter((n): n is number => n != null);
|
||
if (siralar.length === 0) return adlar[0];
|
||
const min = Math.min(...siralar);
|
||
const max = Math.max(...siralar);
|
||
const yil = siraYiliEtiketi(
|
||
dilim.filter((p) => p.siraSon == null && p.efektifSira != null).length,
|
||
siralar.length,
|
||
);
|
||
return min === max
|
||
? `${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`;
|
||
}
|
||
|
||
/**
|
||
* Tablonun altındaki AYRI ikinci kaynak satırı. `VeriKaynagi` kullanılmaz ve
|
||
* kopyalanmaz: o satır YÖK Atlas'ın yerleştirme yılına ve son güncelleme
|
||
* tarihine bağlı, URAP'ın dönemi başka — aynı satırda birleşirlerse URAP
|
||
* verisi YÖK Atlas tarihiyle sunulmuş olur.
|
||
*
|
||
* 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. 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) {
|
||
const yurtDisi =
|
||
uni.unitur === "KKTC" || uni.unitur?.startsWith("YURTDISI");
|
||
return (
|
||
<p className="mt-2 text-xs leading-5 text-slate-500">
|
||
{yurtDisi
|
||
? "Bu üniversite URAP Türkiye sıralamasında yer almıyor — URAP yalnız Türkiye'deki üniversiteleri sıralıyor."
|
||
: `Bu üniversite URAP Türkiye Sıralaması ${URAP_DONEM} listesinde yer almıyor.`}
|
||
</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">
|
||
Öğ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={YOK_ISTATISTIK_KAYNAK_URL}
|
||
target="_blank"
|
||
rel="noopener"
|
||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
istatistik.yok.gov.tr
|
||
</a>
|
||
.
|
||
</p>
|
||
);
|
||
}
|
||
|
||
export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||
const uni = getUniversiteBySlug(slug);
|
||
if (!uni) return {};
|
||
if (sayfa > 1) {
|
||
// Üniversite adları uzun: <title> sayfa numarasıyla kalır (~60 karakter),
|
||
// farklılaştırıcı (o sayfadaki birimler) açıklamaya ve gövdeye gider.
|
||
const birimler = fakulteOzeti(uniSayfaDilimi(uni, sayfa));
|
||
return sayfaMetadata({
|
||
title: `${uni.ad} Taban Puanları ${BASLIK_YILI} (Sayfa ${sayfa})`,
|
||
description: birimler
|
||
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
|
||
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın ${SON_YIL} yerleştirme verileri.`,
|
||
path: `/universite/${slug}/sayfa/${sayfa}`,
|
||
openGraph: {
|
||
images: ustSegmentOgGorseli(
|
||
`/universite/${slug}`,
|
||
"Üniversite taban puanları ve sıralamaları — KolayTercih",
|
||
),
|
||
},
|
||
});
|
||
}
|
||
return sayfaMetadata({
|
||
title: `${uni.ad} Taban Puanları ve Başarı Sıralamaları ${BASLIK_YILI}`,
|
||
description: `${uni.ad} ${SON_YIL} yerleştirme verileri: ${uni.programSayisi} programın taban puanları, başarı sıralamaları ve kontenjanları${uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}. ${TERCIH_YILI} tercih rehberi.`,
|
||
path: `/universite/${slug}`,
|
||
});
|
||
}
|
||
|
||
export function UniversiteIcerik({
|
||
slug,
|
||
sayfa,
|
||
}: {
|
||
slug: string;
|
||
sayfa: number;
|
||
}) {
|
||
const uni = getUniversiteBySlug(slug);
|
||
if (!uni) notFound();
|
||
|
||
const sayfaDilim = uniSayfaDilimi(uni, sayfa);
|
||
const { tumProgramlar, dilim } = sayfaDilim;
|
||
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
|
||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||
const birimler = fakulteOzeti(sayfaDilim);
|
||
const gruplar: {
|
||
fakulte: string;
|
||
programlar: (Program & { bolumSlug: string | null })[];
|
||
}[] = [];
|
||
for (const p of dilim) {
|
||
const anahtar = p.fakulte ?? "Diğer";
|
||
const satir = { ...p, bolumSlug: bolumSlugFromIsim(p.isim) };
|
||
const son = gruplar[gruplar.length - 1];
|
||
if (son && son.fakulte === anahtar) son.programlar.push(satir);
|
||
else gruplar.push({ fakulte: anahtar, programlar: [satir] });
|
||
}
|
||
|
||
const ilkSayfa = sayfa === 1;
|
||
// Tür iki yerde geçer ve bu bilinçli: H1'in altındaki çip taranmak,
|
||
// özet paragrafın ilk virgülündeki öbek okunmak içindir. Izgarada "Tür"
|
||
// kartı yok (şablon 4 hücre, bkz. aşağıdaki ızgara yorumu). Haritada
|
||
// olmayan ya da NULL bir `unitur`'da öbek hiç yazılmaz: cümlede kopuk
|
||
// virgül/boşluk kalmaz ve ham DB değeri cümlenin içine sızmaz.
|
||
const turEtiketi = uni.unitur ? UNITUR_ETIKET[uni.unitur] : undefined;
|
||
const turCumlesi = turEtiketi?.cumle;
|
||
const buYol = ilkSayfa
|
||
? `/universite/${slug}`
|
||
: `/universite/${slug}/sayfa/${sayfa}`;
|
||
const yurtdisi = uniUlkeKonum(uni.ad, uni.il);
|
||
// Üst satır ve alt kart aynı soruyu sorar; sayfalama sayfalarında da
|
||
// üniversite adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor).
|
||
const ctaBaslik = `Sıralaman ${uni.ad} tabanlarının neresinde?`;
|
||
// Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi
|
||
// ö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-8">
|
||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||
<JsonLd
|
||
data={breadcrumbJsonLd([
|
||
{ name: "Ana Sayfa", path: "/" },
|
||
{ name: "Üniversiteler", path: "/universiteler" },
|
||
{ name: uni.ad, path: `/universite/${slug}` },
|
||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||
])}
|
||
/>
|
||
{ilkSayfa ? (
|
||
<JsonLd
|
||
data={{
|
||
"@context": "https://schema.org",
|
||
"@type": "CollegeOrUniversity",
|
||
name: uni.ad,
|
||
url: `${SITE_URL}/universite/${slug}`,
|
||
...(uniLogoYolu(slug)
|
||
? { logo: `${SITE_URL}${uniLogoYolu(slug)}` }
|
||
: {}),
|
||
...(uni.il || yurtdisi
|
||
? {
|
||
address: {
|
||
"@type": "PostalAddress",
|
||
...(yurtdisi?.sehir
|
||
? { addressLocality: yurtdisi.sehir }
|
||
: uni.il
|
||
? { addressLocality: trBaslikDuzeni(uni.il) }
|
||
: {}),
|
||
addressCountry: yurtdisi ? yurtdisi.iso : "TR",
|
||
},
|
||
}
|
||
: {}),
|
||
}}
|
||
/>
|
||
) : null}
|
||
|
||
<nav
|
||
aria-label="breadcrumb"
|
||
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
|
||
>
|
||
<Link
|
||
href="/"
|
||
className="transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
Ana Sayfa
|
||
</Link>
|
||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||
<Link
|
||
href="/universiteler"
|
||
className="transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
Üniversiteler
|
||
</Link>
|
||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||
{ilkSayfa ? (
|
||
<span className="text-slate-700">{uni.ad}</span>
|
||
) : (
|
||
<>
|
||
<Link
|
||
href={`/universite/${slug}`}
|
||
className="transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
{uni.ad}
|
||
</Link>
|
||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||
<span className="text-slate-700">Sayfa {sayfa}</span>
|
||
</>
|
||
)}
|
||
</nav>
|
||
|
||
{/* 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
|
||
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 — 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="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="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
|
||
sonuçlarına göre{" "}
|
||
<strong>{sayi(uni.programSayisi)} programda</strong> öğrenci aldı
|
||
{uni.fakulteSayisi > 1
|
||
? `; programlar ${sayi(uni.fakulteSayisi)} fakülte/yüksekokula dağılıyor`
|
||
: ""}
|
||
.
|
||
{uni.stats.minSira != null ? (
|
||
<>
|
||
{" "}
|
||
En iyi taban başarı sıralaması{" "}
|
||
<strong>{sayi(uni.stats.minSira)}</strong>.
|
||
</>
|
||
) : null}
|
||
{uni.stats.toplamKontenjan > 0 ? (
|
||
<> Toplam kontenjan {sayi(uni.stats.toplamKontenjan)}.</>
|
||
) : null}{" "}
|
||
Aşağıdaki tablolar {TERCIH_YILI} tercihleri için rehber
|
||
niteliğindedir;{" "}
|
||
<em>
|
||
{TERCIH_YILI} taban puanları ve sıralamaları, yerleştirme
|
||
sonuçları açıklandığında netleşir.
|
||
</em>
|
||
</p>
|
||
|
||
{ustSiraSatiri}
|
||
|
||
<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">
|
||
<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="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{" "}
|
||
<Link
|
||
href={`/universite/${slug}`}
|
||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||
>
|
||
ilk sayfada
|
||
</Link>
|
||
.
|
||
</p>
|
||
{ustSiraSatiri}
|
||
</>
|
||
)}
|
||
|
||
<PagePixelDivider seed={57} className="mx-auto mt-12 lg:mx-0" />
|
||
|
||
<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-2 text-base text-slate-500">
|
||
Fakülte ve birimlere göre
|
||
</p>
|
||
</div>
|
||
|
||
<UniversiteProgramTablosu gruplar={gruplar} />
|
||
|
||
<VeriKaynagi>
|
||
"—": o yıl için yerleştirme verisi bulunmuyor (yeni açılan
|
||
veya o yıl yerleşen çıkmayan program).{" "}
|
||
</VeriKaynagi>
|
||
|
||
{ilkSayfa ? <UrapKaynakSatiri uni={uni} /> : null}
|
||
{ilkSayfa ? <IstatistikKaynakSatiri uni={uni} /> : null}
|
||
|
||
<Sayfalama
|
||
tabanYol={`/universite/${slug}`}
|
||
sayfa={sayfa}
|
||
toplamSayfa={toplamSayfa}
|
||
/>
|
||
|
||
<div className="mt-10">
|
||
<CtaSiraForm
|
||
konum="alt"
|
||
baslik={ctaBaslik}
|
||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||
/>
|
||
</div>
|
||
</main>
|
||
<SiteFooter />
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function UniversiteIcerikSkeleton() {
|
||
return (
|
||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||
<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-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-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-20 w-full rounded-2xl" />
|
||
))}
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|