Dev paneli Apple hareket diliyle yenilendi; animasyon ve UX düzeltmeleri
All checks were successful
Deploy / deploy (push) Successful in 9m29s
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:
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
39
src/app/giris/loading.tsx
Normal file
39
src/app/giris/loading.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
// /giris açılırken spinner yerine sayfanın kendi iskeleti: kart, başlık,
|
||||||
|
// Google butonu, ayraç, e-posta alanı ve gönder butonu aynı yerde belirir —
|
||||||
|
// içerik gelince zıplama olmaz.
|
||||||
|
export default function GirisLoading() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col bg-slate-50">
|
||||||
|
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-4 py-16">
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||||
|
{/* Başlık + alt metin */}
|
||||||
|
<Skeleton className="h-8 w-40" />
|
||||||
|
<Skeleton className="mt-3 h-4 w-full" />
|
||||||
|
<Skeleton className="mt-1.5 h-4 w-2/3" />
|
||||||
|
|
||||||
|
{/* Google ile devam et */}
|
||||||
|
<Skeleton className="mt-6 h-11 w-full rounded-lg" />
|
||||||
|
|
||||||
|
{/* "veya" ayracı */}
|
||||||
|
<div className="mt-4 flex items-center gap-3">
|
||||||
|
<Skeleton className="h-px flex-1" />
|
||||||
|
<Skeleton className="h-3 w-8" />
|
||||||
|
<Skeleton className="h-px flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* E-posta alanı + gönder butonu */}
|
||||||
|
<Skeleton className="mt-4 h-11 w-full rounded-lg" />
|
||||||
|
<Skeleton className="mt-3 h-11 w-full rounded-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kart altındaki yasal metin */}
|
||||||
|
<div className="mt-6 flex flex-col items-center gap-1.5">
|
||||||
|
<Skeleton className="h-3 w-3/4" />
|
||||||
|
<Skeleton className="h-3 w-1/2" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -144,6 +144,23 @@
|
|||||||
animation: kt-unblur 0.9s ease-out both;
|
animation: kt-unblur 0.9s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hareket azaltılmışsa blur çözülmesi yerine kısa, yumuşak bir belirme:
|
||||||
|
kavramayı sağlayan geri bildirim kalır, gösterişli efekt düşer. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.kt-unblur {
|
||||||
|
animation: kt-unblur-sade 0.25s ease-out both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kt-unblur-sade {
|
||||||
|
from {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import {
|
import {
|
||||||
AnimatePresence,
|
AnimatePresence,
|
||||||
motion,
|
motion,
|
||||||
|
useReducedMotion,
|
||||||
type Transition,
|
type Transition,
|
||||||
type Variants,
|
type Variants,
|
||||||
} from "motion/react";
|
} from "motion/react";
|
||||||
@@ -36,8 +37,12 @@ export function TextLoop({
|
|||||||
}: TextLoopProps) {
|
}: TextLoopProps) {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const items = Children.toArray(children);
|
const items = Children.toArray(children);
|
||||||
|
// Sürekli dönen dekoratif metin: hareket azaltılsın diyen kullanıcıda
|
||||||
|
// döngü hiç başlamaz, ilk metin sabit kalır.
|
||||||
|
const azMotion = useReducedMotion();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (azMotion) return;
|
||||||
const intervalMs = interval * 1000;
|
const intervalMs = interval * 1000;
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCurrentIndex((current) => {
|
setCurrentIndex((current) => {
|
||||||
@@ -47,7 +52,7 @@ export function TextLoop({
|
|||||||
});
|
});
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [items.length, interval, onIndexChange]);
|
}, [azMotion, items.length, interval, onIndexChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative inline-block whitespace-nowrap", className)}>
|
<div className={cn("relative inline-block whitespace-nowrap", className)}>
|
||||||
|
|||||||
@@ -4,10 +4,16 @@
|
|||||||
// (layout.tsx NODE_ENV kontrolüyle monte eder; aksiyonlar da sunucuda
|
// (layout.tsx NODE_ENV kontrolüyle monte eder; aksiyonlar da sunucuda
|
||||||
// ayrıca korunur). Amaç: satın alma / giriş / kredi durumlarını tek
|
// ayrıca korunur). Amaç: satın alma / giriş / kredi durumlarını tek
|
||||||
// tıkla taklit edip uygulamanın tüm sayfalarını gerçek akışta gezmek.
|
// tıkla taklit edip uygulamanın tüm sayfalarını gerçek akışta gezmek.
|
||||||
|
//
|
||||||
|
// Hareket dili (Apple "Designing Fluid Interfaces"): panel DEV çipinden
|
||||||
|
// büyür ve yine ona küçülür (paylaşılan layoutId ile mekânsal bağ), yay
|
||||||
|
// kritik sönümlüdür (bounce 0) — açılış ortasında kapatılırsa mevcut
|
||||||
|
// değerden geri döner. Yüzey opak kutu değil, yarı saydam malzemedir.
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Loader2, Wrench, X } from "lucide-react";
|
import { Loader2, Wrench, X } from "lucide-react";
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import {
|
import {
|
||||||
@@ -35,12 +41,26 @@ const SAYFALAR: { ad: string; href: string }[] = [
|
|||||||
{ ad: "404", href: "/boyle-bir-sayfa-yok" },
|
{ ad: "404", href: "/boyle-bir-sayfa-yok" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Apple değerleri: damping 1.0 (bounce 0), response ~0.35 sn.
|
||||||
|
// Yay süreli değildir; hedef değişirse mevcut değer + hızdan devam eder.
|
||||||
|
const YAY = { type: "spring", bounce: 0, duration: 0.35 } as const;
|
||||||
|
|
||||||
|
// Basış anında tepki (pointer-down feedback) — bırakınca değil.
|
||||||
|
const BASIS =
|
||||||
|
"active:scale-[0.97] transition-[transform,background-color,border-color,opacity] duration-150 ease-out";
|
||||||
|
|
||||||
|
// Yarı saydam malzeme; şeffaflık azaltılsın diyen kullanıcıya opak yüzey.
|
||||||
|
const MALZEME =
|
||||||
|
"border border-white/60 bg-white/80 shadow-2xl shadow-slate-900/20 backdrop-blur-2xl backdrop-saturate-150 " +
|
||||||
|
"[@media(prefers-reduced-transparency:reduce)]:bg-white [@media(prefers-reduced-transparency:reduce)]:backdrop-blur-none";
|
||||||
|
|
||||||
export function DevPanel() {
|
export function DevPanel() {
|
||||||
const [acik, setAcik] = useState(false);
|
const [acik, setAcik] = useState(false);
|
||||||
const [durum, setDurum] = useState<DevDurum | null>(null);
|
const [durum, setDurum] = useState<DevDurum | null>(null);
|
||||||
const [email, setEmail] = useState("test@kolaytercih.dev");
|
const [email, setEmail] = useState("test@kolaytercih.dev");
|
||||||
const [mesgul, setMesgul] = useState(false);
|
const [mesgul, setMesgul] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const azMotion = useReducedMotion();
|
||||||
|
|
||||||
const yenile = useCallback(async () => {
|
const yenile = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -55,6 +75,16 @@ export function DevPanel() {
|
|||||||
void yenile();
|
void yenile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yol bulma: panelden çıkış her an mümkün — Escape da kapatır.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!acik) return;
|
||||||
|
const f = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") setAcik(false);
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", f);
|
||||||
|
return () => window.removeEventListener("keydown", f);
|
||||||
|
}, [acik]);
|
||||||
|
|
||||||
// Her aksiyonu tek kalıptan geçir: meşgul kilidi + durum tazeleme +
|
// Her aksiyonu tek kalıptan geçir: meşgul kilidi + durum tazeleme +
|
||||||
// server component'lerin (header'daki kredi rozeti dahil) yenilenmesi
|
// server component'lerin (header'daki kredi rozeti dahil) yenilenmesi
|
||||||
async function calistir(fn: () => Promise<unknown>) {
|
async function calistir(fn: () => Promise<unknown>) {
|
||||||
@@ -99,36 +129,71 @@ export function DevPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acik) {
|
const g = durum?.girisli ? durum : null;
|
||||||
|
|
||||||
|
// Azaltılmış hareket: morph yerine kısa çapraz solma (yaysız, kaymasız).
|
||||||
|
const paylasilan = azMotion ? {} : { layoutId: "dev-panel" };
|
||||||
|
const gecis = azMotion ? { duration: 0.15 } : YAY;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<AnimatePresence initial={false}>
|
||||||
|
{!acik ? (
|
||||||
|
<motion.button
|
||||||
|
key="cip"
|
||||||
|
{...paylasilan}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={paneliAc}
|
onClick={paneliAc}
|
||||||
title="Dev süperadmin paneli"
|
title="Dev süperadmin paneli"
|
||||||
className="fixed bottom-20 left-4 z-50 inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full border border-violet-300 bg-violet-600 px-3 text-xs font-bold text-white shadow-lg transition-transform duration-150 hover:bg-violet-700 active:scale-95 print:hidden"
|
style={{ borderRadius: 18 }}
|
||||||
|
transition={gecis}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0.15, ease: "easeOut" } }}
|
||||||
|
className={`fixed bottom-20 left-4 z-50 inline-flex h-9 cursor-pointer items-center gap-1.5 px-3 text-xs font-bold tracking-wide text-white ${MALZEME} !bg-slate-900/85 print:hidden ${BASIS} hover:!bg-slate-800/85`}
|
||||||
>
|
>
|
||||||
<Wrench className="size-3.5" aria-hidden />
|
<Wrench className="size-3.5 text-violet-300" aria-hidden />
|
||||||
DEV
|
DEV
|
||||||
</button>
|
</motion.button>
|
||||||
);
|
) : (
|
||||||
}
|
<motion.div
|
||||||
|
key="panel"
|
||||||
const g = durum?.girisli ? durum : null;
|
{...paylasilan}
|
||||||
|
role="dialog"
|
||||||
return (
|
aria-label="Dev süperadmin paneli"
|
||||||
<div className="fixed bottom-20 left-4 z-50 flex max-h-[72vh] w-80 flex-col overflow-hidden rounded-2xl border border-violet-200 bg-white shadow-2xl print:hidden">
|
style={{ borderRadius: 24, transformOrigin: "bottom left" }}
|
||||||
<div className="flex items-center justify-between border-b border-slate-100 bg-violet-600 px-4 py-2.5 text-white">
|
transition={gecis}
|
||||||
<p className="flex items-center gap-1.5 text-sm font-bold">
|
initial={{ opacity: 0 }}
|
||||||
<Wrench className="size-4" aria-hidden />
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0.15, ease: "easeOut" } }}
|
||||||
|
className={`fixed bottom-20 left-4 z-50 flex max-h-[72vh] w-80 flex-col overflow-hidden ${MALZEME} print:hidden`}
|
||||||
|
>
|
||||||
|
{/* İçerik morph sırasında bozulmasın: yüzey oturduktan hemen
|
||||||
|
sonra belirir (malzeme önce, içerik sonra). */}
|
||||||
|
<motion.div
|
||||||
|
initial={azMotion ? false : { opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.2, delay: azMotion ? 0 : 0.12 }}
|
||||||
|
className="flex min-h-0 flex-col"
|
||||||
|
>
|
||||||
|
{/* Başlık: opak şerit değil, aynı malzemenin üstünde ince
|
||||||
|
bir kenar çizgisiyle ayrışan katman. */}
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-900/5 px-4 py-3">
|
||||||
|
<p className="flex items-center gap-2 font-heading text-sm font-bold tracking-[-0.01em] text-slate-900">
|
||||||
|
<span className="flex size-6 items-center justify-center rounded-lg bg-violet-600 text-white shadow-sm">
|
||||||
|
<Wrench className="size-3.5" aria-hidden />
|
||||||
|
</span>
|
||||||
Dev süperadmin
|
Dev süperadmin
|
||||||
{mesgul ? (
|
{mesgul ? (
|
||||||
<Loader2 className="size-3.5 animate-spin" aria-hidden />
|
<Loader2
|
||||||
|
className="size-3.5 animate-spin text-slate-400"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAcik(false)}
|
onClick={() => setAcik(false)}
|
||||||
className="cursor-pointer rounded-full p-1 hover:bg-white/20"
|
className={`cursor-pointer rounded-full p-1.5 text-slate-500 hover:bg-slate-900/5 hover:text-slate-700 ${BASIS}`}
|
||||||
aria-label="Kapat"
|
aria-label="Kapat"
|
||||||
>
|
>
|
||||||
<X className="size-4" aria-hidden />
|
<X className="size-4" aria-hidden />
|
||||||
@@ -138,13 +203,13 @@ export function DevPanel() {
|
|||||||
<div className="flex flex-col gap-4 overflow-y-auto p-4 text-sm">
|
<div className="flex flex-col gap-4 overflow-y-auto p-4 text-sm">
|
||||||
{/* Oturum */}
|
{/* Oturum */}
|
||||||
<section>
|
<section>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
<p className="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
||||||
Oturum
|
Oturum
|
||||||
</p>
|
</p>
|
||||||
{g ? (
|
{g ? (
|
||||||
<div className="mt-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs">
|
<div className="mt-2 rounded-xl border border-slate-900/5 bg-white/60 px-3 py-2 text-xs">
|
||||||
<p className="font-semibold text-slate-800">{g.email}</p>
|
<p className="font-semibold text-slate-800">{g.email}</p>
|
||||||
<p className="mt-0.5 text-slate-500">
|
<p className="mt-0.5 tabular-nums text-slate-500">
|
||||||
{g.kredi} kredi · {g.hasPaket ? "paketli" : "paketsiz"} ·{" "}
|
{g.kredi} kredi · {g.hasPaket ? "paketli" : "paketsiz"} ·{" "}
|
||||||
{g.raporVar
|
{g.raporVar
|
||||||
? `rapor var (${g.sira?.toLocaleString("tr-TR")} · ${g.tur})`
|
? `rapor var (${g.sira?.toLocaleString("tr-TR")} · ${g.tur})`
|
||||||
@@ -158,13 +223,15 @@ export function DevPanel() {
|
|||||||
await authClient.signOut();
|
await authClient.signOut();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="mt-2 cursor-pointer text-violet-700 underline-offset-2 hover:underline"
|
className="mt-2 cursor-pointer font-medium text-violet-700 underline-offset-2 hover:underline"
|
||||||
>
|
>
|
||||||
Çıkış yap
|
Çıkış yap
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 text-xs text-slate-500">Girişsiz geziyorsun.</p>
|
<p className="mt-2 text-xs text-slate-500">
|
||||||
|
Girişsiz geziyorsun.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 flex gap-1.5">
|
<div className="mt-2 flex gap-1.5">
|
||||||
<input
|
<input
|
||||||
@@ -172,13 +239,13 @@ export function DevPanel() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="deneme@ornek.dev"
|
placeholder="deneme@ornek.dev"
|
||||||
className="h-9 min-w-0 flex-1 rounded-lg border border-slate-200 px-2.5 text-xs outline-none focus-visible:border-violet-400"
|
className="h-9 min-w-0 flex-1 rounded-lg border border-slate-900/10 bg-white/70 px-2.5 text-xs outline-none transition-colors duration-150 focus-visible:border-violet-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={mesgul}
|
disabled={mesgul}
|
||||||
onClick={anindaGiris}
|
onClick={anindaGiris}
|
||||||
className="h-9 shrink-0 cursor-pointer rounded-lg bg-violet-600 px-3 text-xs font-semibold text-white hover:bg-violet-700 disabled:opacity-50"
|
className={`h-9 shrink-0 cursor-pointer rounded-lg bg-violet-600 px-3 text-xs font-semibold text-white hover:bg-violet-700 disabled:opacity-50 ${BASIS}`}
|
||||||
>
|
>
|
||||||
Anında gir
|
Anında gir
|
||||||
</button>
|
</button>
|
||||||
@@ -190,7 +257,7 @@ export function DevPanel() {
|
|||||||
|
|
||||||
{/* Senaryolar */}
|
{/* Senaryolar */}
|
||||||
<section>
|
<section>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
<p className="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
||||||
Senaryo
|
Senaryo
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 grid grid-cols-2 gap-1.5">
|
<div className="mt-2 grid grid-cols-2 gap-1.5">
|
||||||
@@ -226,12 +293,12 @@ export function DevPanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={mesgul || !g}
|
disabled={mesgul || !g}
|
||||||
onClick={() => calistir(s.fn)}
|
onClick={() => calistir(s.fn)}
|
||||||
className="cursor-pointer rounded-lg border border-slate-200 px-2.5 py-2 text-left text-xs hover:border-violet-300 hover:bg-violet-50 disabled:cursor-default disabled:opacity-40"
|
className={`cursor-pointer rounded-xl border border-slate-900/10 bg-white/60 px-2.5 py-2 text-left text-xs hover:border-violet-300 hover:bg-violet-50/80 disabled:cursor-default disabled:opacity-40 ${BASIS}`}
|
||||||
>
|
>
|
||||||
<span className="block font-semibold text-slate-800">
|
<span className="block font-semibold text-slate-800">
|
||||||
{s.ad}
|
{s.ad}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-slate-500">
|
<span className="text-[10px] tabular-nums text-slate-500">
|
||||||
{s.aciklama}
|
{s.aciklama}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -241,18 +308,19 @@ export function DevPanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={mesgul || !g}
|
disabled={mesgul || !g}
|
||||||
onClick={odemeEkrani}
|
onClick={odemeEkrani}
|
||||||
className="mt-1.5 w-full cursor-pointer rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-left text-xs font-semibold text-emerald-800 hover:bg-emerald-100 disabled:cursor-default disabled:opacity-40"
|
className={`mt-1.5 w-full cursor-pointer rounded-xl border border-emerald-600/15 bg-emerald-50/80 px-2.5 py-2 text-left text-xs font-semibold text-emerald-800 hover:bg-emerald-100/80 disabled:cursor-default disabled:opacity-40 ${BASIS}`}
|
||||||
>
|
>
|
||||||
Ödeme başarı ekranı 🎉
|
Ödeme başarı ekranı 🎉
|
||||||
<span className="block text-[10px] font-normal text-emerald-700">
|
<span className="block text-[10px] font-normal text-emerald-700">
|
||||||
sahte "paid" sipariş oluşturup /odeme/sonuc'a gider
|
sahte "paid" sipariş oluşturup /odeme/sonuc'a
|
||||||
|
gider
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Sayfalar */}
|
{/* Sayfalar */}
|
||||||
<section>
|
<section>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
<p className="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
||||||
Sayfalar
|
Sayfalar
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
@@ -260,7 +328,7 @@ export function DevPanel() {
|
|||||||
<a
|
<a
|
||||||
key={s.href}
|
key={s.href}
|
||||||
href={s.href}
|
href={s.href}
|
||||||
className="cursor-pointer rounded-full border border-slate-200 px-2.5 py-1 text-[11px] text-slate-700 hover:border-violet-300 hover:bg-violet-50"
|
className={`cursor-pointer rounded-full border border-slate-900/10 bg-white/60 px-2.5 py-1 text-[11px] text-slate-700 hover:border-violet-300 hover:bg-violet-50/80 ${BASIS}`}
|
||||||
>
|
>
|
||||||
{s.ad}
|
{s.ad}
|
||||||
</a>
|
</a>
|
||||||
@@ -268,6 +336,9 @@ export function DevPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ export function SihirbazAdimlar({
|
|||||||
}) {
|
}) {
|
||||||
const [adim, setAdim] = useState(0);
|
const [adim, setAdim] = useState(0);
|
||||||
const [yon, setYon] = useState(1);
|
const [yon, setYon] = useState(1);
|
||||||
|
const [gecisBasladi, setGecisBasladi] = useState(false);
|
||||||
const [panelRef, panelOlculeri] = useMeasure();
|
const [panelRef, panelOlculeri] = useMeasure();
|
||||||
const hareketiAzalt = useReducedMotion();
|
const hareketiAzalt = useReducedMotion();
|
||||||
const [kategoriler, setKategoriler] = useState<string[]>(
|
const [kategoriler, setKategoriler] = useState<string[]>(
|
||||||
baslangicSecimler?.kategoriler ?? [],
|
baslangicSecimler?.kategoriler ?? [],
|
||||||
);
|
);
|
||||||
const [iller, setIller] = useState<string[]>(baslangicSecimler?.iller ?? []);
|
const [iller, setIller] = useState<string[]>(baslangicSecimler?.iller ?? []);
|
||||||
const [ilArama, setIlArama] = useState("");
|
|
||||||
const [universiteTipi, setUniversiteTipi] = useState<UniversiteTipi>(
|
const [universiteTipi, setUniversiteTipi] = useState<UniversiteTipi>(
|
||||||
baslangicSecimler?.universiteTipi ?? "farketmez",
|
baslangicSecimler?.universiteTipi ?? "farketmez",
|
||||||
);
|
);
|
||||||
@@ -98,9 +98,15 @@ export function SihirbazAdimlar({
|
|||||||
|
|
||||||
function adimaGit(yeniAdim: number) {
|
function adimaGit(yeniAdim: number) {
|
||||||
setYon(yeniAdim > adim ? 1 : -1);
|
setYon(yeniAdim > adim ? 1 : -1);
|
||||||
|
setGecisBasladi(true);
|
||||||
setAdim(yeniAdim);
|
setAdim(yeniAdim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const panelYuksekligi =
|
||||||
|
gecisBasladi && panelOlculeri.height > 0
|
||||||
|
? panelOlculeri.height
|
||||||
|
: "auto";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -109,10 +115,10 @@ export function SihirbazAdimlar({
|
|||||||
key={i}
|
key={i}
|
||||||
className={
|
className={
|
||||||
i === adim
|
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
|
: i < adim
|
||||||
? "h-1.5 w-3 rounded-full bg-primary/50 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-all"
|
: "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) => ({
|
enter: (gecisYonu: number) => ({
|
||||||
x: hareketiAzalt ? 0 : gecisYonu > 0 ? "100%" : "-100%",
|
x: hareketiAzalt ? 0 : gecisYonu > 0 ? "100%" : "-100%",
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
height:
|
height: panelYuksekligi,
|
||||||
panelOlculeri.height > 0 ? panelOlculeri.height : "auto",
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}),
|
}),
|
||||||
center: {
|
center: {
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
x: 0,
|
x: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
height:
|
height: panelYuksekligi,
|
||||||
panelOlculeri.height > 0 ? panelOlculeri.height : "auto",
|
|
||||||
},
|
},
|
||||||
exit: (gecisYonu: number) => ({
|
exit: (gecisYonu: number) => ({
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
@@ -151,7 +155,13 @@ export function SihirbazAdimlar({
|
|||||||
}}
|
}}
|
||||||
transition={
|
transition={
|
||||||
hareketiAzalt
|
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 },
|
x: { type: "spring", stiffness: 300, damping: 30 },
|
||||||
height: { 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">
|
<h3 className="font-heading text-lg font-bold">
|
||||||
Hangi alanlara ilgi duyuyorsun?
|
Hangi alanlara ilgi duyuyorsun?
|
||||||
</h3>
|
</h3>
|
||||||
@@ -187,56 +197,23 @@ export function SihirbazAdimlar({
|
|||||||
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Birincil seçim yolu: arama + çipler (haritadaki küçük iller
|
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
||||||
mobilde güvenilir dokunma hedefi değil) */}
|
güvenilir dokunma hedefi değil); tüm iller program sayısına
|
||||||
<input
|
göre sıralı listelenir. */}
|
||||||
type="search"
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
value={ilArama}
|
{[...facetler.iller]
|
||||||
onChange={(e) => setIlArama(e.target.value)}
|
.sort((a, b) => b.adet - a.adet)
|
||||||
placeholder="İl ara (ör. İzmir)"
|
.map((i) => (
|
||||||
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
|
<Cip
|
||||||
key={i.il}
|
key={i.il}
|
||||||
secili={iller.includes(i.il)}
|
secili={iller.includes(i.il)}
|
||||||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||||||
>
|
>
|
||||||
{i.il.toLocaleLowerCase("tr-TR")}
|
{i.il.toLocaleLowerCase("tr-TR")}
|
||||||
<span className="ml-1.5 text-xs opacity-70">
|
<span className="ml-1.5 text-xs opacity-70">{i.adet}</span>
|
||||||
{i.adet}
|
|
||||||
</span>
|
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
{eslesen.length === 0 ? (
|
|
||||||
<p className="text-sm text-slate-500">
|
|
||||||
Bu sıralamada "{ilArama.trim()}" için program
|
|
||||||
bulunamadı.
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</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>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{iller.length > 0 ? (
|
{iller.length > 0 ? (
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
motion,
|
motion,
|
||||||
useInView,
|
useInView,
|
||||||
|
useReducedMotion,
|
||||||
type DOMMotionComponents,
|
type DOMMotionComponents,
|
||||||
type HTMLMotionProps,
|
type HTMLMotionProps,
|
||||||
type MotionProps,
|
type MotionProps,
|
||||||
@@ -84,6 +85,9 @@ export function TypingAnimation({
|
|||||||
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||||
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||||
const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing")
|
const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing")
|
||||||
|
// Sürekli yazıp silen döngü, hareket azaltılsın diyen kullanıcı için
|
||||||
|
// rahatsız edici: ilk metin tam ve sabit gösterilir, imleç yanmaz.
|
||||||
|
const azMotion = useReducedMotion()
|
||||||
const elementRef = useRef<HTMLElement | null>(null)
|
const elementRef = useRef<HTMLElement | null>(null)
|
||||||
const isInView = useInView(elementRef as RefObject<Element>, {
|
const isInView = useInView(elementRef as RefObject<Element>, {
|
||||||
amount: 0.3,
|
amount: 0.3,
|
||||||
@@ -117,7 +121,7 @@ export function TypingAnimation({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
if (shouldStart && wordsToAnimate.length > 0) {
|
if (!azMotion && shouldStart && wordsToAnimate.length > 0) {
|
||||||
const timeoutDelay =
|
const timeoutDelay =
|
||||||
delay > 0 && displayedText === ""
|
delay > 0 && displayedText === ""
|
||||||
? delay
|
? delay
|
||||||
@@ -175,6 +179,7 @@ export function TypingAnimation({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
azMotion,
|
||||||
shouldStart,
|
shouldStart,
|
||||||
phase,
|
phase,
|
||||||
currentCharIndex,
|
currentCharIndex,
|
||||||
@@ -225,8 +230,8 @@ export function TypingAnimation({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{displayedText}
|
{azMotion ? (wordsToAnimate[0] ?? "") : displayedText}
|
||||||
{shouldShowCursor && (
|
{shouldShowCursor && !azMotion && (
|
||||||
<span
|
<span
|
||||||
className={cn("inline-block", blinkCursor && "animate-blink-cursor")}
|
className={cn("inline-block", blinkCursor && "animate-blink-cursor")}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user