feat: update app.db and BACKLOG.md with new decisions and adjustments
All checks were successful
Deploy / deploy (push) Successful in 7m45s

- Updated the app.db file to reflect recent changes in the application state.
- Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates.
- Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
This commit is contained in:
bilalgursen
2026-10-06 17:17:49 +03:00
parent c0696e2cbc
commit a3cb058a3b
74 changed files with 10334 additions and 445 deletions

View File

@@ -4,14 +4,19 @@ import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı.
// İki varyant:
// Üç varyant:
//
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
// şeftalinin yeri, şeftali → turuncunun, turuncu → mavinin), üç adımda
// herkes başlangıç formuna döner. Uçuş sırasında kare hafifçe küçülüp
// solar, varışta toparlanır. Site header'daki logo hover'da bunu oynatır.
// - "zincir": aynı rota, ama kareler aynı anda değil sırayla yürür — bir kare
// hedefindeki karenin üzerine konarken alttaki yola çıkar, üçüncü kare boş
// kalan yere oturur ve zincir baştan başlar. Danışman avatarı yalnız
// danışman veriye bakarken (araç çalışırken) bunu oynatır.
// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli).
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli; danışman
// avatarı da cevap beklenirken böyle atar).
//
// Performans notu: kareler SVG <rect> değil HTML <span>, animasyon da Web
// Animations API ile yalnızca `translate` / `scale` / `opacity` üzerinden
@@ -49,6 +54,26 @@ const ROTA_NABIZ_ZAMANLARI = [
const ROTA_NABIZ_SCALE = [1, 0.84, 1, 1, 0.84, 1, 1, 0.84, 1, 1];
const ROTA_NABIZ_OPACITY = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1];
// Zincir: iki kalkış arası 450 ms, tek yolculuk 600 ms. Sıradaki kare, gelen
// kare yolun ~%94'ündeyken (üzerine konmuşken) kalkar; son 150 ms'de biri
// inerken diğeri hızlanır, akış hiç durmaz. Her kare turda 3 yolculuk yapar.
const ZINCIR_ADIM = 450;
const ZINCIR_YOL = 600;
const ZINCIR_SURE = ZINCIR_ADIM * 9;
const ZINCIR_YOL_PAYI = ZINCIR_YOL / ZINCIR_SURE;
const ZINCIR_KONUM_ZAMANLARI = [
...[0, 1, 2].flatMap((etap) => [etap / 3, etap / 3 + ZINCIR_YOL_PAYI]),
1,
];
const ZINCIR_NABIZ_ZAMANLARI = [
...[0, 1, 2].flatMap((etap) => [
etap / 3,
etap / 3 + ZINCIR_YOL_PAYI / 2,
etap / 3 + ZINCIR_YOL_PAYI,
]),
1,
];
// Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe
// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez.
const NABIZ_SURE = 1800;
@@ -60,6 +85,10 @@ const YOL = "cubic-bezier(0.65, 0, 0.35, 1)";
// Hover bitince yerine oturma (easeOutQuint): hızlı toparlanır, sarsmadan durur.
const DONUS = "cubic-bezier(0.22, 1, 0.36, 1)";
const DONUS_SURE = 500;
// `oynat` kapanınca yerine oturma: hover'daki kadar aceleci değil — duran kare
// yumuşak kalkar, uzun ve yumuşak iner.
const YERLES = "cubic-bezier(0.4, 0, 0.2, 1)";
const YERLES_SURE = 600;
/** Karenin `translate` değeri: yüzdeler karenin kendi boyutuna göredir, bu
* yüzden işaret hangi boyutta çizilirse çizilsin aynı oran korunur. */
@@ -76,10 +105,12 @@ function rota(baslangic: number, boyut: number): string[] {
return [p0, p1, p1, p2, p2, p0, p0];
}
type Varyant = "rota" | "zincir" | "nabiz";
function donguyuBaslat(
el: HTMLElement,
i: number,
varyant: "rota" | "nabiz",
varyant: Varyant,
): Animation[] {
const k = KARELER[i];
if (varyant === "nabiz") {
@@ -94,9 +125,10 @@ function donguyuBaslat(
),
];
}
const zincir = varyant === "zincir";
const secenek = {
duration: ROTA_SURE,
delay: i * ROTA_GECIKME_ADIMI,
duration: zincir ? ZINCIR_SURE : ROTA_SURE,
delay: i * (zincir ? ZINCIR_ADIM : ROTA_GECIKME_ADIMI),
iterations: Infinity,
};
// Konum ve nabız ayrı animasyonlar: konum etabı tek parça eğriyle akar,
@@ -105,7 +137,7 @@ function donguyuBaslat(
el.animate(
{
translate: rota(k.baslangic, k.boyut),
offset: KONUM_ZAMANLARI,
offset: zincir ? ZINCIR_KONUM_ZAMANLARI : KONUM_ZAMANLARI,
easing: YOL,
},
secenek,
@@ -113,8 +145,10 @@ function donguyuBaslat(
el.animate(
{
scale: ROTA_NABIZ_SCALE.map(String),
opacity: ROTA_NABIZ_OPACITY,
offset: ROTA_NABIZ_ZAMANLARI,
// Zincirde kare solmaz: başka bir karenin üzerine konar, yarı saydam
// olursa alttaki renk içinden görünür.
...(zincir ? {} : { opacity: ROTA_NABIZ_OPACITY }),
offset: zincir ? ZINCIR_NABIZ_ZAMANLARI : ROTA_NABIZ_ZAMANLARI,
easing: SINE,
},
secenek,
@@ -126,22 +160,35 @@ function donguyuBaslat(
* Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner;
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
* header'daki logo linki) ve hover bitince kareler yumuşakça yerine oturur.
* `oynat` ile döngü dışarıdan açılıp kapatılır, `varyant` da çalışırken
* değişebilir (ör. danışman avatarı: cevap beklenirken nabız, veriye bakarken
* zincir); her geçişte kareler önce yumuşakça yerine oturur, yeni döngü
* yerinden başlar.
*/
export function AnimasyonluMark({
className,
sadeceHover = false,
oynat = true,
varyant = "rota",
renk = "marka",
}: {
className?: string;
sadeceHover?: boolean;
varyant?: "rota" | "nabiz";
/** `sadeceHover` yokken döngü açık mı. `false` iken işaret durağandır. */
oynat?: boolean;
varyant?: Varyant;
/** "marka": logodaki üç renk; "turuncu": tüm kareler sitenin CTA turuncusu
* (orange-500) — ör. danışman panel başlığı. */
renk?: "marka" | "turuncu";
}) {
const kokRef = useRef<HTMLSpanElement>(null);
const kontrolRef = useRef<{
ayarla: (hedef: Varyant | null, cevik?: boolean) => void;
} | null>(null);
// Animasyonların sahibi: bileşen yaşadıkça bir kez kurulur. Prop değişimi
// bunu söküp kurmaz (kareler sıçrardı); aşağıdaki efekt `ayarla` ile hedefi
// bildirir.
useEffect(() => {
const kok = kokRef.current;
if (!kok || typeof kok.animate !== "function") return;
@@ -150,32 +197,25 @@ export function AnimasyonluMark({
let dongu: Animation[] = [];
let donus: Animation[] = [];
let istekli = false;
/** Şu an dönen döngü ve istenen döngü (`null`: durağan). */
let aktif: Varyant | null = null;
let hedef: Varyant | null = null;
// Döngü bir mikro görev sonra başlar; o arada efekt sökülmüşse (React
// geliştirme modunda her efekti kur-sök-kur yapar) başlamamalı — yoksa
// kimsenin durduramadığı sahipsiz bir döngü kalır.
let sokuldu = false;
const baslat = () => {
if (istekli) return;
istekli = true;
if (azHareket.matches) return;
// Kareler hâlâ yerine dönüyorsa (hızlı çık-gir) dönüşün bitmesi
// beklenir; döngü hep yerinden başlar, sıçrama olmaz.
Promise.all(donus.map((a) => a.finished))
.then(() => {
if (!istekli || dongu.length > 0) return;
dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, varyant));
})
.catch(() => {});
};
const durdur = () => {
istekli = false;
if (dongu.length === 0) return;
// Kareler o an neredeyse oradan yerlerine süzülür.
/** Döngüyü keser; kareler o an neredeyse oradan yerlerine süzülür. */
const yerles = (cevik: boolean) => {
const anlik = kareler.map((el) => {
const cs = getComputedStyle(el);
return { translate: cs.translate, scale: cs.scale, opacity: cs.opacity };
});
// Nabızda kareler zaten yerindedir, yalnız boyut toparlanır: kısa sürer.
const sure = aktif === "nabiz" ? YERLES_SURE / 2 : YERLES_SURE;
dongu.forEach((a) => a.cancel());
dongu = [];
aktif = null;
donus = kareler.map((el, i) =>
el.animate(
[
@@ -186,38 +226,73 @@ export function AnimasyonluMark({
opacity: 1,
},
],
{ duration: DONUS_SURE, easing: DONUS },
cevik
? { duration: DONUS_SURE, easing: DONUS }
: { duration: sure, easing: YERLES },
),
);
};
if (!sadeceHover) {
baslat();
return () => {
dongu.forEach((a) => a.cancel());
};
}
const ayarla = (yeni: Varyant | null, cevik = false) => {
if (yeni === hedef) return;
hedef = yeni;
if (aktif !== null) yerles(cevik);
if (yeni === null || azHareket.matches) return;
// Kareler hâlâ yerine dönüyorsa dönüşün bitmesi beklenir; döngü hep
// yerinden başlar, sıçrama olmaz.
Promise.all(donus.map((a) => a.finished))
.then(() => {
if (sokuldu || hedef !== yeni || aktif !== null) return;
dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, yeni));
aktif = yeni;
})
.catch(() => {});
};
const kontrol = { ayarla };
kontrolRef.current = kontrol;
return () => {
sokuldu = true;
if (kontrolRef.current === kontrol) kontrolRef.current = null;
[...dongu, ...donus].forEach((a) => a.cancel());
};
}, []);
// Üstteki efektten sonra koşar (aynı bileşende efektler sırayla çalışır),
// o yüzden kontrolRef burada doludur.
useEffect(() => {
const kok = kokRef.current;
if (!kok) return;
if (!sadeceHover) {
kontrolRef.current?.ayarla(oynat ? varyant : null);
return;
}
kontrolRef.current?.ayarla(null, true);
// Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site
// header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine
// gelindiğinde de animasyon başlar. `group`'suz kullanımda köke düşer.
const hedef = kok.closest(".group") ?? kok;
hedef.addEventListener("mouseenter", baslat);
hedef.addEventListener("mouseleave", durdur);
const hoverHedefi = kok.closest(".group") ?? kok;
const gir = () => kontrolRef.current?.ayarla(varyant, true);
const cik = () => kontrolRef.current?.ayarla(null, true);
hoverHedefi.addEventListener("mouseenter", gir);
hoverHedefi.addEventListener("mouseleave", cik);
return () => {
hedef.removeEventListener("mouseenter", baslat);
hedef.removeEventListener("mouseleave", durdur);
[...dongu, ...donus].forEach((a) => a.cancel());
hoverHedefi.removeEventListener("mouseenter", gir);
hoverHedefi.removeEventListener("mouseleave", cik);
};
}, [sadeceHover, varyant]);
}, [oynat, sadeceHover, varyant]);
return (
<span
ref={kokRef}
aria-hidden="true"
className={cn("relative block shrink-0", className)}
className={cn(
"relative block shrink-0",
varyant !== "rota" && "isolate",
className,
)}
>
{KARELER.map((k) => (
{KARELER.map((k, i) => (
<span
key={k.renk}
className={cn(
@@ -230,6 +305,11 @@ export function AnimasyonluMark({
borderRadius: `${(13 / k.boyut) * 100}%`,
backgroundColor: renk === "marka" ? k.renk : undefined,
translate: yer(k.baslangic, k.boyut),
// Zincirde yürüyen kare hep hedefindekinin üstüne konar: sırası
// önce gelen üstte durur (mavi > şeftali > turuncu). Avatar nabız
// ile zincir arasında gidip geldiği için sıra nabızda da aynıdır
// (orada kareler üst üste gelmez); header'daki rota'ya dokunulmaz.
zIndex: varyant !== "rota" ? KARELER.length - i : undefined,
}}
/>
))}