feat(tasarim): bölüm sayfası "şişko ve sade" dile göre
- Başlık bloğu lg altında ortalı, kimlik hapları (düzey + puan türü), dört özet kartı (Program, Devlet, Vakıf, En iyi sıralama), piksel ayraç - OzetKart src/components/ozet-kart.tsx'e çıkarıldı; üniversite ve bölüm sayfası oradan kullanıyor. 6-7 haneli sayı birimle kısalıyor (ölçüldü) - Program listesi §4.15: ad sarar, varyant ayrı satırda, 44 px butonlar, telefonda taban sıra adın altında, butonlar alt alta - Sayfalama 44 px, telefonda tek satır; hover'lar tek turuncu - Denetim: kanonik liste satırı adının mavi hover'ı artık uyarı vermiyor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
58
src/components/ozet-kart.tsx
Normal file
58
src/components/ozet-kart.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Özet kartı (DESIGN.md §4.8 — 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 (§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. Üniversite ve bölüm sayfalarının 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; çağıran
|
||||
* kartları bir `<dl>` içine koyar.
|
||||
*/
|
||||
export 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>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import {
|
||||
Building2,
|
||||
Calculator,
|
||||
ChevronRight,
|
||||
GraduationCap,
|
||||
Landmark,
|
||||
LayoutList,
|
||||
Target,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getAllBolumler,
|
||||
getAllBolumSlugs,
|
||||
@@ -23,6 +31,8 @@ import {
|
||||
import { BolumProgramListesi } from "./bolum-program-listesi";
|
||||
import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { NavBaslik } from "./nav-baslik";
|
||||
import { OzetKart } from "@/components/ozet-kart";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -35,6 +45,32 @@ import {
|
||||
|
||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||
|
||||
/**
|
||||
* Özet kartında iri sayı telefonda ~94 px'e sığmalı (DESIGN.md §4.8):
|
||||
* 36 px Cal Sans'ta "1.618.988" 123 px ölçüldü, ikonun altına giriyor. 6 ve
|
||||
* 7 haneli sıralar birimle kısalır: 245.300 → "245" + "bin",
|
||||
* 1.618.988 → "1,62" + "milyon".
|
||||
*/
|
||||
function kartSayisi(n: number): { deger: string; birim?: string } {
|
||||
// 999.500+ "1.000 bin" olmasın diye milyona geçer
|
||||
if (n >= 999_500) {
|
||||
return {
|
||||
deger: (n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 }),
|
||||
birim: "milyon",
|
||||
};
|
||||
}
|
||||
if (n >= 100_000) return { deger: sayi(Math.round(n / 1000)), birim: "bin" };
|
||||
return { deger: sayi(n) };
|
||||
}
|
||||
|
||||
/** Lisans puan türünün kimlik hapındaki adı (DB: SAYISAL, EŞİT AĞIRLIK…). */
|
||||
const PUAN_TURU_HAP: Record<string, string> = {
|
||||
SAYISAL: "Sayısal puan",
|
||||
"EŞİT AĞIRLIK": "Eşit ağırlık puanı",
|
||||
SÖZEL: "Sözel puan",
|
||||
DİL: "Dil puanı",
|
||||
};
|
||||
|
||||
export function bolumToplamSayfa(programSayisi: number): number {
|
||||
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
|
||||
}
|
||||
@@ -179,76 +215,126 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
// adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor).
|
||||
const ctaBaslik = `Sıralaman ${bolum.ad} tabanlarının neresinde?`;
|
||||
|
||||
// Kimlik hapları (DESIGN.md §4.12): düzey + lisans puan türleri. TYT
|
||||
// puanı önlisansın kendisi olduğundan ayrı hap açmaz.
|
||||
const puanTurleri = [
|
||||
...new Set(bolum.lisansProgramlari.map((p) => p.tur)),
|
||||
].flatMap((t) => (PUAN_TURU_HAP[t] ? [PUAN_TURU_HAP[t]] : []));
|
||||
const duzeyler = [
|
||||
...(bolum.lisansProgramlari.length > 0 ? ["Lisans"] : []),
|
||||
...(bolum.onlisansProgramlari.length > 0 ? ["Önlisans"] : []),
|
||||
];
|
||||
// Özet kartları: devlet ve vakıf ayrı sayılır; KKTC ve yurt dışı
|
||||
// programları toplamda var ama iki karta da girmez (vakıf sayılmaz).
|
||||
const devletUni = new Set<string>();
|
||||
const vakifUni = new Set<string>();
|
||||
let vakifProgram = 0;
|
||||
for (const p of tumProgramlar) {
|
||||
if (p.unitur === "DEVLET") devletUni.add(p.universite);
|
||||
else if (p.unitur === "VAKIF" || p.unitur === "VAKIF MYO") {
|
||||
vakifProgram += 1;
|
||||
vakifUni.add(p.universite);
|
||||
}
|
||||
}
|
||||
const { stats } = bolum;
|
||||
const hapSinifi =
|
||||
"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";
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
||||
<JsonLd
|
||||
data={breadcrumbJsonLd([
|
||||
{ name: "Ana Sayfa", path: "/" },
|
||||
{ name: "Bölümler", path: "/bolumler" },
|
||||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||||
])}
|
||||
/>
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||
<JsonLd
|
||||
data={breadcrumbJsonLd([
|
||||
{ name: "Ana Sayfa", path: "/" },
|
||||
{ name: "Bölümler", path: "/bolumler" },
|
||||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||||
])}
|
||||
/>
|
||||
|
||||
<nav
|
||||
aria-label="İçerik yolu"
|
||||
className="mb-5 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<nav
|
||||
aria-label="İçerik yolu"
|
||||
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
|
||||
>
|
||||
Ana sayfa
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
<Link
|
||||
href="/bolumler"
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Bölümler
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
{ilkSayfa ? (
|
||||
<span className="text-foreground">{bolum.ad}</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{bolum.ad}
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
<span className="text-foreground">Sayfa {sayfa}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<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="/bolumler"
|
||||
className="transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
Bölümler
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||
{ilkSayfa ? (
|
||||
<span className="text-slate-700">{bolum.ad}</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
{bolum.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 bölüm adı belirir */}
|
||||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||||
<header className="border-b pb-7 sm:pb-9">
|
||||
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir
|
||||
(logosu yok — DESIGN.md §4.14). Geniş ekran (lg) altında başlık
|
||||
bloğu ortalı (§5.2). */}
|
||||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||||
<h1
|
||||
id="bolum-baslik"
|
||||
className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||||
className="mt-6 text-center font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl lg:text-left"
|
||||
>
|
||||
{bolum.ad}
|
||||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||||
{!ilkSayfa && aralik ? (
|
||||
<>
|
||||
{" "}
|
||||
<span className="mt-2 block text-lg font-semibold tracking-normal text-muted-foreground sm:text-2xl">
|
||||
<span className="mt-2 block text-lg font-semibold text-slate-500 sm:text-2xl">
|
||||
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</h1>
|
||||
|
||||
{duzeyler.length > 0 || puanTurleri.length > 0 ? (
|
||||
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||||
{duzeyler.map((d) => (
|
||||
<span key={d} className={hapSinifi}>
|
||||
<GraduationCap className="size-4 text-primary" aria-hidden />
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
{puanTurleri.map((t) => (
|
||||
<span key={t} className={hapSinifi}>
|
||||
<Calculator className="size-4 text-primary" aria-hidden />
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{ilkSayfa ? (
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
{sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban
|
||||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
|
||||
<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">
|
||||
{bolum.ad} bölümünü açan {sayi(tumProgramlar.length)} üniversite
|
||||
programının {SON_YIL} taban puanlarını, başarı sıralamalarını ve
|
||||
kontenjanlarını incele.{" "}
|
||||
<em>
|
||||
{TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında
|
||||
netleşir.
|
||||
</em>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
<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">
|
||||
{bolum.ad} taban puanları tablosunun {sayfa}. sayfası (
|
||||
{sayi(tumProgramlar.length)} programın{" "}
|
||||
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
||||
@@ -257,54 +343,99 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
{aralik ? `: ${aralik}` : ""}. Listenin başı{" "}
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
ilk sayfada
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi
|
||||
başlık bloğunun hemen altında, listenin üstünde. Alttaki kart kalır. */}
|
||||
<div className="mt-6">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="universite-programlari" className="mt-8">
|
||||
<div className="mb-4 flex items-end justify-between gap-4">
|
||||
<h2
|
||||
id="universite-programlari"
|
||||
className="font-heading text-xl font-bold text-foreground sm:text-2xl"
|
||||
>
|
||||
Üniversite programları
|
||||
</h2>
|
||||
<span className="shrink-0 text-sm text-muted-foreground">
|
||||
{sayi(tumProgramlar.length)} program
|
||||
</span>
|
||||
{/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra
|
||||
girişi özet paragrafın hemen altında, listenin üstünde. Alttaki
|
||||
kart kalır. */}
|
||||
<div className="mt-8">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
|
||||
<BolumProgramListesi
|
||||
programlar={tumProgramlar.map((program) => ({
|
||||
...program,
|
||||
universiteSlug: uniSlugFromAd(program.universite),
|
||||
}))}
|
||||
bazAd={bolum.ad}
|
||||
tabanYol={`/bolum/${slug}`}
|
||||
sayfa={sayfa}
|
||||
/>
|
||||
<VeriKaynagi className="mt-6" />
|
||||
</section>
|
||||
{/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin
|
||||
sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */}
|
||||
{ilkSayfa ? (
|
||||
<dl className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<OzetKart
|
||||
ikon={LayoutList}
|
||||
etiket="Program"
|
||||
deger={sayi(stats.toplam)}
|
||||
alt={`${sayi(stats.ilSayisi)} ilde`}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Landmark}
|
||||
etiket="Devlet"
|
||||
deger={sayi(stats.devlet)}
|
||||
alt={
|
||||
devletUni.size > 0
|
||||
? `${sayi(devletUni.size)} üniversitede`
|
||||
: "bu bölümde yok"
|
||||
}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Building2}
|
||||
etiket="Vakıf"
|
||||
deger={sayi(vakifProgram)}
|
||||
alt={
|
||||
vakifUni.size > 0
|
||||
? `${sayi(vakifUni.size)} üniversitede`
|
||||
: "bu bölümde yok"
|
||||
}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Target}
|
||||
etiket="En iyi sıralama"
|
||||
{...(stats.minSira != null
|
||||
? kartSayisi(stats.minSira)
|
||||
: { deger: "—" })}
|
||||
alt={`${SON_YIL} taban sırası`}
|
||||
/>
|
||||
</dl>
|
||||
) : null}
|
||||
|
||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
konum="alt"
|
||||
baslik={ctaBaslik}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
<PagePixelDivider seed={83} className="mx-auto mt-12 lg:mx-0" />
|
||||
|
||||
<section aria-labelledby="universite-programlari" className="mt-10">
|
||||
<div className="text-center lg:text-left">
|
||||
<h2
|
||||
id="universite-programlari"
|
||||
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">
|
||||
{sayi(tumProgramlar.length)} program, taban sırasına göre
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<BolumProgramListesi
|
||||
programlar={tumProgramlar.map((program) => ({
|
||||
...program,
|
||||
universiteSlug: uniSlugFromAd(program.universite),
|
||||
}))}
|
||||
bazAd={bolum.ad}
|
||||
tabanYol={`/bolum/${slug}`}
|
||||
sayfa={sayfa}
|
||||
/>
|
||||
</div>
|
||||
<VeriKaynagi className="mt-6" />
|
||||
</section>
|
||||
|
||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
konum="alt"
|
||||
baslik={ctaBaslik}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
@@ -313,16 +444,20 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
|
||||
export function BolumIcerikSkeleton() {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
||||
<Skeleton className="h-5 w-56" />
|
||||
<div className="border-b pb-7 sm:pb-9">
|
||||
<Skeleton className="mt-5 h-10 w-2/3 max-w-xl" />
|
||||
<Skeleton className="mt-3 h-5 w-full max-w-2xl" />
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||
<Skeleton className="mx-auto h-5 w-56 lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-6 h-10 w-2/3 max-w-xl lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-5 h-10 w-48 rounded-full lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-6 h-5 w-full max-w-3xl lg:mx-0" />
|
||||
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<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>
|
||||
|
||||
@@ -15,7 +15,7 @@ function BolumLinkleri({ bolumler }: { bolumler: BolumOzet[] }) {
|
||||
<li key={b.slug}>
|
||||
<Link
|
||||
href={`/bolum/${b.slug}`}
|
||||
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 hover:text-primary"
|
||||
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 transition-colors hover:text-orange-500"
|
||||
>
|
||||
<span className="group-hover:underline">{b.ad}</span>
|
||||
<span className="text-xs text-slate-400">
|
||||
|
||||
@@ -58,7 +58,7 @@ function SiralamaDugmesi({
|
||||
aktif && yon === "artan" ? "azalan" : "artan"
|
||||
} sırala`}
|
||||
aria-pressed={aktif}
|
||||
className="inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-1.5 text-sm font-semibold text-slate-500 transition-colors hover:text-orange-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<span>{etiket}</span>
|
||||
<span className="flex flex-col -space-y-1" aria-hidden>
|
||||
@@ -66,7 +66,7 @@ function SiralamaDugmesi({
|
||||
className={`size-3 ${
|
||||
aktif && yon === "artan"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground/40"
|
||||
: "text-slate-300"
|
||||
}`}
|
||||
strokeWidth={aktif && yon === "artan" ? 3 : 2}
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ function SiralamaDugmesi({
|
||||
className={`size-3 ${
|
||||
aktif && yon === "azalan"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground/40"
|
||||
: "text-slate-300"
|
||||
}`}
|
||||
strokeWidth={aktif && yon === "azalan" ? 3 : 2}
|
||||
/>
|
||||
@@ -154,14 +154,21 @@ export function BolumProgramListesi({
|
||||
ref={listeRef}
|
||||
className="scroll-mt-24 overflow-hidden rounded-2xl border border-slate-200 bg-white"
|
||||
>
|
||||
{/* Şişko ve sade (DESIGN.md §4.15): üniversite adı kesilmez, sarar;
|
||||
satır butonları 44 px. 375 px'te yalnız ad + butonlar sütun
|
||||
olarak kalır; taban sıra adın altındaki satıra iner. Sıralama
|
||||
düğmeleri sütun başlıklarıyla birlikte sm'den itibaren görünür —
|
||||
telefonda liste zaten taban sırasına göre dizili. */}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Üniversite</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">
|
||||
Üniversite
|
||||
</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 lg:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="puanSon"
|
||||
aktifAlan={aktifAlan}
|
||||
@@ -169,7 +176,7 @@ export function BolumProgramListesi({
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right sm:table-cell">
|
||||
<TableHead className="hidden h-12 text-right md:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="kontenjanSon"
|
||||
aktifAlan={aktifAlan}
|
||||
@@ -177,7 +184,7 @@ export function BolumProgramListesi({
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<TableHead className="hidden h-12 text-right sm:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="siraSon"
|
||||
aktifAlan={aktifAlan}
|
||||
@@ -185,7 +192,7 @@ export function BolumProgramListesi({
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-24 text-right">
|
||||
<TableHead className="h-12 w-14 text-right sm:w-28">
|
||||
<span className="sr-only">Netler ve listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -194,12 +201,13 @@ export function BolumProgramListesi({
|
||||
{gosterilenProgramlar.map((p) => {
|
||||
const uniAd = trBaslikDuzeni(p.universite);
|
||||
const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null;
|
||||
const devlet = p.unitur === "DEVLET";
|
||||
const tabanMetni =
|
||||
p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—";
|
||||
|
||||
return (
|
||||
<TableRow key={p.id} className="kt-cv-satir">
|
||||
<TableCell className="max-w-0 w-full">
|
||||
<span className="flex min-w-0 items-center gap-2.5">
|
||||
<TableCell className="w-full whitespace-normal py-4 pl-4 sm:pl-5">
|
||||
<span className="flex items-start gap-3">
|
||||
{p.universiteSlug ? (
|
||||
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
|
||||
) : null}
|
||||
@@ -207,71 +215,71 @@ export function BolumProgramListesi({
|
||||
{p.universiteSlug ? (
|
||||
<Link
|
||||
href={`/universite/${p.universiteSlug}`}
|
||||
className="block truncate text-sm font-semibold 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"
|
||||
>
|
||||
{uniAd}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
<span className="block text-base font-semibold leading-snug text-slate-900">
|
||||
{uniAd}
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
||||
{varyant ? (
|
||||
<span className="mt-0.5 block text-sm leading-snug text-slate-600">
|
||||
{varyant}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500">
|
||||
{p.unitur ? (
|
||||
<span
|
||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||
devlet
|
||||
? "bg-slate-100 text-slate-600"
|
||||
: "bg-violet-50 text-violet-600"
|
||||
}`}
|
||||
>
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700">
|
||||
{UNITUR_ETIKET[p.unitur]?.cip ??
|
||||
trBaslikDuzeni(p.unitur)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate">
|
||||
{p.il ? trBaslikDuzeni(p.il) : null}
|
||||
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
<span className="shrink-0 text-slate-400">
|
||||
· {p.sure} yıl
|
||||
{p.il ? (
|
||||
<span className="shrink-0">
|
||||
{trBaslikDuzeni(p.il)}
|
||||
</span>
|
||||
) : null}
|
||||
{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>
|
||||
</span>
|
||||
</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} />
|
||||
{p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"}
|
||||
{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 flex-col items-center gap-2 sm:flex-row">
|
||||
<ProgramNetleriButonu
|
||||
programId={p.id}
|
||||
isim={p.isim.trim()}
|
||||
universite={uniAd}
|
||||
className="size-8 min-h-8 min-w-8"
|
||||
/>
|
||||
<ProgramEkleButonu
|
||||
program={p}
|
||||
kaynak="bolum"
|
||||
className="size-8 min-h-8 min-w-8"
|
||||
/>
|
||||
<ProgramEkleButonu program={p} kaynak="bolum" />
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -96,7 +96,7 @@ export function ProgramNetleriButonu({
|
||||
type="button"
|
||||
title="Son yerleşenin netleri"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-primary hover:bg-primary/5 hover:text-primary active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
|
||||
className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 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.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
|
||||
>
|
||||
<ListChecks className="size-4" aria-hidden />
|
||||
<span className="sr-only">{isim} — son yerleşenin netleri</span>
|
||||
|
||||
@@ -95,7 +95,9 @@ export function Sayfalama({
|
||||
</Link>
|
||||
);
|
||||
|
||||
// 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son
|
||||
// 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son. Telefonda
|
||||
// (sm altı) tek satır kalsın diye yalnız Önceki · aktif · Sonraki görünür;
|
||||
// diğer numaralar HTML'de durur (tarayıcı botları tüm sayfaları görür).
|
||||
const numaralar: (number | "...")[] = [];
|
||||
if (toplamSayfa <= 7) {
|
||||
for (let n = 1; n <= toplamSayfa; n++) numaralar.push(n);
|
||||
@@ -116,12 +118,12 @@ export function Sayfalama({
|
||||
return (
|
||||
<nav
|
||||
aria-label="Sayfalama"
|
||||
className="mt-6 flex flex-wrap items-center justify-center gap-1.5"
|
||||
className="mt-8 flex flex-wrap items-center justify-center gap-2"
|
||||
>
|
||||
{sayfa > 1
|
||||
? gecis(
|
||||
sayfa - 1,
|
||||
"inline-flex h-9 items-center gap-1 rounded-full border border-slate-200 bg-white px-3.5 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
||||
"inline-flex h-11 items-center gap-1 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-700 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]",
|
||||
<>
|
||||
<ChevronLeft className="size-4" aria-hidden />
|
||||
Önceki
|
||||
@@ -133,7 +135,7 @@ export function Sayfalama({
|
||||
n === "..." ? (
|
||||
<span
|
||||
key={`bosluk-${i}`}
|
||||
className="px-1.5 text-sm text-slate-400"
|
||||
className="hidden px-1.5 text-sm text-slate-400 sm:inline"
|
||||
aria-hidden
|
||||
>
|
||||
…
|
||||
@@ -142,8 +144,8 @@ export function Sayfalama({
|
||||
gecis(
|
||||
n,
|
||||
n === sayfa
|
||||
? "inline-flex h-9 min-w-9 items-center justify-center rounded-full bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
||||
: "inline-flex h-9 min-w-9 items-center justify-center rounded-full border border-slate-200 bg-white px-2 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
||||
? "inline-flex h-11 min-w-11 items-center justify-center rounded-full bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
||||
: "hidden h-11 min-w-11 items-center justify-center rounded-full border border-slate-200 bg-white px-2 text-sm font-semibold text-slate-700 sm:inline-flex 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]",
|
||||
n,
|
||||
{ aktif: n === sayfa },
|
||||
)
|
||||
@@ -152,7 +154,7 @@ export function Sayfalama({
|
||||
{sayfa < toplamSayfa
|
||||
? gecis(
|
||||
sayfa + 1,
|
||||
"inline-flex h-9 items-center gap-1 rounded-full border border-slate-200 bg-white px-3.5 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
||||
"inline-flex h-11 items-center gap-1 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-700 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]",
|
||||
<>
|
||||
Sonraki
|
||||
<ChevronRight className="size-4" aria-hidden />
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
Target,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
getAllUniversiteler,
|
||||
getAllUniversiteSlugs,
|
||||
@@ -55,6 +53,7 @@ import { UniLogo } from "@/components/uni-logo";
|
||||
import { UrapLogo } from "./urap-logo";
|
||||
import { AkademikAyrinti } from "./akademik-ayrinti";
|
||||
import { NavBaslik } from "./nav-baslik";
|
||||
import { OzetKart } from "@/components/ozet-kart";
|
||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { Program } from "@/types/yokatlas";
|
||||
@@ -313,61 +312,6 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ö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
|
||||
|
||||
@@ -65,7 +65,7 @@ export function UniversiteProgramTablosu({
|
||||
<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">
|
||||
<TableHead className="h-12 w-14 text-right sm:w-28">
|
||||
<span className="sr-only">Netler ve listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -125,7 +125,7 @@ export function UniversiteProgramTablosu({
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="py-4 pr-4 text-right sm:pr-5">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="inline-flex flex-col items-center gap-2 sm:flex-row">
|
||||
<ProgramNetleriButonu
|
||||
programId={p.id}
|
||||
isim={p.isim.trim()}
|
||||
|
||||
@@ -111,7 +111,7 @@ export function ProgramEkleButonu({
|
||||
className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.92] disabled:cursor-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
||||
listede
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-600"
|
||||
: "border-slate-200 bg-white text-slate-500 hover:border-primary hover:bg-primary/5 hover:text-primary disabled:opacity-40"
|
||||
: "border-slate-200 bg-white text-slate-500 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 disabled:opacity-40"
|
||||
} ${className}`}
|
||||
>
|
||||
{listede ? (
|
||||
|
||||
Reference in New Issue
Block a user