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ı,
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
kalktı; koşullar, üniversite ve kaynak satırı kartsız kalır. Açılış sahnesi değişmedi (logo →
başlık); kareler tek `data-sonra` bloğu olarak netleşir.
kalktı; koşullar, üniversite ve kaynak satırı kartsız kalır.
- **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
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

View File

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

View File

@@ -27,12 +27,20 @@ const SURUMLER: {
arayuz?: string[];
}[] = [
{
surum: "2.0.1",
surum: "2.0.2",
tarih: "11 Ekim 2026",
maddeler: [
"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",
tarih: "11 Ekim 2026",

View File

@@ -9,9 +9,10 @@
// - Kartsız (program-ayrinti.tsx, §4.19 "kart yok"): `[data-kare]` yok,
// `[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
// `[data-sonra]` bloklar netleşerek gelir. sahnenin kardeşi bir `[data-logo-hedef]` varsa kareler
// sönmek yerine küçülerek o yazıya (çekmece başlığı) uçar, yazı onlardan
// netleşir (Bilal, 10 Eki 2026).
// `[data-sonra]` bloklar netleşerek gelir. İçerikte `[data-logo-hedef]`
// kart alanları varsa kareler sönmek yerine aynı adlı karta uçar ve onun
// 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
// bekleyen çekmecede (`hazir` verilmiş) sahne yalnız veri ilk geldiğinde
// (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,
1,
];
// Hedefli kartsız mod: kareler sönmek yerine LOGO_UCUS süresince hedef yazıya
// uçar; uçuşun son yarısında solar, yazı o sırada netleşir.
// Hedefli kartsız mod: kareler sönmek yerine LOGO_UCUS süresince hedef karta
// 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 UCUS_SURE = LOGO_BELIRME + LOGO_DURUS + LOGO_UCUS;
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
* (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ı
* (sahnenin kardeşi, ör. çekmece başlığı). Kareler yazının üç eşit dilimine
* uçar; yazı sahne başında görünmez olur, kareler varırken netleşir.
* `data-logo-hedef="sol|sag-ust|sag-alt"`: kartsız modda o logo karesinin
* dönüşeceği kart alanı (içerikte, ör. gösterge karesi). Kare kartın yerine
* 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({
children,
@@ -224,15 +226,12 @@ export function AcilisSahnesi({
// iner ki altında boşluk kalmasın; içerik KARTSIZ_ICERIK_BASI'ndan
// itibaren, sönmeyle çaprazlanarak gelir.
let sonKare = 0;
// Hedef yazının kutusu (blok değil, yazının kendisi: Range).
const hedef =
kap.parentElement?.querySelector<HTMLElement>("[data-logo-hedef]") ?? null;
let hedefKutu: DOMRect | null = null;
if (hedef) {
const aralik = document.createRange();
aralik.selectNodeContents(hedef);
hedefKutu = aralik.getBoundingClientRect();
}
const hedefler = new Map<string, HTMLElement>();
kap
.querySelectorAll<HTMLElement>("[data-logo-hedef]")
.forEach((el) => hedefler.set(el.dataset.logoHedef ?? "", el));
// Logo kabı sıfır boya inince kartlar bu kadar yukarı kayar.
const logoBoyu = logo!.offsetHeight;
logoKareleri.forEach((el) => {
const slot = (el.dataset.logoKare ?? "sol") as Slot;
const sira = SLOT_SIRASI.indexOf(slot);
@@ -240,16 +239,31 @@ export function AcilisSahnesi({
sonKare = Math.max(sonKare, gecikme);
el.style.opacity = "0";
dokunulan.push(el);
if (hedefKutu) {
const hedef = hedefler.get(slot);
if (hedef) {
// Kare ile hedef aynı çekmecede: çekmece kayarken fark değişmez.
const k = el.getBoundingClientRect();
const dx =
hedefKutu.left + (hedefKutu.width * (sira + 0.5)) / 3 - (k.left + k.width / 2);
const dy = hedefKutu.top + hedefKutu.height / 2 - (k.top + k.height / 2);
const h = hedef.getBoundingClientRect();
const dx = h.left + h.width / 2 - (k.left + k.width / 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(
animate(
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,
delay: gecikme,
@@ -267,6 +281,11 @@ export function AcilisSahnesi({
ease: [GUCLU_CIKIS, "linear", "easeIn"],
},
),
animate(
hedef,
{ opacity: [0, 1] },
{ duration: LOGO_UCUS * 0.5, delay: ucusBasi + LOGO_UCUS * 0.4, ease: "easeOut" },
),
);
return;
}
@@ -288,22 +307,21 @@ export function AcilisSahnesi({
});
const sonmeBasi = sonKare + LOGO_SURE * LOGO_ZAMANLARI[2];
dokunulan.push(logo!);
if (hedef && hedefKutu) {
netlestir(hedef, sonmeBasi + LOGO_UCUS * 0.4, 0.4);
}
const kapat = animate(
logo!,
{ 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(() => {
logo!.style.display = "none";
});
kontroller.push(kapat);
sonra.forEach((el, i) =>
netlestir(el, KARTSIZ_ICERIK_BASI + i * KARTSIZ_BLOK_ADIMI, KARTSIZ_NETLESME),
);
sonra.forEach((el, i) => {
// 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;
}

View File

@@ -197,15 +197,7 @@ export function ProgramAyrintiButonu({
<span className="sr-only">Kapat</span>
</AltSayfaKapat>
<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
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",
)}
>
<AltSayfaBaslik className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
{isim}
</AltSayfaBaslik>
<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>
);
}
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo küçülerek başlığa
// dönüşür, bloklar yukarıdan aşağıya 0,05 s arayla netleşir.
// Sıra = AcilisSahnesi'nin `data-sonra` sırası: logo kareleri gösterge
// karelerine dönüşür, bloklar yukarıdan aşağıya 0,05 s arayla netleşir.
return (
<>
{/* Logo kareleri: sahne sırasında görünür, küçülerek başlığa uçar; kap
sıfır boya iner. */}
{/* Logo kareleri: sahne sırasında görünür, aşağıdaki gösterge karelerine
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>
{LOGO_SLOTLARI.map((slot) => (
<span
@@ -589,9 +582,15 @@ function BolumKutulari({ veri }: { veri: ProgramAyrintisi }) {
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) => (
<motion.button
<div
key={x.anahtar}
data-logo-hedef={LOGO_SLOTLARI[kutular.indexOf(x)]}
className="rounded-3xl"
>
<motion.button
ref={(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 />
<span className="sr-only"> — {x.baslik}, tamamını göster</span>
</motion.button>
</div>
);
return (