merge(main): 2.0.1 üzerine Turnstile bot koruması (→ 2.0.2)
All checks were successful
Deploy / deploy (push) Successful in 12m39s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-11 02:17:07 +03:00
5 changed files with 78 additions and 47 deletions

View File

@@ -723,8 +723,13 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
altında yan yana durur, dokununca yer değiştirir. Odak dönüşen öğeyle gider (açılınca çarpı, altında yan yana durur, dokununca yer değiştirir. Odak dönüşen öğeyle gider (açılınca çarpı,
kapanınca kare). "Hareketi azalt"ta dönüşüm yok (`MotionConfig reducedMotion="user"`). Verisi kapanınca kare). "Hareketi azalt"ta dönüşüm yok (`MotionConfig reducedMotion="user"`). Verisi
olmayan bölümün karesi çizilmez. 10 Ekim'deki "bu çekmecede kart yok" kuralı bu üç bölüm için olmayan bölümün karesi çizilmez. 10 Ekim'deki "bu çekmecede kart yok" kuralı bu üç bölüm için
kalktı; koşullar, üniversite ve kaynak satırı kartsız kalır. Açılış sahnesi değişmedi (logo → kalktı; koşullar, üniversite ve kaynak satırı kartsız kalır.
başlık); kareler tek `data-sonra` bloğu olarak netleşir. - **11 Eki 2026 — logo → gösterge kareleri:** Bilal: "logonun dönüştüğü başlık olmamalı, içerikteki
kart alanları olmalı." Logonun üç karesi artık başlığa değil, aşağıdaki gösterge karelerine uçar
ve onların boyuna büyür (`data-logo-hedef="sol|sag-ust|sag-alt"`, `AcilisSahnesi` sözleşmesi;
kareler sırayla sol / sağ üst / sağ alt logo karesini alır); kare uçuşun son yarısında solar,
kart o sırada belirir. Hedefi olmayan logo karesi (verisi olmayan bölüm) olduğu yerde söner.
Başlık ilk andan görünür. 10 Ekim'deki logo → başlık dönüşümü kalktı.
- **10 Eki 2026 — büyük logo, logo → başlık (morph):** Bilal: "logo küçücük kalmış; logolar - **10 Eki 2026 — büyük logo, logo → başlık (morph):** Bilal: "logo küçücük kalmış; logolar
küçülürken başlıktaki yazıya morph olsun." Logo kabı `size-44` (akademik göstergelerdeki küçülürken başlıktaki yazıya morph olsun." Logo kabı `size-44` (akademik göstergelerdeki
logoyla aynı ölçek). Kareler sönmek yerine küçülerek (scale 0.2) başlık yazısının üç eşit logoyla aynı ölçek). Kareler sönmek yerine küçülerek (scale 0.2) başlık yazısının üç eşit

View File

