Dev paneli Apple hareket diliyle yenilendi; animasyon ve UX düzeltmeleri
All checks were successful
Deploy / deploy (push) Successful in 9m29s

- Dev paneli: DEV çipinden layoutId morph ile büyüyüp küçülür (kritik sönümlü
  spring), yarı saydam malzeme (backdrop-blur + saturate), basışta anında
  scale geri bildirimi, Escape ile kapanış, reduced-motion/transparency desteği
- TypingAnimation ve TextLoop: prefers-reduced-motion'da sonsuz döngü durur,
  ilk metin sabit gösterilir
- kt-unblur: reduced-motion'da blur çözülmesi yerine kısa yumuşak belirme
- Sihirbaz: reduced-motion'da adım geçişinde height/x anında oturur;
  adım noktalarında transition-all yerine kapsamlı geçiş özellikleri
- Sihirbaz il adımındaki arama kutusu kaldırıldı; tüm iller program sayısına
  göre sıralı çip olarak listelenir
- /giris: spinner yerine kartın iskeletini gösteren route-level Skeleton

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-07-26 15:26:40 +03:00
parent 7288c926d0
commit 40a1c8ef8e
7 changed files with 338 additions and 224 deletions

View File

@@ -63,13 +63,13 @@ export function SihirbazAdimlar({
}) {
const [adim, setAdim] = useState(0);
const [yon, setYon] = useState(1);
const [gecisBasladi, setGecisBasladi] = useState(false);
const [panelRef, panelOlculeri] = useMeasure();
const hareketiAzalt = useReducedMotion();
const [kategoriler, setKategoriler] = useState<string[]>(
baslangicSecimler?.kategoriler ?? [],
);
const [iller, setIller] = useState<string[]>(baslangicSecimler?.iller ?? []);
const [ilArama, setIlArama] = useState("");
const [universiteTipi, setUniversiteTipi] = useState<UniversiteTipi>(
baslangicSecimler?.universiteTipi ?? "farketmez",
);
@@ -98,9 +98,15 @@ export function SihirbazAdimlar({
function adimaGit(yeniAdim: number) {
setYon(yeniAdim > adim ? 1 : -1);
setGecisBasladi(true);
setAdim(yeniAdim);
}
const panelYuksekligi =
gecisBasladi && panelOlculeri.height > 0
? panelOlculeri.height
: "auto";
return (
<div className="flex flex-col gap-5">
<div className="flex items-center gap-1.5">
@@ -109,10 +115,10 @@ export function SihirbazAdimlar({
key={i}
className={
i === adim
? "h-1.5 w-6 rounded-full bg-primary transition-all"
? "h-1.5 w-6 rounded-full bg-primary transition-[width,background-color] duration-200 ease-out"
: i < adim
? "h-1.5 w-3 rounded-full bg-primary/50 transition-all"
: "h-1.5 w-1.5 rounded-full bg-slate-200 transition-all"
? "h-1.5 w-3 rounded-full bg-primary/50 transition-[width,background-color] duration-200 ease-out"
: "h-1.5 w-1.5 rounded-full bg-slate-200 transition-[width,background-color] duration-200 ease-out"
}
/>
))}
@@ -128,16 +134,14 @@ export function SihirbazAdimlar({
enter: (gecisYonu: number) => ({
x: hareketiAzalt ? 0 : gecisYonu > 0 ? "100%" : "-100%",
opacity: 0,
height:
panelOlculeri.height > 0 ? panelOlculeri.height : "auto",
height: panelYuksekligi,
position: "relative",
}),
center: {
zIndex: 1,
x: 0,
opacity: 1,
height:
panelOlculeri.height > 0 ? panelOlculeri.height : "auto",
height: panelYuksekligi,
},
exit: (gecisYonu: number) => ({
zIndex: 0,
@@ -151,7 +155,13 @@ export function SihirbazAdimlar({
}}
transition={
hareketiAzalt
? { opacity: { duration: 0.12 } }
? // Azaltılmış harekette yalnızca kısa çapraz solma; height/x
// varsayılan spring'e düşüp oynamasın diye açıkça sıfırlanır.
{
opacity: { duration: 0.12 },
x: { duration: 0 },
height: { duration: 0 },
}
: {
x: { type: "spring", stiffness: 300, damping: 30 },
height: { type: "spring", stiffness: 300, damping: 30 },
@@ -159,7 +169,7 @@ export function SihirbazAdimlar({
}
}
>
<div ref={panelRef}>
<div ref={panelRef} className="px-px pb-2">
<h3 className="font-heading text-lg font-bold">
Hangi alanlara ilgi duyuyorsun?
</h3>
@@ -187,56 +197,23 @@ export function SihirbazAdimlar({
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
</p>
{/* Birincil seçim yolu: arama + çipler (haritadaki küçük iller
mobilde güvenilir dokunma hedefi değil) */}
<input
type="search"
value={ilArama}
onChange={(e) => setIlArama(e.target.value)}
placeholder="İl ara (ör. İzmir)"
aria-label="İl ara"
className="mt-4 h-10 w-full rounded-lg border border-slate-200 bg-white px-3 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
{(() => {
const q = ilArama.trim().toLocaleLowerCase("tr-TR");
const sirali = [...facetler.iller].sort((a, b) => b.adet - a.adet);
const eslesen = q
? sirali.filter((i) =>
i.il.toLocaleLowerCase("tr-TR").includes(q),
)
: sirali.slice(0, 12);
const kalan = q ? 0 : facetler.iller.length - eslesen.length;
return (
<div className="mt-3">
<div className="flex flex-wrap gap-2">
{eslesen.map((i) => (
<Cip
key={i.il}
secili={iller.includes(i.il)}
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
>
{i.il.toLocaleLowerCase("tr-TR")}
<span className="ml-1.5 text-xs opacity-70">
{i.adet}
</span>
</Cip>
))}
{eslesen.length === 0 ? (
<p className="text-sm text-slate-500">
Bu sıralamada &quot;{ilArama.trim()}&quot; için program
bulunamadı.
</p>
) : null}
</div>
{kalan > 0 ? (
<p className="mt-2 text-xs text-slate-400">
En çok programı olan 12 il gösteriliyor diğer {kalan} il
için arama yaz.
</p>
) : null}
</div>
);
})()}
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
güvenilir dokunma hedefi değil); tüm iller program sayısına
göre sıralı listelenir. */}
<div className="mt-4 flex flex-wrap gap-2">
{[...facetler.iller]
.sort((a, b) => b.adet - a.adet)
.map((i) => (
<Cip
key={i.il}
secili={iller.includes(i.il)}
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
>
{i.il.toLocaleLowerCase("tr-TR")}
<span className="ml-1.5 text-xs opacity-70">{i.adet}</span>
</Cip>
))}
</div>
{iller.length > 0 ? (
<div className="mt-3 flex flex-wrap items-center gap-2">