Akademik göstergeler çekmecesine açılış koreografisi
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Kareler önce logonun üç karesi gibi belirir, sonra yerlerine büyür; içerik satır satır netleşir. Hareketi azalt'ta yalnız kısa belirme. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useLayoutEffect, useRef, type ReactNode } from "react";
|
||||
import { animate, useReducedMotion } from "motion/react";
|
||||
import { ChevronRight, GraduationCap, X } from "lucide-react";
|
||||
import {
|
||||
Drawer,
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
* kartlarda, ayrıntı bir dokunuş derinde. İkinci satır içeride ne olduğunu
|
||||
* önceden söyler. Ayrıntı alttan açılan çekmecede; çekmece ve kapatma
|
||||
* çarpısı cta-sira-form.tsx'teki çekmeceyle birebir aynı sınıf setinde.
|
||||
* Kartlar sunucuda üretilip `children` olarak gelir.
|
||||
* Kartlar sunucuda üretilip `children` olarak gelir; açılış koreografisini
|
||||
* AcilisSahnesi oynatır.
|
||||
*/
|
||||
export function AkademikAyrinti({
|
||||
universite,
|
||||
@@ -64,9 +66,195 @@ export function AkademikAyrinti({
|
||||
<DrawerDescription className="mt-1 px-10 text-center text-sm text-slate-500">
|
||||
{universite}
|
||||
</DrawerDescription>
|
||||
<div className="mt-5">{children}</div>
|
||||
<AcilisSahnesi>{children}</AcilisSahnesi>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
// Logodaki üç kare (public/kolay-tercih-mark.svg, 180'lik viewBox): solda
|
||||
// mavi, sağ üstte şeftali, sağ altta turuncu. animasyonlu-mark.tsx ile aynı.
|
||||
const LOGO = {
|
||||
sol: { x: 27, y: 56, boyut: 57, renk: "#3b82f6" },
|
||||
"sag-ust": { x: 91, y: 29, boyut: 56, renk: "#fdc7a7" },
|
||||
"sag-alt": { x: 91, y: 95, boyut: 56, renk: "#ff5a00" },
|
||||
} as const;
|
||||
const LOGO_RX = 13;
|
||||
const SLOT_SIRASI = ["sol", "sag-ust", "sag-alt"] as const;
|
||||
|
||||
// Saniye. Kare: belir (logo) → bekle → yerine büyü; içerik kare oturunca.
|
||||
const BASLANGIC = 0.1;
|
||||
const SLOT_ADIMI = 0.08;
|
||||
const KARE_SURE = 1.15;
|
||||
const KARE_ZAMANLARI = [0, 0.24, 0.52, 1];
|
||||
const ICERIK_BASI = 0.95;
|
||||
const GUCLU_CIKIS = [0.23, 1, 0.32, 1] as const;
|
||||
const YERLESME = [0.32, 0.72, 0, 1] as const;
|
||||
|
||||
/**
|
||||
* Çekmece açılınca oynayan sahne: renkli kareler önce ortada logonun üç
|
||||
* karesi gibi belirir, sonra kendi yerlerine ve boylarına büyür (renk logo
|
||||
* renginden kendi rengine döner), en son içleri satır satır bulanıklıktan
|
||||
* netleşerek dolar; ardından alttaki not, buton ve kaynaklar gelir.
|
||||
*
|
||||
* Sunucu işaretlemesiyle sözleşme (universite-icerik.tsx, AkademikGostergeler):
|
||||
* `data-sahne` kareleri saran `relative` kap, `data-kare="sol|sag-ust|sag-alt"`
|
||||
* karenin logodaki yeri (aynı yeri paylaşan kareler üst üste başlar, sonra
|
||||
* ayrışır), `data-sonra` karelerden sonra gelen bloklar.
|
||||
*
|
||||
* 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. "Hareketi
|
||||
* azalt"ta logo ve büyüme yok, yalnız kısa belirme (DESIGN.md §6).
|
||||
*/
|
||||
function AcilisSahnesi({ children }: { children: ReactNode }) {
|
||||
const kapRef = useRef<HTMLDivElement>(null);
|
||||
const azHareket = useReducedMotion();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const kap = kapRef.current;
|
||||
if (!kap) return;
|
||||
const sahne = kap.querySelector<HTMLElement>("[data-sahne]");
|
||||
const kareler = Array.from(
|
||||
kap.querySelectorAll<HTMLElement>("[data-kare]"),
|
||||
);
|
||||
const sonra = Array.from(kap.querySelectorAll<HTMLElement>("[data-sonra]"));
|
||||
const kontroller: { stop: () => void }[] = [];
|
||||
const sayaclar: number[] = [];
|
||||
const dokunulan: HTMLElement[] = [];
|
||||
|
||||
if (azHareket || !sahne || kareler.length === 0) {
|
||||
kontroller.push(
|
||||
animate(kap, { opacity: [0, 1] }, { duration: 0.25, ease: "easeOut" }),
|
||||
);
|
||||
return () => kontroller.forEach((k) => k.stop());
|
||||
}
|
||||
|
||||
// Bulanıklıktan netleşerek, hafifçe aşağıdan gelen satır.
|
||||
const netlestir = (el: HTMLElement, gecikme: number) => {
|
||||
el.style.opacity = "0";
|
||||
dokunulan.push(el);
|
||||
const k = animate(
|
||||
el,
|
||||
{
|
||||
opacity: [0, 1],
|
||||
filter: ["blur(10px)", "blur(0px)"],
|
||||
y: [10, 0],
|
||||
},
|
||||
{ duration: 0.5, delay: gecikme, ease: GUCLU_CIKIS },
|
||||
);
|
||||
k.then(() => {
|
||||
el.style.filter = "";
|
||||
});
|
||||
kontroller.push(k);
|
||||
};
|
||||
|
||||
// Logo, kare alanının ortasında; 180'lik viewBox'ın ekrandaki boyu.
|
||||
const W = sahne.offsetWidth;
|
||||
const H = sahne.offsetHeight;
|
||||
const logoBoy = Math.min(W * 0.5, 200);
|
||||
const olcek = logoBoy / 180;
|
||||
const logoX = (W - logoBoy) / 2;
|
||||
const logoY = (H - logoBoy) / 2;
|
||||
|
||||
let sonKare = 0;
|
||||
kareler.forEach((el, i) => {
|
||||
const slot = LOGO[el.dataset.kare as keyof typeof LOGO]
|
||||
? el.dataset.kare!
|
||||
: "sol";
|
||||
const logo = LOGO[slot as keyof typeof LOGO];
|
||||
// offset*: transform'dan etkilenmez (çekmece o sırada kayıyor).
|
||||
let sol = 0;
|
||||
let ust = 0;
|
||||
for (
|
||||
let d: HTMLElement | null = el;
|
||||
d && d !== sahne;
|
||||
d = d.offsetParent as HTMLElement | null
|
||||
) {
|
||||
sol += d.offsetLeft;
|
||||
ust += d.offsetTop;
|
||||
}
|
||||
const w = el.offsetWidth;
|
||||
const h = el.offsetHeight;
|
||||
const boy = logo.boyut * olcek;
|
||||
const sx = boy / w;
|
||||
const sy = boy / h;
|
||||
const dx = logoX + logo.x * olcek + boy / 2 - (sol + w / 2);
|
||||
const dy = logoY + logo.y * olcek + boy / 2 - (ust + h / 2);
|
||||
// Ölçek köşeyi de küçültür; logodaki köşe oranı için ters ölçekli elips.
|
||||
const rx = (LOGO_RX * olcek) / sx;
|
||||
const ry = (LOGO_RX * olcek) / sy;
|
||||
const logoKose = `${rx}px / ${ry}px`;
|
||||
const kose = getComputedStyle(el).borderTopLeftRadius;
|
||||
const gecikme =
|
||||
BASLANGIC +
|
||||
SLOT_SIRASI.indexOf(slot as (typeof SLOT_SIRASI)[number]) * SLOT_ADIMI;
|
||||
sonKare = Math.max(sonKare, gecikme);
|
||||
|
||||
el.style.opacity = "0";
|
||||
el.style.backgroundColor = logo.renk;
|
||||
dokunulan.push(el);
|
||||
const k = animate(
|
||||
el,
|
||||
{
|
||||
opacity: [0, 1, 1, 1],
|
||||
x: [dx, dx, dx, 0],
|
||||
y: [dy, dy, dy, 0],
|
||||
scaleX: [sx * 0.4, sx, sx, 1],
|
||||
scaleY: [sy * 0.4, sy, sy, 1],
|
||||
borderRadius: [logoKose, logoKose, logoKose, `${kose} / ${kose}`],
|
||||
},
|
||||
{
|
||||
duration: KARE_SURE,
|
||||
delay: gecikme,
|
||||
times: KARE_ZAMANLARI,
|
||||
ease: [GUCLU_CIKIS, "linear", YERLESME],
|
||||
},
|
||||
);
|
||||
k.then(() => {
|
||||
el.style.borderRadius = "";
|
||||
});
|
||||
kontroller.push(k);
|
||||
// Yerine büyürken logo renginden karenin kendi rengine (sınıftaki renk).
|
||||
sayaclar.push(
|
||||
window.setTimeout(
|
||||
() => {
|
||||
el.style.transition = "background-color 450ms ease-out";
|
||||
el.style.backgroundColor = "";
|
||||
},
|
||||
(gecikme + KARE_SURE * KARE_ZAMANLARI[2]) * 1000,
|
||||
),
|
||||
);
|
||||
|
||||
Array.from(el.children).forEach((satir, j) =>
|
||||
netlestir(
|
||||
satir as HTMLElement,
|
||||
gecikme + ICERIK_BASI + i * 0.06 + j * 0.05,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
sonra.forEach((el, i) =>
|
||||
netlestir(el, sonKare + ICERIK_BASI + 0.3 + i * 0.08),
|
||||
);
|
||||
|
||||
return () => {
|
||||
kontroller.forEach((k) => k.stop());
|
||||
sayaclar.forEach((s) => window.clearTimeout(s));
|
||||
for (const el of dokunulan) {
|
||||
el.style.opacity = "";
|
||||
el.style.filter = "";
|
||||
el.style.transform = "";
|
||||
el.style.transition = "";
|
||||
el.style.backgroundColor = "";
|
||||
el.style.borderRadius = "";
|
||||
}
|
||||
};
|
||||
}, [azHareket]);
|
||||
|
||||
return (
|
||||
<div ref={kapRef} className="mt-5">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,6 +190,8 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
// sütun, sağdaki aşağı kaymış, kare boyları farklı — ne tepeler ne
|
||||
// bitişler hizalı (masonry). Renk sempati katar, anlamı etiket taşır;
|
||||
// yazı beyaz (turuncu/mavi zeminde sitenin CTA'larıyla aynı ikili).
|
||||
// data-* işaretleri açılış koreografisinin sözleşmesi (akademik-ayrinti.tsx,
|
||||
// AcilisSahnesi).
|
||||
const kare =
|
||||
"flex flex-col items-center justify-center rounded-3xl px-3 text-white";
|
||||
const etiket = "text-sm font-bold text-white/90";
|
||||
@@ -198,7 +200,7 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
const orta = "font-heading text-3xl font-bold leading-none";
|
||||
|
||||
const siraKaresi = urap ? (
|
||||
<div className={`${kare} min-h-40 bg-orange-500 py-4`}>
|
||||
<div data-kare="sol" className={`${kare} min-h-40 bg-orange-500 py-4`}>
|
||||
<dt className={etiket}>Türkiye sırası</dt>
|
||||
<dd className="mt-3 flex items-baseline justify-center gap-1">
|
||||
<span className={devSayi}>{sayi(urap.sira)}.</span>
|
||||
@@ -225,7 +227,7 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
// yapılmaz. Bu yüzden ayrı kare değil, öğrenci karesinin alt satırı.
|
||||
const kadin = i ? kadinYuzdesi(i) : null;
|
||||
const ogrenciKaresi = oranVar ? (
|
||||
<div className={`${kare} min-h-24 bg-indigo-500 py-3`}>
|
||||
<div data-kare="sol" className={`${kare} min-h-24 bg-indigo-500 py-3`}>
|
||||
<dd className={orta}>{sayi(i.ogrenci)}</dd>
|
||||
<dt className={`mt-1.5 ${etiket}`}>Örgün öğrenci</dt>
|
||||
{kadin != null ? (
|
||||
@@ -236,7 +238,7 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
</div>
|
||||
) : null;
|
||||
const oranKaresi = oranVar ? (
|
||||
<div className={`${kare} min-h-32 bg-primary py-4`}>
|
||||
<div data-kare="sag-ust" className={`${kare} min-h-32 bg-primary py-4`}>
|
||||
<dt className={etiket}>Hoca başına öğrenci</dt>
|
||||
<dd className={`mt-3 ${devSayi}`}>{sayi(oran)}</dd>
|
||||
<dd className="mt-2 text-base font-bold text-white/90">
|
||||
@@ -245,7 +247,7 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
</div>
|
||||
) : null;
|
||||
const uyeKaresi = oranVar ? (
|
||||
<div className={`${kare} bg-green-600 py-5`}>
|
||||
<div data-kare="sag-alt" className={`${kare} bg-green-600 py-5`}>
|
||||
<dd className={orta}>{sayi(i.ogretimUyesi)}</dd>
|
||||
<dt className={`mt-1.5 ${etiket}`}>Öğretim üyesi</dt>
|
||||
</div>
|
||||
@@ -254,7 +256,10 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<dl className="flex w-full max-w-md items-start gap-3">
|
||||
<dl
|
||||
data-sahne
|
||||
className="relative flex w-full max-w-md items-start gap-3"
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-3">
|
||||
{siraKaresi}
|
||||
{ogrenciKaresi}
|
||||
@@ -269,7 +274,10 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
|
||||
{urap ? (
|
||||
<>
|
||||
<p className="mt-4 max-w-md text-sm leading-relaxed text-slate-500">
|
||||
<p
|
||||
data-sonra
|
||||
className="mt-4 max-w-md text-sm leading-relaxed text-slate-500"
|
||||
>
|
||||
Türkiye sırası araştırma performansını ölçer; eğitim kalitesini
|
||||
ya da iş bulma oranını ölçmez.
|
||||
</p>
|
||||
@@ -277,6 +285,7 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
data-sonra
|
||||
className="mt-4 cursor-pointer px-6 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||||
>
|
||||
<Link href="/universite-siralamasi">
|
||||
@@ -287,7 +296,10 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 flex w-full max-w-md items-center justify-center gap-3 border-t border-slate-100 pt-4 text-left text-xs leading-5 text-slate-500">
|
||||
<div
|
||||
data-sonra
|
||||
className="mt-5 flex w-full max-w-md items-center justify-center gap-3 border-t border-slate-100 pt-4 text-left text-xs leading-5 text-slate-500"
|
||||
>
|
||||
{urap ? <UrapLogo className="h-7 shrink-0" /> : null}
|
||||
<div>
|
||||
{urap ? (
|
||||
|
||||
Reference in New Issue
Block a user