feat(tasarim): şehir sayfasına hero dilinde piksel harita; üniversite konum kartı da aynı harita
All checks were successful
Deploy / deploy (push) Successful in 4m39s
All checks were successful
Deploy / deploy (push) Successful in 4m39s
- scripts/il-izgara-uret.ts → src/lib/il-izgara-verisi.ts: 81 il ve 7 ülke kontürü hero'nun dot-matrix ızgarasına (72×31) rasterize; küçük iller 2×2 garanti. - SehirPikselHaritasi: iri kareler, altı opaklık, kıyı serpintisi, usePixelHeat ısı fırçası; bulunulan il iri turuncu karelerle sırayla belirir (kt-piksel-belir) ve nabız atar; KKTC/yurt dışında şehir merkezinde 2×2 iri kare. - Şehir sayfası başlığı beyaz piksel dokulu yüzeyde (SehirYuzeyi), harita sağda/altta. - UniversiteKonumHaritasi düz SVG yerine aynı piksel haritayı kullanır. - DESIGN.md §4.12/§4.17/§10.8 güncellendi. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -352,7 +352,13 @@ aşağı kaymış, kare boyları farklı — ne tepeler ne bitişler hizalı.**
|
||||
Başlığın altında kurumun kimliği; navbar haplarının ölçüsünde:
|
||||
`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`
|
||||
+ başta `size-4 text-primary` ikon (`Landmark` devlet, `Building2` vakıf, `MapPin` il). Tür
|
||||
renkle değil ikonla ayrılır. Etkileşimli değildir; rozet (§4.2) değildir.
|
||||
renkle değil ikonla ayrılır. Rozet (§4.2) değildir. Tür hapı etkileşimsizdir; **il hapı şehir
|
||||
sayfasına link**tir (Bilal, 24 Eyl 2026: "şehirlerin geçtiği her yer tıklanabilir") — aynı
|
||||
sınıf + `hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]`.
|
||||
Kural genel: bir şehir adı nerede geçerse geçsin (`/universiteler` listesi, bölüm/sonuç/listem/
|
||||
tadımlık satırlarındaki tür-il-süre metası) `ilSayfaHref(il)` ile `/universiteler/[il]`'e
|
||||
gider; satır metasında `SATIR_IKINCIL_LINK`. İstisna: satırın tamamı zaten link olan yerler
|
||||
(URAP sıralaması, arama sonucu) — iç içe `<a>` olmaz, orada düz metin kalır.
|
||||
|
||||
### 4.13 Piksel sıra şeridi — `PixelSira`, `src/components/pixel-decor.tsx`
|
||||
Bir sıranın (ör. 198 içinde 5.) yerini tek satır piksel şeridinde gösterir; sıranın düştüğü
|
||||
@@ -441,11 +447,29 @@ bulunulan şehir `border-primary bg-primary text-white` + `aria-current="page"`.
|
||||
- Çip satırında yalnız **2+ üniversiteli** şehirler (≈28) durur; 90'ı aşkın şehrin tamamı
|
||||
`/universiteler#tum-sehirler` altındaki A–Z üç sütunlu metin listesindedir (§1.1 "sık
|
||||
bakılan önde, ayrıntı bir dokunuş derinde"). Satırın sonunda "Tüm şehirler (N)" çipi.
|
||||
- Şehir sayfası (`il-icerik.tsx`) katalog detay düzenindedir (§5.2): yol satırı, h1
|
||||
- Şehir sayfası (`il-icerik.tsx`) katalog detay düzenindedir (§5.2): yol satırı, **başlık
|
||||
yüzeyi** (`SehirYuzeyi`: `relative isolate overflow-hidden rounded-3xl border border-slate-200
|
||||
bg-white p-6 sm:p-8`, `PikselZemin ton="acik"` seed 31, harita kabı `haric`), içinde h1
|
||||
"İstanbul Üniversiteleri Taban Puanları 2026", kimlik hapları (`MapPin` il + üniversite
|
||||
sayısı), özet paragraf, kompakt `CtaSiraForm`, `PagePixelDivider` (seed 71), devlet /
|
||||
vakıf / KKTC-yurt dışı öbekleri (`UniLinkleri`), altta "Diğer şehirler" çipleri ve CTA
|
||||
bandı. Şehir adı `trBaslikDuzeni` + tire sonrası baş harf (Bakü-Azerbaycan).
|
||||
sayısı), özet paragraf; sağda (lg, 24rem sütun) ya da altta (telefon) **şehri temsil eden
|
||||
piksel harita** `SehirPikselHaritasi`. Sonra kompakt `CtaSiraForm`, `PagePixelDivider`
|
||||
(seed 71), devlet / vakıf / KKTC-yurt dışı öbekleri (`UniLinkleri`), "Diğer şehirler"
|
||||
çipleri ve mavi CTA bandı. Şehir adı `trBaslikDuzeni` + tire sonrası baş harf
|
||||
(Bakü-Azerbaycan). Bir üniversite programı olan her ilde sayılır (ODTÜ Ankara + KKTC).
|
||||
- **Şehir piksel haritası** — `sehir-piksel-haritasi.tsx`: **hero'daki dot-matrix Türkiye'nin
|
||||
kendisi** (Bilal, 24 Eyl 2026: ince taramalı ilk sürüm "oyunsu Apple/Duolingo hissine
|
||||
uymuyor" diye geri alındı; seçenekler A/B/C arasından "A, pikseller C'deki gibi iri"
|
||||
seçildi). Izgara 72 sütun × 31 satır (`scripts/il-izgara-uret.ts` →
|
||||
`src/lib/il-izgara-verisi.ts`; her il en az 2×2 kare), kare hücrenin %74'ü, altı farklı
|
||||
opaklık, kıyının altına seyrelen serpinti, `text-primary` zemin, imleç yaklaşınca turuncuya
|
||||
ısınma (`usePixelHeat`). Bulunulan ilin kareleri `fill-orange-500`, hücrenin %88'i kadar
|
||||
iri, 35 ms arayla belirir (`kt-piksel-belir`) ve grup hâlinde nabız atar (`animate-pulse`
|
||||
3.6 s); hareketi azalt açıkken dolu ve durağan. KKTC/yurt dışı: ülke kontürü mavi doku,
|
||||
şehir merkezinde 2×2 hücrelik tek iri turuncu kare; şehri bilinmeyen "KIBRIS" için ülkenin
|
||||
tamamı turuncu. Markup hero'daki gibi deterministik string (`dangerouslySetInnerHTML`).
|
||||
`role="img"` + "İstanbul Türkiye haritasında". Üniversite sayfasının konum kartı
|
||||
(`universite-konum-haritasi.tsx`) aynı bileşeni kullanır. Veri taşıyan pikseldir (§1.2),
|
||||
süs ayraç yasağına girmez.
|
||||
|
||||
## 5. Yerleşim
|
||||
|
||||
@@ -726,11 +750,17 @@ karar.
|
||||
sm:py-5` + ilk çocuk `PikselZemin`), kenarlık saydam kalır ki iki hâlin kutusu aynı olsun.
|
||||
Aynı sayfadaki iki band farklı `seed` alır.
|
||||
|
||||
**Piksel zemin yalnız CTA yüzeylerinde — `PikselZemin`, `src/components/piksel-zemin.tsx`**
|
||||
**Piksel zemin yalnız CTA ve başlık yüzeylerinde — `PikselZemin`, `src/components/piksel-zemin.tsx`**
|
||||
(Bilal, 24 Eyl 2026: önce "her CTA benzeri kısmın arkasında olmalı", sonra "/sonuc'taki kartın
|
||||
arkasına da piksel koymuşsun; pikseller CTA'larda olacak sadece"). CTA yüzeyi = renkli zeminli
|
||||
çağrı yüzeyi: mavi `CtaBandi`, koyu kapanış kartı `PikselCtaYuzeyi`, arama penceresindeki koyu
|
||||
kart. Beyaz içerik kartlarına (tadımlık, `/listem` üretim/kilit kartları, paket kartı, kaydet
|
||||
arkasına da piksel koymuşsun; pikseller CTA'larda olacak sadece"). CTA yüzeyi = çağrı yüzeyi:
|
||||
mavi `CtaBandi`, koyu kapanış kartı `PikselCtaYuzeyi`, arama penceresindeki koyu kart.
|
||||
**Ton ikidir:** `ton="koyu"` (renkli zemin, beyaz kareler) ve `ton="acik"` (beyaz zemin,
|
||||
`text-slate-400` kareler; turuncu vurgu kareleri aynı). Bilal (24 Eyl 2026): "her kısımda mavi
|
||||
arka kullanmışsın, çeşitlendir; beyaz arka planda piksel de güzel görünüyor" — bu yüzden
|
||||
`CtaBandi ton="beyaz"` (slate kenarlık, koyu yazı, paragraf `text-slate-600`) var ve aynı
|
||||
sayfada iki bant üst üste gelmesin diye dönüşümlü kullanılır: `/universiteler` altı ve
|
||||
`/meraklisina` kapanışı beyaz; şehir sayfası altı, `/sonuc` panelleri, çekmece altı mavi.
|
||||
Şehir sayfasının başlık yüzeyi (§4.17) de açık tonlu piksel zemindir. Beyaz içerik kartlarına (tadımlık, `/listem` üretim/kilit kartları, paket kartı, kaydet
|
||||
bandı, katalog üstü kompakt satırın **form** hâli, boş durumlar, sıralama-yok sayfası)
|
||||
**konmaz** — 24 Eyl'de denendi ve geri alındı. Aynı kompakt satırın kayıtlı sıralamalı hâli
|
||||
ise formsuz tek butonlu bir çağrı olduğundan mavi banttır ve dokuyu alır (yukarıda). Kap `relative isolate overflow-hidden`; zemin ilk çocuk; kareler beyaz.
|
||||
|
||||
170
scripts/il-izgara-uret.ts
Normal file
170
scripts/il-izgara-uret.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
// Şehir haritası (DESIGN.md §4.17) için il ve ülke kontürlerini hero'nun
|
||||
// dot-matrix ızgarasına rasterize eder. Hero'daki TurkeyPixelMap 64 sütun;
|
||||
// burada 72 sütun ki her il en az birkaç kare alsın (Yalova, Kilis gibi
|
||||
// küçük iller için 2×2 blok garantisi vardır). Çıktı
|
||||
// src/lib/il-izgara-verisi.ts dosyasına yazılır ve commit'lenir.
|
||||
//
|
||||
// Kaynak: turkey-map-react il path'leri (viewBox 1050×585, üstten 138 kırpık)
|
||||
// ve harita-ulkeler-verisi.ts ülke kontürleri (1050×447). Hücre merkezi
|
||||
// poligonun içindeyse hücre o ile aittir (ray casting; Bézier eğrileri uç
|
||||
// noktalarıyla düzleştirilir — hücre ölçeğinde fark etmez).
|
||||
//
|
||||
// Çalıştırma: pnpm tsx scripts/il-izgara-uret.ts
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { cities } from "turkey-map-react/lib/data";
|
||||
import { normalizeIlAdi, HARITA_UST_KIRPMA, HARITA_GENISLIK, HARITA_YUKSEKLIK } from "../src/lib/harita";
|
||||
import { ULKE_HARITALARI } from "../src/lib/harita-ulkeler-verisi";
|
||||
|
||||
const SUTUN = 72;
|
||||
const HUCRE = HARITA_GENISLIK / SUTUN;
|
||||
const YUKSEKLIK = HARITA_YUKSEKLIK - HARITA_UST_KIRPMA; // 447
|
||||
const SATIR = Math.ceil(YUKSEKLIK / HUCRE);
|
||||
|
||||
type Nokta = [number, number];
|
||||
type Hucre = [number, number];
|
||||
|
||||
function poligonlar(d: string): Nokta[][] {
|
||||
const toks = d.match(/[MLHVCZmlhvcz]|-?\d*\.?\d+(?:e-?\d+)?/g) ?? [];
|
||||
const polys: Nokta[][] = [];
|
||||
let cur: Nokta[] = [];
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let cmd = "";
|
||||
let i = 0;
|
||||
const num = () => Number(toks[i++]);
|
||||
const kapat = () => {
|
||||
if (cur.length > 2) polys.push(cur);
|
||||
cur = [];
|
||||
};
|
||||
while (i < toks.length) {
|
||||
const t = toks[i];
|
||||
if (/[A-Za-z]/.test(t)) {
|
||||
cmd = t;
|
||||
i++;
|
||||
if (cmd === "Z" || cmd === "z") kapat();
|
||||
continue;
|
||||
}
|
||||
switch (cmd) {
|
||||
case "M": x = num(); y = num(); kapat(); cur = [[x, y]]; cmd = "L"; break;
|
||||
case "m": x += num(); y += num(); kapat(); cur = [[x, y]]; cmd = "l"; break;
|
||||
case "L": x = num(); y = num(); cur.push([x, y]); break;
|
||||
case "l": x += num(); y += num(); cur.push([x, y]); break;
|
||||
case "H": x = num(); cur.push([x, y]); break;
|
||||
case "h": x += num(); cur.push([x, y]); break;
|
||||
case "V": y = num(); cur.push([x, y]); break;
|
||||
case "v": y += num(); cur.push([x, y]); break;
|
||||
case "C": num(); num(); num(); num(); x = num(); y = num(); cur.push([x, y]); break;
|
||||
case "c": num(); num(); num(); num(); x += num(); y += num(); cur.push([x, y]); break;
|
||||
default: i++;
|
||||
}
|
||||
}
|
||||
kapat();
|
||||
return polys;
|
||||
}
|
||||
|
||||
function icinde(px: number, py: number, poly: Nokta[]): boolean {
|
||||
let ic = false;
|
||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||
const [xi, yi] = poly[i];
|
||||
const [xj, yj] = poly[j];
|
||||
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) ic = !ic;
|
||||
}
|
||||
return ic;
|
||||
}
|
||||
|
||||
/** Her hücre merkezini ilk eşleşen bölgeye verir; sıra öncelik demektir. */
|
||||
function rasterize(
|
||||
bolgeler: { ad: string; polys: Nokta[][] }[],
|
||||
ustKirpma: number,
|
||||
): Map<string, Hucre[]> {
|
||||
const sonuc = new Map<string, Hucre[]>();
|
||||
for (let r = 0; r < SATIR; r++) {
|
||||
for (let q = 0; q < SUTUN; q++) {
|
||||
const px = (q + 0.5) * HUCRE;
|
||||
const py = ustKirpma + (r + 0.5) * HUCRE;
|
||||
const b = bolgeler.find((b) => b.polys.some((p) => icinde(px, py, p)));
|
||||
if (!b) continue;
|
||||
const liste = sonuc.get(b.ad) ?? [];
|
||||
liste.push([q, r]);
|
||||
sonuc.set(b.ad, liste);
|
||||
}
|
||||
}
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
// ---- Türkiye illeri
|
||||
const iller = cities.map((c) => ({ ad: normalizeIlAdi(c.name), polys: poligonlar(c.path) }));
|
||||
const ilHucre = rasterize(iller, HARITA_UST_KIRPMA);
|
||||
// Küçük il garantisi: 4 hücreden azı olan il bbox merkezinde 2×2 blok alır,
|
||||
// o hücreler komşulardan düşer.
|
||||
for (const il of iller) {
|
||||
const mevcut = ilHucre.get(il.ad) ?? [];
|
||||
if (mevcut.length >= 4) continue;
|
||||
const pts = il.polys.flat();
|
||||
const cx = (Math.min(...pts.map((p) => p[0])) + Math.max(...pts.map((p) => p[0]))) / 2;
|
||||
const cy = (Math.min(...pts.map((p) => p[1])) + Math.max(...pts.map((p) => p[1]))) / 2;
|
||||
const q0 = Math.min(SUTUN - 2, Math.max(0, Math.round(cx / HUCRE) - 1));
|
||||
const r0 = Math.min(SATIR - 2, Math.max(0, Math.round((cy - HARITA_UST_KIRPMA) / HUCRE) - 1));
|
||||
const blok: Hucre[] = [[q0, r0], [q0 + 1, r0], [q0, r0 + 1], [q0 + 1, r0 + 1]];
|
||||
const anahtar = new Set(blok.map(([q, r]) => `${q},${r}`));
|
||||
for (const [ad, liste] of ilHucre) {
|
||||
if (ad === il.ad) continue;
|
||||
ilHucre.set(ad, liste.filter(([q, r]) => !anahtar.has(`${q},${r}`)));
|
||||
}
|
||||
const birlesik = new Map(mevcut.map((h) => [`${h[0]},${h[1]}`, h]));
|
||||
for (const h of blok) birlesik.set(`${h[0]},${h[1]}`, h);
|
||||
ilHucre.set(il.ad, [...birlesik.values()]);
|
||||
}
|
||||
|
||||
// ---- Ülkeler (KKTC, yurt dışı): ülke öncelikli, bağlam ikincil
|
||||
const ulkeHucre: Record<string, { ulke: Hucre[]; baglam: Hucre[] }> = {};
|
||||
for (const [kod, harita] of Object.entries(ULKE_HARITALARI)) {
|
||||
const r = rasterize(
|
||||
[
|
||||
{ ad: "ulke", polys: harita.ulke.flatMap(poligonlar) },
|
||||
{ ad: "baglam", polys: harita.baglam.flatMap(poligonlar) },
|
||||
],
|
||||
0,
|
||||
);
|
||||
ulkeHucre[kod] = { ulke: r.get("ulke") ?? [], baglam: r.get("baglam") ?? [] };
|
||||
}
|
||||
|
||||
const enKucuk = [...ilHucre.entries()].sort((a, b) => a[1].length - b[1].length).slice(0, 5);
|
||||
console.log(
|
||||
`${SUTUN}×${SATIR}, hücre ${HUCRE.toFixed(3)}; kara ${[...ilHucre.values()].reduce((a, b) => a + b.length, 0)}; en küçük: ${enKucuk.map(([a, l]) => `${a}:${l.length}`).join(" ")}`,
|
||||
);
|
||||
|
||||
const satirlar = [...ilHucre.entries()]
|
||||
.sort((a, b) => a[0].localeCompare(b[0], "tr-TR"))
|
||||
.map(([ad, liste]) => ` ${JSON.stringify(ad)}: ${JSON.stringify(liste)},`);
|
||||
const ulkeSatirlar = Object.entries(ulkeHucre).map(
|
||||
([kod, v]) => ` ${JSON.stringify(kod)}: { ulke: ${JSON.stringify(v.ulke)}, baglam: ${JSON.stringify(v.baglam)} },`,
|
||||
);
|
||||
const cikti = `// Bu dosya scripts/il-izgara-uret.ts tarafından üretildi — elle düzenleme.
|
||||
// Hero'nun dot-matrix ızgarasında (${SUTUN} sütun × ${SATIR} satır, hücre ${HUCRE.toFixed(4)}
|
||||
// viewBox birimi) her ilin ve ülkenin kapladığı hücreler [sütun, satır].
|
||||
|
||||
import type { UlkeKodu } from "./harita-ulkeler-verisi";
|
||||
|
||||
export const IL_IZGARA = {
|
||||
sutun: ${SUTUN},
|
||||
satir: ${SATIR},
|
||||
hucre: ${HUCRE.toFixed(4)},
|
||||
} as const;
|
||||
|
||||
export type IzgaraHucre = readonly [number, number];
|
||||
|
||||
/** Anahtar DB biçiminde il adı (normalizeIlAdi). */
|
||||
export const IL_HUCRELERI: Record<string, readonly IzgaraHucre[]> = {
|
||||
${satirlar.join("\n")}
|
||||
};
|
||||
|
||||
export const ULKE_HUCRELERI: Record<UlkeKodu, { ulke: readonly IzgaraHucre[]; baglam: readonly IzgaraHucre[] }> = {
|
||||
${ulkeSatirlar.join("\n")}
|
||||
};
|
||||
`;
|
||||
const hedef = path.join(process.cwd(), "src/lib/il-izgara-verisi.ts");
|
||||
fs.writeFileSync(hedef, cikti);
|
||||
console.log("yazıldı:", hedef, `${(cikti.length / 1024).toFixed(0)} KB`);
|
||||
@@ -322,6 +322,31 @@
|
||||
animation: kt-piksel-nabiz 0.42s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
/* Şehir haritasında (sehir-piksel-haritasi.tsx) ilin kareleri sırayla
|
||||
belirir; her karenin gecikmesi inline (35 ms × sıra). DESIGN.md §4.17. */
|
||||
@keyframes kt-piksel-belir {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.kt-piksel-belir {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kt-piksel-belir {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bildirim (toast, sonner) — DESIGN.md §4.16. Başlık ve açıklama slate
|
||||
tonlarında okunur; aksiyon düğmesi üst çubuktaki koyu hap dilinde.
|
||||
".toaster" öneki şart: sonner kendi stilini çalışma anında enjekte eder,
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||
import { NavBaslik } from "./nav-baslik";
|
||||
import { SehirCipleri, oneCikanIller } from "./sehir-cipleri";
|
||||
import { SehirPikselHaritasi } from "./sehir-piksel-haritasi";
|
||||
import { SehirYuzeyi } from "./sehir-yuzeyi";
|
||||
import { UniLinkleri } from "./universite-listesi";
|
||||
|
||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||
@@ -123,9 +125,16 @@ export function IlIcerik({ slug }: { slug: string }) {
|
||||
</nav>
|
||||
|
||||
<NavBaslik baslikId="il-baslik" ad={`${il.ad} üniversiteleri`} />
|
||||
{/* Başlık yüzeyi: beyaz kart + slate piksel doku, şehrin dot-matrix
|
||||
haritası (DESIGN.md §4.17). lg altında metin ortalı, harita altta. */}
|
||||
<SehirYuzeyi
|
||||
seed={31}
|
||||
harita={<SehirPikselHaritasi il={il.il} ad={il.ad} />}
|
||||
metin={
|
||||
<div className="text-center lg:text-left">
|
||||
<h1
|
||||
id="il-baslik"
|
||||
className="mt-6 text-center font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl lg:text-left"
|
||||
className="font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl"
|
||||
>
|
||||
{il.ad} Üniversiteleri Taban Puanları {BASLIK_YILI}
|
||||
</h1>
|
||||
@@ -146,16 +155,19 @@ export function IlIcerik({ slug }: { slug: string }) {
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<p className="mx-auto mt-6 max-w-2xl text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0">
|
||||
{il.ad}'da {SON_YIL} YKS yerleştirmesinde öğrenci alan{" "}
|
||||
<strong>{sayi(il.uniler.length)} üniversite</strong>
|
||||
{ozetParcalar.length > 1 ? ` (${ozetParcalar.join(", ")})` : ""} ve
|
||||
toplam <strong>{sayi(il.programSayisi)} program</strong> var.
|
||||
{ozetParcalar.length > 1 ? ` (${ozetParcalar.join(", ")})` : ""}{" "}
|
||||
ve toplam <strong>{sayi(il.programSayisi)} program</strong> var.
|
||||
Üniversiteye tıklayınca programlarının taban puanlarını, başarı
|
||||
sıralamalarını ve kontenjanlarını fakülte fakülte görürsün. Veriler
|
||||
YÖK Atlas {SON_YIL} yerleştirme sonuçlarına dayanır; {TERCIH_YILI}{" "}
|
||||
tercihleri için rehber niteliğindedir.
|
||||
sıralamalarını ve kontenjanlarını fakülte fakülte görürsün.
|
||||
Veriler YÖK Atlas {SON_YIL} yerleştirme sonuçlarına dayanır;{" "}
|
||||
{TERCIH_YILI} tercihleri için rehber niteliğindedir.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mt-8">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
@@ -196,12 +208,7 @@ export function IlIcerikSkeleton() {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||
<Skeleton className="mx-auto h-4 w-64 lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-6 h-10 w-full max-w-xl lg:mx-0" />
|
||||
<div className="mt-5 flex justify-center gap-2 lg:justify-start">
|
||||
<Skeleton className="h-10 w-28 rounded-full" />
|
||||
<Skeleton className="h-10 w-32 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="mx-auto mt-6 h-5 w-full max-w-3xl lg:mx-0" />
|
||||
<Skeleton className="mt-6 h-72 w-full rounded-3xl" />
|
||||
<div className="mt-10 space-y-4">
|
||||
<Skeleton className="h-8 w-72" />
|
||||
{Array.from({ length: 6 }, (_, i) => (
|
||||
|
||||
225
src/features/katalog/components/sehir-piksel-haritasi.tsx
Normal file
225
src/features/katalog/components/sehir-piksel-haritasi.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Şehri temsil eden piksel harita (DESIGN.md §4.17) — hero'daki dot-matrix
|
||||
* Türkiye'nin (turkey-pixel-map.tsx) şehir sayfası ve üniversite konum kartı
|
||||
* için genellenmiş hâli. Aynı dil: iri kareler, altı farklı opaklık, kıyının
|
||||
* altına seyrelen serpinti, imleç yaklaşınca turuncuya ısınan pikseller
|
||||
* (usePixelHeat). Fark: ızgara 72 sütun (her il en az 2×2 kare) ve
|
||||
* bulunulan ilin kareleri turuncu, hücrenin %88'i kadar iri (Bilal, 24 Eyl
|
||||
* 2026: "C'deki gibi piksel olabilir"), sırayla belirir ve nabız atar.
|
||||
*
|
||||
* Markup hero'daki gibi deterministik string olarak üretilir ve
|
||||
* dangerouslySetInnerHTML ile basılır: ~1300 rect React elemanı olarak
|
||||
* hydrate edilmez; server ve client aynı çıktıyı verir.
|
||||
*/
|
||||
|
||||
import { useRef } from "react";
|
||||
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||||
import { ilUlkeHaritasi, type UlkeKodu } from "@/lib/harita-ulkeler";
|
||||
import {
|
||||
IL_HUCRELERI,
|
||||
IL_IZGARA,
|
||||
ULKE_HUCRELERI,
|
||||
type IzgaraHucre,
|
||||
} from "@/lib/il-izgara-verisi";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const { sutun: SUTUN, satir: SATIR, hucre: CELL } = IL_IZGARA;
|
||||
const DOT = CELL * 0.74;
|
||||
const INSET = (CELL - DOT) / 2;
|
||||
// Vurgu kareleri iri ve dolu (C seçeneğinin hissi)
|
||||
const DOT_VURGU = CELL * 0.88;
|
||||
const INSET_VURGU = (CELL - DOT_VURGU) / 2;
|
||||
// Güney kıyının altına seyrelerek dağılan serpinti derinliği (hücre)
|
||||
const SERPINTI = 5;
|
||||
const VIEW_W = SUTUN * CELL;
|
||||
const VIEW_H = (SATIR + SERPINTI) * CELL;
|
||||
// Hero'nun ısı fırçası 640 birimlik viewBox'ta 34; burada aynı oran.
|
||||
const ISI_YARICAPI = 34 * (VIEW_W / 640);
|
||||
|
||||
const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
|
||||
const opacityFor = (x: number, y: number) =>
|
||||
OPACITIES[(x * 7 + y * 11) % OPACITIES.length];
|
||||
const VURGU_OPACITIES = [1, 0.9, 0.82, 0.95];
|
||||
const vurguOpacityFor = (x: number, y: number) =>
|
||||
VURGU_OPACITIES[(x * 3 + y * 5) % VURGU_OPACITIES.length];
|
||||
|
||||
// Deterministik sözde-rastgele (hero ile aynı): server ve client aynı deseni üretir
|
||||
const hash = (x: number, y: number, seed = 0) => {
|
||||
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||
return n - Math.floor(n);
|
||||
};
|
||||
|
||||
const f = (n: number) => n.toFixed(2);
|
||||
|
||||
function zeminKare(q: number, r: number, op: number) {
|
||||
return `<rect data-px data-cx="${f(q * CELL + CELL / 2)}" data-cy="${f(r * CELL + CELL / 2)}" data-op="${op}" x="${f(q * CELL + INSET)}" y="${f(r * CELL + INSET)}" width="${f(DOT)}" height="${f(DOT)}" rx="${f(CELL * 0.2)}" fill="currentColor" fill-opacity="${op}"/>`;
|
||||
}
|
||||
|
||||
function vurguKare(q: number, r: number, i: number) {
|
||||
return `<rect class="kt-piksel-belir fill-orange-500" style="animation-delay:${i * 35}ms" x="${f(q * CELL + INSET_VURGU)}" y="${f(r * CELL + INSET_VURGU)}" width="${f(DOT_VURGU)}" height="${f(DOT_VURGU)}" rx="${f(CELL * 0.26)}" fill-opacity="${vurguOpacityFor(q, r)}"/>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zemin (mavi doku) + vurgu (turuncu) hücrelerinden statik SVG içi markup.
|
||||
* `isaret`: ülke haritasında şehir merkezi — 2×2 hücrelik tek iri kare.
|
||||
*/
|
||||
function markupUret(
|
||||
zemin: readonly IzgaraHucre[],
|
||||
vurgu: readonly IzgaraHucre[],
|
||||
isaret: IzgaraHucre | null,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
const vurguSet = new Set(vurgu.map(([q, r]) => `${q},${r}`));
|
||||
const enAlt: number[] = Array(SUTUN).fill(-1);
|
||||
for (const [q, r] of zemin) {
|
||||
if (vurguSet.has(`${q},${r}`)) continue;
|
||||
parts.push(zeminKare(q, r, opacityFor(q, r)));
|
||||
if (r > enAlt[q]) enAlt[q] = r;
|
||||
}
|
||||
for (const [q, r] of vurgu) if (r > enAlt[q]) enAlt[q] = r;
|
||||
// Kıyının altına serpinti (hero ile aynı kural): kıyıdan uzaklaştıkça
|
||||
// seyrelen, derinlikle sönen kareler; ısı fırçasına katılırlar.
|
||||
for (let q = 0; q < SUTUN; q++) {
|
||||
if (enAlt[q] < 0) continue;
|
||||
for (let d = 1; d <= SERPINTI; d++) {
|
||||
const r = enAlt[q] + d;
|
||||
if (r >= SATIR + SERPINTI) break;
|
||||
const density = 0.05 + 0.4 * (1 - (d - 1) / SERPINTI) ** 2;
|
||||
if (hash(q, r) > density) continue;
|
||||
const op = Math.round(opacityFor(q, r) * (1 - d / (SERPINTI + 1)) * 100) / 100;
|
||||
parts.push(zeminKare(q, r, op));
|
||||
}
|
||||
}
|
||||
// Vurgu: sırayla belirir (kt-piksel-belir), grup hâlinde nabız atar
|
||||
parts.push(
|
||||
`<g class="animate-pulse motion-reduce:animate-none" style="animation-duration:3.6s">`,
|
||||
);
|
||||
vurgu.forEach(([q, r], i) => parts.push(vurguKare(q, r, i)));
|
||||
if (isaret) {
|
||||
const [q, r] = isaret;
|
||||
const boy = 2 * CELL - 2 * INSET_VURGU;
|
||||
parts.push(
|
||||
`<rect class="kt-piksel-belir fill-orange-500" x="${f(q * CELL + INSET_VURGU)}" y="${f(r * CELL + INSET_VURGU)}" width="${f(boy)}" height="${f(boy)}" rx="${f(CELL * 0.5)}"/>`,
|
||||
);
|
||||
}
|
||||
parts.push("</g>");
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
const onbellek = new Map<string, string>();
|
||||
function markup(anahtar: string, uret: () => string): string {
|
||||
let m = onbellek.get(anahtar);
|
||||
if (!m) {
|
||||
m = uret();
|
||||
onbellek.set(anahtar, m);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function PikselSvg({
|
||||
html,
|
||||
etiket,
|
||||
className,
|
||||
}: {
|
||||
html: string;
|
||||
etiket: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
usePixelHeat(svgRef, { radius: ISI_YARICAPI });
|
||||
return (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
||||
className={cn("h-auto w-full text-primary", className)}
|
||||
role="img"
|
||||
aria-label={etiket}
|
||||
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const TUM_IL_HUCRELERI = Object.values(IL_HUCRELERI).flat();
|
||||
|
||||
/** Türkiye; `il` (DB biçimi, ör. "İZMİR") turuncu karelerle yanar. */
|
||||
export function TurkiyePikselHaritasi({
|
||||
il,
|
||||
etiket,
|
||||
className,
|
||||
}: {
|
||||
il: string;
|
||||
etiket: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const html = markup(`tr:${il}`, () =>
|
||||
markupUret(TUM_IL_HUCRELERI, IL_HUCRELERI[il] ?? [], null),
|
||||
);
|
||||
return <PikselSvg html={html} etiket={etiket} className={className} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ülke (KKTC, yurt dışı). Şehir konumu verilirse ülke mavi kalır ve şehirde
|
||||
* 2×2 hücrelik iri turuncu kare durur; verilmezse ülkenin tamamı turuncu.
|
||||
*/
|
||||
export function UlkePikselHaritasi({
|
||||
ulke,
|
||||
sehir,
|
||||
etiket,
|
||||
className,
|
||||
}: {
|
||||
ulke: UlkeKodu;
|
||||
/** viewBox koordinatı (harita-ulkeler'deki proje ile) */
|
||||
sehir: { x: number; y: number } | null;
|
||||
etiket: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const html = markup(
|
||||
`ulke:${ulke}:${sehir ? `${Math.round(sehir.x)},${Math.round(sehir.y)}` : "-"}`,
|
||||
() => {
|
||||
const h = ULKE_HUCRELERI[ulke];
|
||||
if (sehir) {
|
||||
const q = Math.min(SUTUN - 2, Math.max(0, Math.round(sehir.x / CELL) - 1));
|
||||
const r = Math.min(SATIR - 2, Math.max(0, Math.round(sehir.y / CELL) - 1));
|
||||
return markupUret([...h.baglam, ...h.ulke], [], [q, r]);
|
||||
}
|
||||
return markupUret(h.baglam, h.ulke, null);
|
||||
},
|
||||
);
|
||||
return <PikselSvg html={html} etiket={etiket} className={className} />;
|
||||
}
|
||||
|
||||
/** Şehir sayfası: il değerinden Türkiye ya da ülke haritasını seçer. */
|
||||
export function SehirPikselHaritasi({
|
||||
il,
|
||||
ad,
|
||||
className,
|
||||
}: {
|
||||
/** DB biçiminde il ("İSTANBUL", "LEFKOŞA", "KIBRIS") */
|
||||
il: string;
|
||||
/** Görünen ad (etiket için) */
|
||||
ad: string;
|
||||
className?: string;
|
||||
}) {
|
||||
if (IL_HUCRELERI[il]) {
|
||||
return (
|
||||
<TurkiyePikselHaritasi
|
||||
il={il}
|
||||
etiket={`${ad} Türkiye haritasında`}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const ulke = ilUlkeHaritasi(il);
|
||||
if (!ulke) return null;
|
||||
return (
|
||||
<UlkePikselHaritasi
|
||||
ulke={ulke.ulke}
|
||||
sehir={ulke.sehir ? { x: ulke.sehir.x, y: ulke.sehir.y } : null}
|
||||
etiket={`${ad} ${ulke.ulkeAd} haritasında`}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
35
src/features/katalog/components/sehir-yuzeyi.tsx
Normal file
35
src/features/katalog/components/sehir-yuzeyi.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, type ReactNode } from "react";
|
||||
import { PikselZemin } from "@/components/piksel-zemin";
|
||||
|
||||
/**
|
||||
* Şehir sayfasının başlık yüzeyi (DESIGN.md §4.17): beyaz kart, slate
|
||||
* piksel doku, sağda (lg) ya da altta (telefon) şehrin dot-matrix haritası.
|
||||
* Doku ile harita üst üste binmez: harita kabı `haric` ile zemine verilir
|
||||
* (§10.8 kuralı). Yalnız ref için client; metin ve harita sunucudan gelir.
|
||||
*/
|
||||
export function SehirYuzeyi({
|
||||
metin,
|
||||
harita,
|
||||
seed = 31,
|
||||
}: {
|
||||
metin: ReactNode;
|
||||
harita: ReactNode;
|
||||
seed?: number;
|
||||
}) {
|
||||
const haritaRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<section className="relative isolate mt-6 overflow-hidden rounded-3xl border border-slate-200 bg-white p-6 sm:p-8">
|
||||
<PikselZemin ton="acik" seed={seed} haric={haritaRef} />
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,24rem)] lg:items-center lg:gap-10">
|
||||
<div>{metin}</div>
|
||||
{harita ? (
|
||||
<div ref={haritaRef} className="mx-auto w-full max-w-md lg:max-w-none">
|
||||
{harita}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import { normalizeIlAdi } from "@/lib/harita";
|
||||
import { uniUlkeKonum, yurtdisiUniKonum } from "@/lib/harita-ulkeler";
|
||||
import { cities } from "turkey-map-react/lib/data";
|
||||
import {
|
||||
HARITA_GENISLIK,
|
||||
HARITA_UST_KIRPMA,
|
||||
HARITA_YUKSEKLIK,
|
||||
normalizeIlAdi,
|
||||
} from "@/lib/harita";
|
||||
import {
|
||||
ULKE_HARITA_GENISLIK,
|
||||
ULKE_HARITA_YUKSEKLIK,
|
||||
uniUlkeKonum,
|
||||
type UlkeKonum,
|
||||
} from "@/lib/harita-ulkeler";
|
||||
TurkiyePikselHaritasi,
|
||||
UlkePikselHaritasi,
|
||||
} from "./sehir-piksel-haritasi";
|
||||
|
||||
const SECILI_DOLGU = "oklch(0.809 0.105 251.8)";
|
||||
const ZEMIN_DOLGU = "oklch(0.929 0.013 255.5)";
|
||||
// Harita il adları modül yükünde bir kez DB biçimine çevrilir.
|
||||
const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name)));
|
||||
|
||||
/**
|
||||
* Üniversite sayfasındaki konum kartı: şehir sayfasıyla aynı dot-matrix
|
||||
* harita (DESIGN.md §4.17; Bilal, 24 Eyl 2026: "bu kart kısmına piksel
|
||||
* dokuları ekle"). Türkiye'de il turuncu noktalarla yanar; KKTC/yurt dışında
|
||||
* ülke kontürü + kampüs şehrinde iri turuncu piksel.
|
||||
*/
|
||||
export function UniversiteKonumHaritasi({
|
||||
universite,
|
||||
il,
|
||||
@@ -24,9 +24,15 @@ export function UniversiteKonumHaritasi({
|
||||
}) {
|
||||
const yurtdisi = uniUlkeKonum(universite, il);
|
||||
if (yurtdisi) {
|
||||
const konum = yurtdisiUniKonum(universite, il);
|
||||
return (
|
||||
<KonumBolumu universite={universite}>
|
||||
<UlkeHaritasi universite={universite} konum={yurtdisi} />
|
||||
<UlkePikselHaritasi
|
||||
ulke={yurtdisi.ulke}
|
||||
sehir={konum ? { x: konum.x, y: konum.y } : null}
|
||||
etiket={`${universite} konumunun ${yurtdisi.ulkeAd} haritasındaki görünümü${yurtdisi.sehir ? ` (${yurtdisi.sehir})` : ""}`}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
<p className="pointer-events-none absolute bottom-0 left-0 text-xs font-medium text-slate-500">
|
||||
{yurtdisi.ulkeAd}
|
||||
{yurtdisi.sehir ? ` · ${yurtdisi.sehir}` : ""}
|
||||
@@ -35,31 +41,15 @@ export function UniversiteKonumHaritasi({
|
||||
);
|
||||
}
|
||||
|
||||
if (!il) return null;
|
||||
if (!il || !HARITA_ILLERI.has(il)) return null;
|
||||
|
||||
return (
|
||||
<KonumBolumu universite={universite}>
|
||||
<svg
|
||||
viewBox={`0 ${HARITA_UST_KIRPMA} ${HARITA_GENISLIK} ${HARITA_YUKSEKLIK - HARITA_UST_KIRPMA}`}
|
||||
<TurkiyePikselHaritasi
|
||||
il={il}
|
||||
etiket={`${universite} konumunun Türkiye haritasındaki görünümü`}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
role="img"
|
||||
aria-label={`${universite} konumunun Türkiye haritasındaki görünümü`}
|
||||
>
|
||||
{cities.map((city) => {
|
||||
const secili = il ? normalizeIlAdi(city.name) === il : false;
|
||||
return (
|
||||
<path
|
||||
key={city.id}
|
||||
d={city.path}
|
||||
fill={secili ? SECILI_DOLGU : ZEMIN_DOLGU}
|
||||
stroke="white"
|
||||
strokeWidth={secili ? 1.8 : 1}
|
||||
>
|
||||
<title>{city.name}</title>
|
||||
</path>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
/>
|
||||
</KonumBolumu>
|
||||
);
|
||||
}
|
||||
@@ -83,30 +73,3 @@ function KonumBolumu({
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function UlkeHaritasi({
|
||||
universite,
|
||||
konum,
|
||||
}: {
|
||||
universite: string;
|
||||
konum: UlkeKonum;
|
||||
}) {
|
||||
const { harita, ulkeAd, sehir } = konum;
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${ULKE_HARITA_GENISLIK} ${ULKE_HARITA_YUKSEKLIK}`}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
role="img"
|
||||
aria-label={`${universite} konumunun ${ulkeAd} haritasındaki görünümü${sehir ? ` (${sehir})` : ""}`}
|
||||
>
|
||||
{harita.baglam.map((d, i) => (
|
||||
<path key={`b${i}`} d={d} fill={ZEMIN_DOLGU} stroke="white" strokeWidth={1} />
|
||||
))}
|
||||
{harita.ulke.map((d, i) => (
|
||||
<path key={`u${i}`} d={d} fill={SECILI_DOLGU} stroke="white" strokeWidth={1.8}>
|
||||
<title>{ulkeAd}</title>
|
||||
</path>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
108
src/lib/il-izgara-verisi.ts
Normal file
108
src/lib/il-izgara-verisi.ts
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user