@@ -1,6 +1,6 @@
{ {
"name": "kolaytercih", "name": "kolaytercih",
"version": "2.0.1", "version": "2.0.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -27,12 +27,20 @@ const SURUMLER: {
arayuz?: string[]; arayuz?: string[];
}[] = [ }[] = [
{ {
surum: "2.0.1", surum: "2.0.2",
tarih: "11 Ekim 2026", tarih: "11 Ekim 2026",
maddeler: [ maddeler: [
"Geri bildirim kutusu otomatik gönderimlere karşı korunuyor; çoğu zaman fark etmezsin, nadiren tek tıkla onay istenir.", "Geri bildirim kutusu otomatik gönderimlere karşı korunuyor; çoğu zaman fark etmezsin, nadiren tek tıkla onay istenir.",
], ],
}, },
{
surum: "2.0.1",
tarih: "11 Ekim 2026",
maddeler: [],
arayuz: [
"Program ayrıntısı açılırken logo kareleri başlığa değil, kontenjan, netler ve mezun istihdamı karelerine dönüşür.",
],
},
{ {
surum: "2.0.0", surum: "2.0.0",
tarih: "11 Ekim 2026", tarih: "11 Ekim 2026",

View File

@@ -9,9 +9,10 @@
// - Kartsız (program-ayrinti.tsx, §4.19 "kart yok"): `[data-kare]` yok, // - Kartsız (program-ayrinti.tsx, §4.19 "kart yok"): `[data-kare]` yok,
// `[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. sahnenin kardeşi bir `[data-logo-hedef]` varsa kareler // `[data-sonra]` bloklar netleşerek gelir. İçerikte `[data-logo-hedef]`
// sönmek yerine küçülerek o yazıya (çekmece başlığı) uçar, yazı onlardan // kart alanları varsa kareler sönmek yerine aynı adlı karta uçar ve onun
// netleşir (Bilal, 10 Eki 2026). // boyuna büyür; kart kareden belirir (Bilal, 11 Eki 2026; önce çekmece
// başlığına dönüşüyordu).
// "Hareketi azalt"ta logo ve büyüme yok, yalnız 0,25 s solma (§6). Veri // "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 // bekleyen çekmecede (`hazir` verilmiş) sahne yalnız veri ilk geldiğinde
// (false → true) oynar; önbellekli yeniden açılışta aynı 0,25 s solma // (false → true) oynar; önbellekli yeniden açılışta aynı 0,25 s solma
@@ -58,8 +59,8 @@ const LOGO_ZAMANLARI = [
(LOGO_BELIRME + LOGO_DURUS) / LOGO_SURE, (LOGO_BELIRME + LOGO_DURUS) / LOGO_SURE,
1, 1,
]; ];
// Hedefli kartsız mod: kareler sönmek yerine LOGO_UCUS süresince hedef yazıya // Hedefli kartsız mod: kareler sönmek yerine LOGO_UCUS süresince hedef karta
// uçar; uçuşun son yarısında solar, yazı o sırada netleşir. // uçup onun boyuna büyür; uçuşun son yarısında solar, kart o sırada belirir.
const LOGO_UCUS = 0.45; const LOGO_UCUS = 0.45;
const UCUS_SURE = LOGO_BELIRME + LOGO_DURUS + LOGO_UCUS; const UCUS_SURE = LOGO_BELIRME + LOGO_DURUS + LOGO_UCUS;
const UCUS_ZAMANLARI = [ const UCUS_ZAMANLARI = [
@@ -127,9 +128,10 @@ export function logoKareStili(slot: Slot, kapBoyu = LOGO_KAP_BOYU) {
* çekmece daha ekrana kaymadan oynuyor, gövde boş kalıyordu. Verilmezse * çekmece daha ekrana kaymadan oynuyor, gövde boş kalıyordu. Verilmezse
* (içerik sunucudan hazır gelen çekmece) sahne her montajda oynar. * (içerik sunucudan hazır gelen çekmece) sahne her montajda oynar.
* *
* `data-logo-hedef`: kartsız modda logo karelerinin küçülerek dönüşeceği yazı * `data-logo-hedef="sol|sag-ust|sag-alt"`: kartsız modda o logo karesinin
* (sahnenin kardeşi, ör. çekmece başlığı). Kareler yazının üç eşit dilimine * dönüşeceği kart alanı (içerikte, ör. gösterge karesi). Kare kartın yerine
* uçar; yazı sahne başında görünmez olur, kareler varırken netleşir. * uçup onun boyuna büyür; kart sahne başında görünmez olur, kare varırken
* belirir. Hedefi olmayan kare olduğu yerde söner.
*/ */
export function AcilisSahnesi({ export function AcilisSahnesi({
children, children,
@@ -224,15 +226,12 @@ export function AcilisSahnesi({
// iner ki altında boşluk kalmasın; içerik KARTSIZ_ICERIK_BASI'ndan // iner ki altında boşluk kalmasın; içerik KARTSIZ_ICERIK_BASI'ndan
// itibaren, sönmeyle çaprazlanarak gelir. // itibaren, sönmeyle çaprazlanarak gelir.
let sonKare = 0; let sonKare = 0;
// Hedef yazının kutusu (blok değil, yazının kendisi: Range). const hedefler = new Map<string, HTMLElement>();
const hedef = kap
kap.parentElement?.querySelector<HTMLElement>("[data-logo-hedef]") ?? null; .querySelectorAll<HTMLElement>("[data-logo-hedef]")
let hedefKutu: DOMRect | null = null; .forEach((el) => hedefler.set(el.dataset.logoHedef ?? "", el));
if (hedef) { // Logo kabı sıfır boya inince kartlar bu kadar yukarı kayar.
const aralik = document.createRange(); const logoBoyu = logo!.offsetHeight;
aralik.selectNodeContents(hedef);
hedefKutu = aralik.getBoundingClientRect();
}
logoKareleri.forEach((el) => { logoKareleri.forEach((el) => {
const slot = (el.dataset.logoKare ?? "sol") as Slot; const slot = (el.dataset.logoKare ?? "sol") as Slot;
const sira = SLOT_SIRASI.indexOf(slot); const sira = SLOT_SIRASI.indexOf(slot);
@@ -240,16 +239,31 @@ export function AcilisSahnesi({
sonKare = Math.max(sonKare, gecikme); sonKare = Math.max(sonKare, gecikme);
el.style.opacity = "0"; el.style.opacity = "0";
dokunulan.push(el); dokunulan.push(el);
if (hedefKutu) { const hedef = hedefler.get(slot);
if (hedef) {
// Kare ile hedef aynı çekmecede: çekmece kayarken fark değişmez. // Kare ile hedef aynı çekmecede: çekmece kayarken fark değişmez.
const k = el.getBoundingClientRect(); const k = el.getBoundingClientRect();
const dx = const h = hedef.getBoundingClientRect();
hedefKutu.left + (hedefKutu.width * (sira + 0.5)) / 3 - (k.left + k.width / 2); const dx = h.left + h.width / 2 - (k.left + k.width / 2);
const dy = hedefKutu.top + hedefKutu.height / 2 - (k.top + k.height / 2); const dy = h.top + h.height / 2 - logoBoyu - (k.top + k.height / 2);
const sx = h.width / k.width;
const sy = h.height / k.height;
// Ölçek köşeyi de büyütür; kartın köşesi için ters ölçekli elips.
const kose = getComputedStyle(el).borderTopLeftRadius;
const kartKose = parseFloat(getComputedStyle(hedef).borderTopLeftRadius) || 0;
const ucusBasi = gecikme + LOGO_BELIRME + LOGO_DURUS;
hedef.style.opacity = "0";
dokunulan.push(hedef);
kontroller.push( kontroller.push(
animate( animate(
el, el,
{ x: [0, 0, 0, dx], y: [0, 0, 0, dy], scale: [0.4, 1, 1, 0.2] }, {
x: [0, 0, 0, dx],
y: [0, 0, 0, dy],
scaleX: [0.4, 1, 1, sx],
scaleY: [0.4, 1, 1, sy],
borderRadius: [kose, kose, kose, `${kartKose / sx}px / ${kartKose / sy}px`],
},
{ {
duration: UCUS_SURE, duration: UCUS_SURE,
delay: gecikme, delay: gecikme,
@@ -267,6 +281,11 @@ export function AcilisSahnesi({
ease: [GUCLU_CIKIS, "linear", "easeIn"], ease: [GUCLU_CIKIS, "linear", "easeIn"],
}, },
), ),
animate(
hedef,
{ opacity: [0, 1] },
{ duration: LOGO_UCUS * 0.5, delay: ucusBasi + LOGO_UCUS * 0.4, ease: "easeOut" },
),
); );
return; return;
} }
@@ -288,22 +307,21 @@ export function AcilisSahnesi({
}); });
const sonmeBasi = sonKare + LOGO_SURE * LOGO_ZAMANLARI[2]; const sonmeBasi = sonKare + LOGO_SURE * LOGO_ZAMANLARI[2];
dokunulan.push(logo!); dokunulan.push(logo!);
if (hedef && hedefKutu) {
netlestir(hedef, sonmeBasi + LOGO_UCUS * 0.4, 0.4);
}
const kapat = animate( const kapat = animate(
logo!, logo!,
{ height: [logo!.offsetHeight, 0] }, { height: [logo!.offsetHeight, 0] },
{ duration: hedefKutu ? LOGO_UCUS : LOGO_SONME, delay: sonmeBasi, ease: YERLESME }, { duration: hedefler.size > 0 ? LOGO_UCUS : 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) => sonra.forEach((el, i) => {
netlestir(el, KARTSIZ_ICERIK_BASI + i * KARTSIZ_BLOK_ADIMI, KARTSIZ_NETLESME), // Hedef kartları taşıyan blok kendi netleşmez: kartlar karelerden belirir.
); if (el.querySelector("[data-logo-hedef]")) return;
netlestir(el, KARTSIZ_ICERIK_BASI + i * KARTSIZ_BLOK_ADIMI, KARTSIZ_NETLESME);
});
return temizle; return temizle;
} }

View File

@@ -197,15 +197,7 @@ export function ProgramAyrintiButonu({
<span className="sr-only">Kapat</span> <span className="sr-only">Kapat</span>
</AltSayfaKapat> </AltSayfaKapat>
<AltSayfaGovde className="px-4 pt-3 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6"> <AltSayfaGovde className="px-4 pt-3 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6">
{/* Başlık veri gelene dek görünmez: logo kareleri küçülüp buraya <AltSayfaBaslik className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
uçar, yazı onlardan netleşir (AcilisSahnesi `data-logo-hedef`). */}
<AltSayfaBaslik
data-logo-hedef
className={cn(
"px-10 text-center font-heading text-2xl font-bold text-slate-900",
(durum === "bekliyor" || durum === "yukleniyor") && "opacity-0",
)}
>
{isim} {isim}
</AltSayfaBaslik> </AltSayfaBaslik>
<AltSayfaAciklama className="mx-auto mt-1 max-w-md px-4 text-center text-sm text-slate-500"> <AltSayfaAciklama className="mx-auto mt-1 max-w-md px-4 text-center text-sm text-slate-500">
@@ -248,12 +240,13 @@ function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi |
</p> </p>
); );
} }
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo küçülerek başlığa // Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo kareleri gösterge
// dönüşür, bloklar yukarıdan aşağıya 0,05 s arayla netleşir. // karelerine dönüşür, bloklar yukarıdan aşağıya 0,05 s arayla netleşir.
return ( return (
<> <>
{/* Logo kareleri: sahne sırasında görünür, küçülerek başlığa uçar; kap {/* Logo kareleri: sahne sırasında görünür, aşağıdaki gösterge karelerine
sıfır boya iner. */} uçup onlara dönüşür (BolumKutulari `data-logo-hedef`); kap sıfır boya
iner. */}
<div data-logo className="relative mx-auto size-44" aria-hidden> <div data-logo className="relative mx-auto size-44" aria-hidden>
{LOGO_SLOTLARI.map((slot) => ( {LOGO_SLOTLARI.map((slot) => (
<span <span
@@ -589,9 +582,15 @@ function BolumKutulari({ veri }: { veri: ProgramAyrintisi }) {
setAcik(null); setAcik(null);
} }
// Sarmalayıcı: açılış sahnesinde bu kareye dönüşen logo karesinin hedefi
// (AcilisSahnesi). Düz öğe; sahne `motion` öğesinin stiline dokunmaz.
const kare = (x: Kutu) => ( const kare = (x: Kutu) => (
<motion.button <div
key={x.anahtar} key={x.anahtar}
data-logo-hedef={LOGO_SLOTLARI[kutular.indexOf(x)]}
className="rounded-3xl"
>
<motion.button
ref={(el) => { ref={(el) => {
kareRef.current[x.anahtar] = el; kareRef.current[x.anahtar] = el;
}} }}
@@ -618,6 +617,7 @@ function BolumKutulari({ veri }: { veri: ProgramAyrintisi }) {
<Maximize2 className="absolute top-3 right-3 size-4 text-white/80" aria-hidden /> <Maximize2 className="absolute top-3 right-3 size-4 text-white/80" aria-hidden />
<span className="sr-only"> — {x.baslik}, tamamını göster</span> <span className="sr-only"> — {x.baslik}, tamamını göster</span>
</motion.button> </motion.button>
</div>
); );
return ( return (