v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
3 changed files with 62 additions and 26 deletions
Showing only changes of commit 2ec9ecdc17 - Show all commits

View File

@@ -661,8 +661,14 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
gelince başlığın altında üç logo karesi (`data-logo` kabı, `relative mx-auto h-16 w-16`) gelince başlığın altında üç logo karesi (`data-logo` kabı, `relative mx-auto h-16 w-16`)
belirir, kısa durur, sönerek kaybolur (scale 0.6 + opacity 0; kap sıfır boya iner), içerik belirir, kısa durur, sönerek kaybolur (scale 0.6 + opacity 0; kap sıfır boya iner), içerik
blokları (`data-sonra`: künye, kontenjan türleri, netler, koşullar, istihdam, üniversite, blokları (`data-sonra`: künye, kontenjan türleri, netler, koşullar, istihdam, üniversite,
kaynak) 0,08 s arayla bulanıktan netleşir; toplam ≈ 1 s. Hareketi azalt: logo yok, 0,25 s kaynak) 0,05 s arayla bulanıktan netleşir; toplam ≈ 1,2 s (kareler 0,1–0,2 s'de belirir,
solma (§6). Akademik göstergeler çekmecesi (§4.9) aynı bileşenin kartlı modunu kullanır. 0,94 s'de sönmüş olur; ilk blok ≈ 0,95 s'de, sonuncusu ≈ 1,3 s'de net — 8 Eki 2026, önceki
0,08'lik aralık ≈ 2 s tutuyordu). Sahne yalnız veri ilk geldiğinde (`hazir` false → true)
oynar; önbellekli yeniden açılışta sahne atlanır, içerik 0,25 s solmayla gelir — gövde hiçbir
an boş kalmaz (iskelet ya da içerik). Hareketi azalt: logo yok, 0,25 s solma (§6). Akademik
göstergeler çekmecesi (§4.9) aynı bileşenin kartlı modunu kullanır (her açılışta oynar).
Gövde kaydırılınca içerik tutamaç ve çarpının altına maskesiz girmesin diye `AltSayfaGovde`
üst kenarında 20 px saydam → dolu `mask-image` var (§4.10; tüm çekmeceler).
### 4.20 Danışman sohbeti — `sohbet-client.tsx`, `arac-pilleri.tsx` ### 4.20 Danışman sohbeti — `sohbet-client.tsx`, `arac-pilleri.tsx`
- **Soru formu hazırlanırken** (7 Eki 2026): `soru_sor` aracı tamamlanana dek cevap iskeleti değil - **Soru formu hazırlanırken** (7 Eki 2026): `soru_sor` aracı tamamlanana dek cevap iskeleti değil

View File

@@ -10,7 +10,11 @@
// `[data-logo]` kabındaki üç `[data-logo-kare]` belirir, kısa durur, sonra // `[data-logo]` kabındaki üç `[data-logo-kare]` belirir, kısa durur, sonra
// sönerek kaybolur (scale 0.6 + opacity 0; Bilal, 7 Eki 2026) ve // sönerek kaybolur (scale 0.6 + opacity 0; Bilal, 7 Eki 2026) ve
// `[data-sonra]` bloklar netleşerek gelir. // `[data-sonra]` bloklar netleşerek gelir.
// "Hareketi azalt"ta logo ve büyüme yok, yalnız 0,25 s solma (§6). // "Hareketi azalt"ta logo ve büyüme yok, yalnız 0,25 s solma (§6). Veri
// bekleyen çekmecede (`hazir` verilmiş) sahne yalnız veri ilk geldiğinde
// (false → true) oynar; önbellekli yeniden açılışta aynı 0,25 s solma
// (8 Eki 2026: ikinci açılışta kareler çekmece ekrana kaymadan oynuyor,
// gövde ~1 s boş kalıyordu).
import { useLayoutEffect, useRef, type ReactNode } from "react"; import { useLayoutEffect, useRef, type ReactNode } from "react";
import { animate, useReducedMotion } from "motion/react"; import { animate, useReducedMotion } from "motion/react";
@@ -36,15 +40,25 @@ const ICERIK_BASI = 0.95;
const GUCLU_CIKIS = [0.23, 1, 0.32, 1] as const; const GUCLU_CIKIS = [0.23, 1, 0.32, 1] as const;
const YERLESME = [0.32, 0.72, 0, 1] as const; const YERLESME = [0.32, 0.72, 0, 1] as const;
// Kartsız mod: logo karesi belir → dur → sön. Belirme ve duruş kartlı // Kartsız mod (saniye): logo karesi belir → dur → sön; kareler 0,05 s arayla
// modla aynı anlara denk gelir (KARE_SURE·0.24, ·0.52); sönme 0,4 s. // gelir, içerik blokları sönmeyle çaprazlanarak 0,05 s arayla netleşir.
const LOGO_SURE = KARE_SURE * KARE_ZAMANLARI[2] + 0.4; // Toplam ≈ 1,2 s: son logo karesi 0,2'de başlar, sönme 0,94'te biter, ilk
// blok ≈ 0,95'te net, 7–8 blokla sonuncusu ≈ 1,3'te (8 Eki 2026; önceki
// 0,08'lik aralıklarla ≈ 2 s sürüyor, DESIGN.md §4.19 "≈ 1 s" ile çelişiyordu).
const LOGO_BELIRME = 0.28;
const LOGO_DURUS = 0.16;
const LOGO_SONME = 0.3;
const LOGO_SURE = LOGO_BELIRME + LOGO_DURUS + LOGO_SONME;
const LOGO_ZAMANLARI = [ const LOGO_ZAMANLARI = [
0, 0,
(KARE_SURE * KARE_ZAMANLARI[1]) / LOGO_SURE, LOGO_BELIRME / LOGO_SURE,
(KARE_SURE * KARE_ZAMANLARI[2]) / LOGO_SURE, (LOGO_BELIRME + LOGO_DURUS) / LOGO_SURE,
1, 1,
]; ];
const KARTSIZ_SLOT_ADIMI = 0.05;
const KARTSIZ_ICERIK_BASI = 0.6;
const KARTSIZ_BLOK_ADIMI = 0.05;
const KARTSIZ_NETLESME = 0.35;
/** Kartsız modun logo kareleri, belirme sırasıyla (`data-logo-kare` değerleri). */ /** Kartsız modun logo kareleri, belirme sırasıyla (`data-logo-kare` değerleri). */
export const LOGO_SLOTLARI = SLOT_SIRASI; export const LOGO_SLOTLARI = SLOT_SIRASI;
@@ -80,13 +94,16 @@ export function logoKareStili(slot: Slot, kapBoyu = 64) {
* Kareler yalnız transform ile oynar (yerleşim ölçülür, bozulmaz); içerik o * Kareler yalnız transform ile oynar (yerleşim ölçülür, bozulmaz); içerik o
* sırada görünmez olduğu için kare ölçeklenirken yazı ezilmez. * sırada görünmez olduğu için kare ölçeklenirken yazı ezilmez.
* *
* `hazir`: false iken sahne oynamaz (iskelet görünür kalır), true olunca * `hazir`: veri bekleyen çekmece için. false iken sahne oynamaz (iskelet
* oynar — veri bekleyen çekmece için. Varsayılan true (içerik sunucudan * görünür kalır); sahne yalnız false → true geçişinde oynar. Bileşen
* hazır gelen çekmece). * `hazir={true}` ile monte olursa veri önbellektedir (çekmece yeniden
* açıldı): sahne atlanır, içerik 0,25 s solmayla gelir — kareler o sırada
* çekmece daha ekrana kaymadan oynuyor, gövde boş kalıyordu. Verilmezse
* (içerik sunucudan hazır gelen çekmece) sahne her montajda oynar.
*/ */
export function AcilisSahnesi({ export function AcilisSahnesi({
children, children,
hazir = true, hazir,
className = "mt-5", className = "mt-5",
}: { }: {
children: ReactNode; children: ReactNode;
@@ -95,12 +112,16 @@ export function AcilisSahnesi({
}) { }) {
const kapRef = useRef<HTMLDivElement>(null); const kapRef = useRef<HTMLDivElement>(null);
const azHareket = useReducedMotion(); const azHareket = useReducedMotion();
// Montajda veri hazır mıydı? (ref: Strict Mode'un çift effect'inde de
// aynı kalır; yeniden montajda — çekmece yeniden açılınca — yeniden ölçülür)
const onbellekli = useRef(hazir === true);
const oynat = hazir ?? true;
useLayoutEffect(() => { useLayoutEffect(() => {
const kap = kapRef.current; const kap = kapRef.current;
// hazir false → true tek geçiş: sahne o geçişte bir kez oynar (ref // hazir false → true tek geçiş: sahne o geçişte bir kez oynar (ref
// tutulmaz; Strict Mode'un çift effect'i temizlik sonrası yeniden kurar). // tutulmaz; Strict Mode'un çift effect'i temizlik sonrası yeniden kurar).
if (!kap || !hazir) return; if (!kap || !oynat) return;
const sahne = kap.querySelector<HTMLElement>("[data-sahne]"); const sahne = kap.querySelector<HTMLElement>("[data-sahne]");
const logo = kap.querySelector<HTMLElement>("[data-logo]"); const logo = kap.querySelector<HTMLElement>("[data-logo]");
const kareler = Array.from( const kareler = Array.from(
@@ -117,8 +138,10 @@ export function AcilisSahnesi({
const kartli = !!sahne && kareler.length > 0; const kartli = !!sahne && kareler.length > 0;
const kartsiz = !kartli && !!logo && logoKareleri.length > 0; const kartsiz = !kartli && !!logo && logoKareleri.length > 0;
if (azHareket || (!kartli && !kartsiz)) { if (azHareket || onbellekli.current || (!kartli && !kartsiz)) {
// Logo kabı sahnesiz anlamsız: yer kaplamasın. // Sahnesiz yol (hareketi azalt, önbellekli açılış): içerik solarak
// gelir, gövde hiçbir an boş kalmaz. Logo kabı sahnesiz anlamsız: yer
// kaplamasın.
if (logo) { if (logo) {
logo.style.display = "none"; logo.style.display = "none";
dokunulan.push(logo); dokunulan.push(logo);
@@ -133,7 +156,7 @@ export function AcilisSahnesi({
} }
// Bulanıklıktan netleşerek, hafifçe aşağıdan gelen satır. // Bulanıklıktan netleşerek, hafifçe aşağıdan gelen satır.
const netlestir = (el: HTMLElement, gecikme: number) => { const netlestir = (el: HTMLElement, gecikme: number, sure = 0.5) => {
el.style.opacity = "0"; el.style.opacity = "0";
dokunulan.push(el); dokunulan.push(el);
const k = animate( const k = animate(
@@ -143,7 +166,7 @@ export function AcilisSahnesi({
filter: ["blur(10px)", "blur(0px)"], filter: ["blur(10px)", "blur(0px)"],
y: [10, 0], y: [10, 0],
}, },
{ duration: 0.5, delay: gecikme, ease: GUCLU_CIKIS }, { duration: sure, delay: gecikme, ease: GUCLU_CIKIS },
); );
k.then(() => { k.then(() => {
el.style.filter = ""; el.style.filter = "";
@@ -168,11 +191,12 @@ export function AcilisSahnesi({
if (kartsiz) { if (kartsiz) {
// Kartsız: logo kareleri belirir, durur, söner; kap sönerken sıfır boya // Kartsız: logo kareleri belirir, durur, söner; kap sönerken sıfır boya
// iner ki altında boşluk kalmasın; içerik ICERIK_BASI'ndan itibaren gelir. // iner ki altında boşluk kalmasın; içerik KARTSIZ_ICERIK_BASI'ndan
// itibaren, sönmeyle çaprazlanarak gelir.
let sonKare = 0; let sonKare = 0;
logoKareleri.forEach((el) => { logoKareleri.forEach((el) => {
const slot = (el.dataset.logoKare ?? "sol") as Slot; const slot = (el.dataset.logoKare ?? "sol") as Slot;
const gecikme = BASLANGIC + SLOT_SIRASI.indexOf(slot) * SLOT_ADIMI; const gecikme = BASLANGIC + SLOT_SIRASI.indexOf(slot) * KARTSIZ_SLOT_ADIMI;
sonKare = Math.max(sonKare, gecikme); sonKare = Math.max(sonKare, gecikme);
el.style.opacity = "0"; el.style.opacity = "0";
dokunulan.push(el); dokunulan.push(el);
@@ -197,14 +221,16 @@ export function AcilisSahnesi({
const kapat = animate( const kapat = animate(
logo!, logo!,
{ height: [logo!.offsetHeight, 0] }, { height: [logo!.offsetHeight, 0] },
{ duration: 0.4, delay: sonmeBasi, ease: YERLESME }, { duration: LOGO_SONME, delay: sonmeBasi, ease: YERLESME },
); );
kapat.then(() => { kapat.then(() => {
logo!.style.display = "none"; logo!.style.display = "none";
}); });
kontroller.push(kapat); kontroller.push(kapat);
sonra.forEach((el, i) => netlestir(el, ICERIK_BASI + i * 0.08)); sonra.forEach((el, i) =>
netlestir(el, KARTSIZ_ICERIK_BASI + i * KARTSIZ_BLOK_ADIMI, KARTSIZ_NETLESME),
);
return temizle; return temizle;
} }
@@ -298,7 +324,7 @@ export function AcilisSahnesi({
); );
return temizle; return temizle;
}, [azHareket, hazir]); }, [azHareket, oynat]);
return ( return (
<div ref={kapRef} className={className}> <div ref={kapRef} className={className}>

View File

@@ -7,8 +7,9 @@
// türleri, son yerleşenin netleri, yerleşme koşulları, TÜİK mezun istihdamı, // türleri, son yerleşenin netleri, yerleşme koşulları, TÜİK mezun istihdamı,
// üniversite özeti. API'nin `gecmis` (yıllara göre sıra/puan/kontenjan) // üniversite özeti. API'nin `gecmis` (yıllara göre sıra/puan/kontenjan)
// alanı burada gösterilmez (Bilal, 7 Eki 2026: yıl tablosu kaldırıldı; // alanı burada gösterilmez (Bilal, 7 Eki 2026: yıl tablosu kaldırıldı;
// backlog). Açılış: veri gelince logo kareleri belirir, söner, içerik // backlog). Açılış: veri ilk gelince logo kareleri belirir, söner, içerik
// bloklar hâlinde netleşir (AcilisSahnesi kartsız modu). // bloklar hâlinde netleşir (AcilisSahnesi kartsız modu); veri önbellekteyken
// (çekmece yeniden açıldı) sahne yok, içerik 0,25 s solmayla gelir.
// Kadın-erkek dağılımı program düzeyinde bilinçli olarak yok: YÖK Atlas 2026 // Kadın-erkek dağılımı program düzeyinde bilinçli olarak yok: YÖK Atlas 2026
// SPA'sına geçerken cinsiyet panelini yayından kaldırdı (bkz. scripts/detay.ts). // SPA'sına geçerken cinsiyet panelini yayından kaldırdı (bkz. scripts/detay.ts).
// Çekmece içerik ağırlıklı olduğu için liste çekmecesi gibi kaydırılır (§4.10 // Çekmece içerik ağırlıklı olduğu için liste çekmecesi gibi kaydırılır (§4.10
@@ -226,7 +227,7 @@ function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi |
); );
} }
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo söner, bloklar // Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo söner, bloklar
// yukarıdan aşağıya 0,08 s arayla netleşir. // yukarıdan aşağıya 0,05 s arayla netleşir.
return ( return (
<> <>
{/* Logo kareleri: sahne sırasında görünür, sönünce kap sıfır boya iner. */} {/* Logo kareleri: sahne sırasında görünür, sönünce kap sıfır boya iner. */}
@@ -473,12 +474,15 @@ function Istihdam({ veri }: { veri: ProgramAyrintisi }) {
v.kazanc ? `kazanç grubu ${v.kazanc.toLocaleLowerCase("tr-TR")}` : null, v.kazanc ? `kazanç grubu ${v.kazanc.toLocaleLowerCase("tr-TR")}` : null,
].filter(Boolean); ].filter(Boolean);
if (parcalar.length === 0) return null; if (parcalar.length === 0) return null;
// Cümle başı büyük (§10.4); hangi parçanın başa geldiği veriye bağlı.
const birlesik = parcalar.join(", ");
const cumle = birlesik.charAt(0).toLocaleUpperCase("tr-TR") + birlesik.slice(1);
const ort = v.ortalama.istihdam != null ? ` Türkiye ortalaması %${ondalik(v.ortalama.istihdam)}.` : ""; const ort = v.ortalama.istihdam != null ? ` Türkiye ortalaması %${ondalik(v.ortalama.istihdam)}.` : "";
return ( return (
<div data-sonra> <div data-sonra>
<h3 className={BOLUM_BASLIK}>Mezun istihdamı</h3> <h3 className={BOLUM_BASLIK}>Mezun istihdamı</h3>
<p className={NOT}> <p className={NOT}>
{parcalar.join(", ")} (TÜİK {v.yil}, ülke geneli, {v.duzey === "lisans" ? "lisans" : "ön lisans"}{" "} {cumle} (TÜİK {v.yil}, ülke geneli, {v.duzey === "lisans" ? "lisans" : "ön lisans"}{" "}
mezunları; üniversiteye göre değişir, kırılım yok).{ort} mezunları; üniversiteye göre değişir, kırılım yok).{ort}
</p> </p>
</div> </div>