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:
bilalgursen
2026-09-24 00:39:21 +03:00
parent 6a9af19ef2
commit 5b51fd16e0
11 changed files with 394 additions and 214 deletions

View File

@@ -222,7 +222,7 @@ shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı
### 4.7 İkonlar
Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz.
### 4.8 Özet kartı — `OzetKart`, `universite-icerik.tsx`
### 4.8 Özet kartı — `OzetKart`, `src/components/ozet-kart.tsx`
Apple Fitness özet kartının sitenin ölçüleriyle hâli. Sayfada **en çok dört** tane (harita
yanındaki 2×2).
@@ -239,7 +239,11 @@ yanındaki 2×2).
ikon kutusu `flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-10`.
- `dl` anlamı için `dt` (başlık) DOM'da önce gelir, görsel sıra `order-*` ile kurulur.
- Telefonda kart iç genişliği ~131 px: en uzun sayı ("13.889", 36 px'te ~90 px) + 32 px ikon
sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz.
sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz. Ölçülen sınır: 36 px'te
"1.618.988" 123 px — ikonun altına girer. 6–7 haneli sayı birimle kısalır: 245.300 →
`245` + birim `bin`, 1.618.988 → `1,62` + birim `milyon` (bölüm sayfasındaki `kartSayisi`).
- Kullanıldığı yerler: üniversite ve bölüm sayfası (bölümde dört kart: Program, Devlet,
Vakıf, En iyi sıralama; `grid grid-cols-2 gap-4 lg:grid-cols-4`, haritası olmadığı için).
- Tıklanabilir kart (ör. Türkiye sırası → sıralama sayfası): kartın tamamı link olur
(`after:absolute after:inset-0` gerilmiş link), iç kaynak linki `relative z-10` ile üstte
kalır; hover §2.1.
@@ -341,7 +345,14 @@ opaklık.
adın altındaki satıra iner: `mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden`
içinde değer `font-semibold text-slate-900`.
- Tür çipi: `rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700`.
- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez.
- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. Telefonda
iki buton alt alta durur (`inline-flex flex-col items-center gap-2 sm:flex-row`, sütun
`w-14 sm:w-28`): yan yana 96 px yiyip adı 157 px'e sıkıştırıyordu, alt alta 273 px kalıyor
(24 Eyl 2026 ölçümü).
- Satırda logo varsa (bölüm sayfası) `UniLogo boy="sm"` adın solunda, `items-start`.
- Program varyantı ("… (İngilizce) (Burslu)") adın altında ayrı satır
`mt-0.5 block text-sm leading-snug text-slate-600`; tür çipine yapıştırılıp kesilmez.
- Tür çipi devlet/vakıf için de aynı slate çip; vakıf violet değildir (§2.2).
- Tablo başlığı `h-12 text-sm font-semibold text-slate-500`; hücre dolgusu `py-4 pl-4 sm:pl-5`.
## 5. Yerleşim
@@ -378,6 +389,11 @@ kartlarla aynı hizada:
yorar), form alanları, uzun rehber metni.
- Alt sayfa (§4.10) içeriği her ekranda ortalıdır.
### 5.2.1 Sayfalama — `Sayfalama`, `sayfalama.tsx`
Haplar `h-11` (44 px), hover §2.1. Telefonda (sm altı) tek satır kalsın diye yalnız Önceki ·
aktif sayfa · Sonraki görünür; diğer numaralar `hidden sm:inline-flex` ile HTML'de durur
(tarayıcı botları tüm sayfaları görür). 375 px'te 7 sayfalık şerit iki satıra kırılıyordu.
### 5.3 Verimli ve havadar
- İlişkili öğeler yan yana dizilir (iki sütun) — tek sütunda alt alta uzayan panel yapılmaz.
- Yan yana dizmek "hepsini tek şeride sıkıştırmak" değildir: 23 Eyl'de denenen yatay "bilgi
@@ -470,7 +486,11 @@ Bu dilde sorunların çoğu koddan görünmez (taşma, sığma, hizalanma). Tasa
### 9.4 Site taraması için denetim uyarıları
`pnpm tasarim:denetim` bu dilin eski izlerini **uyarı** olarak listeler (lint'i kırmaz, dönüşüm
listesidir): `minik-yazi` (10–11 px), `buyuk-harf-etiket` (uppercase + tracking-wide),
`mavi-hover` (§2.1; liste satırı adı istisna), `iri-sayi-tabular` (Cal Sans + tabular-nums).
`mavi-hover` (§2.1), `iri-sayi-tabular` (Cal Sans + tabular-nums).
`mavi-hover` §4.15 kanonik liste satırı adını (`text-base font-semibold leading-snug` +
`hover:text-primary hover:underline` aynı satırda) kendiliğinden atlar — istisna uyarı
vermez (Bilal'in kararı, 24 Eyl 2026). Eski `truncate text-sm` adlar yine uyarır; önce §4.15'e
çevrilir.
Bir ekranı dönüştüren ajan o ekrandaki uyarıları sıfırlar ya da neden kaldığını yazar.
### 9.5 Bitirmeden

View File

@@ -191,7 +191,20 @@ const KURALLAR: Kural[] = [
ad: "mavi-hover",
seviye: "uyari",
mesaj: "Hover rengi tek turuncu (orange-500); liste satırındaki ad linkiyse istisna, mavi kalır (DESIGN.md §2.1)",
bul: regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/),
// §2.1 istisnası: §4.15 kanonik liste satırı adı (sarar, text-base
// font-semibold leading-snug + hover:text-primary hover:underline) mavi
// kalır. Eski truncate/text-sm adlar istisnaya girmez — önce §4.15'e
// çevrilir. (Bilal'in kararı, 24 Eyl 2026: istisna uyarı vermesin.)
bul: (icerik) =>
regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/)(icerik).filter((e) => {
const bas = icerik.lastIndexOf("\n", e.index) + 1;
const son = icerik.indexOf("\n", e.index);
const satir = icerik.slice(bas, son === -1 ? undefined : son);
return !(
/\btext-base font-semibold leading-snug\b/.test(satir) &&
/\bhover:text-primary hover:underline\b/.test(satir)
);
}),
},
{
ad: "iri-sayi-tabular",

View 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>
);
}

View File

@@ -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,9 +215,34 @@ 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">
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
<JsonLd
data={breadcrumbJsonLd([
{ name: "Ana Sayfa", path: "/" },
@@ -193,62 +254,87 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
<nav
aria-label="İçerik yolu"
className="mb-5 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
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-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="transition-colors hover:text-orange-500 hover:underline"
>
Ana sayfa
Ana Sayfa
</Link>
<ChevronRight className="size-3.5" aria-hidden />
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
<Link
href="/bolumler"
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="transition-colors hover:text-orange-500 hover:underline"
>
Bölümler
</Link>
<ChevronRight className="size-3.5" aria-hidden />
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
{ilkSayfa ? (
<span className="text-foreground">{bolum.ad}</span>
<span className="text-slate-700">{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"
className="transition-colors hover:text-orange-500 hover:underline"
>
{bolum.ad}
</Link>
<ChevronRight className="size-3.5" aria-hidden />
<span className="text-foreground">Sayfa {sayfa}</span>
<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 */}
{/* 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} />
<header className="border-b pb-7 sm:pb-9">
<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,34 +343,78 @@ 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">
{/* 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>
<section aria-labelledby="universite-programlari" className="mt-8">
<div className="mb-4 flex items-end justify-between gap-4">
{/* 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}
<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-xl font-bold text-foreground sm:text-2xl"
className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl"
>
Üniversite programları
</h2>
<span className="shrink-0 text-sm text-muted-foreground">
{sayi(tumProgramlar.length)} program
</span>
<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,
@@ -294,6 +424,7 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
tabanYol={`/bolum/${slug}`}
sayfa={sayfa}
/>
</div>
<VeriKaynagi className="mt-6" />
</section>
@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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

View File

@@ -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()}

View File

@@ -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 ? (