refactor: kullanici feature'ı — giriş kartları feature bileşenlerine çıktı (Faz 4)
- giris-form, giris-baslik, user-nav, sign-out-button features/kullanici/components/ altına taşındı - /giris'in session+redirect+callback temizliği GirisKarti'ya (async), /giris/dogrula'nın inline doğrulama formu + guvenliYol kalkanı DogrulaKarti'ya çıkarıldı; skeleton'lar aynı dosyada - İki sayfa senkron: searchParams.then() + sayfada Suspense; app/giris/loading.tsx silindi (geometrisi GirisKartiSkeleton'da yaşıyor) - site-header'ın UserNav importu güncellendi; mevcut Suspense sınırı yerinde - Davranış değişikliği yok Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
104
src/features/kullanici/components/dogrula-karti.tsx
Normal file
104
src/features/kullanici/components/dogrula-karti.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { PixelCorner } from "@/components/pixel-decor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
/** Yalnızca site içi (göreli) callback'lere izin ver — open redirect kalkanı. */
|
||||
function guvenliYol(deger: string | undefined, varsayilan: string): string {
|
||||
return deger && deger.startsWith("/") && !deger.startsWith("//")
|
||||
? deger
|
||||
: varsayilan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maildeki giriş bağlantısının indiği onay kartı. Doğrulama ucu
|
||||
* (/api/auth/magic-link/verify) GET'lendiği anda token'ı tüketir; mail
|
||||
* güvenlik tarayıcıları ve tarayıcı ön-yüklemeleri linki kullanıcıdan önce
|
||||
* açıp token'ı yakabildiği için maildeki link token'ı TÜKETMEYEN bu sayfaya
|
||||
* gelir. Verify ancak buradaki form gönderilince tetiklenir — tarayıcı botları
|
||||
* sayfayı GET'lese bile form'u göndermez, token yanmaz.
|
||||
*/
|
||||
export function DogrulaKarti({
|
||||
token,
|
||||
callbackURL,
|
||||
errorCallbackURL,
|
||||
newUserCallbackURL,
|
||||
}: {
|
||||
token?: string;
|
||||
callbackURL?: string;
|
||||
errorCallbackURL?: string;
|
||||
newUserCallbackURL?: string;
|
||||
}) {
|
||||
if (!token) redirect("/giris");
|
||||
|
||||
const callback = guvenliYol(callbackURL, "/");
|
||||
const hataCallback = guvenliYol(
|
||||
errorCallbackURL,
|
||||
`/giris?hata=gecersiz-baglanti&callback=${encodeURIComponent(callback)}`,
|
||||
);
|
||||
const yeniKullaniciCallback = guvenliYol(newUserCallbackURL, callback);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<h1 className="font-heading text-xl font-bold">Girişini onayla</h1>
|
||||
<p className="mt-2 text-sm text-slate-600">
|
||||
Bu bağlantı senin için üretildi ve{" "}
|
||||
<span className="font-semibold">tek kullanımlık</span>. Aşağıdaki
|
||||
butona tıkladığında KolayTercih hesabına giriş yapacaksın.
|
||||
</p>
|
||||
<form
|
||||
method="GET"
|
||||
action="/api/auth/magic-link/verify"
|
||||
className="mt-6"
|
||||
>
|
||||
<input type="hidden" name="token" value={token} />
|
||||
<input type="hidden" name="callbackURL" value={callback} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="errorCallbackURL"
|
||||
value={hataCallback}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="newUserCallbackURL"
|
||||
value={yeniKullaniciCallback}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-11 w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
Giriş yap
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</form>
|
||||
<p className="mt-4 text-xs leading-relaxed text-slate-500">
|
||||
Bu isteği sen yapmadıysan bu sayfayı kapatabilirsin — butona
|
||||
tıklanmadığı sürece hesabında hiçbir şey değişmez.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 z-10 text-primary"
|
||||
>
|
||||
<PixelCorner className="absolute -right-4 -top-4 size-9 [&_rect]:[fill-opacity:0.65]" />
|
||||
<PixelCorner className="absolute -bottom-4 -right-4 size-9 rotate-90 [&_rect]:[fill-opacity:0.65]" />
|
||||
<PixelCorner className="absolute -bottom-4 -left-4 size-9 rotate-180 [&_rect]:[fill-opacity:0.65]" />
|
||||
<PixelCorner className="absolute -left-4 -top-4 size-9 -rotate-90 [&_rect]:[fill-opacity:0.65]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DogrulaKartiSkeleton() {
|
||||
return (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<Skeleton className="h-7 w-40" />
|
||||
<Skeleton className="mt-3 h-4 w-full" />
|
||||
<Skeleton className="mt-1.5 h-4 w-2/3" />
|
||||
<Skeleton className="mt-6 h-11 w-full rounded-lg" />
|
||||
<Skeleton className="mt-4 h-3 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/features/kullanici/components/giris-baslik.tsx
Normal file
40
src/features/kullanici/components/giris-baslik.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { TypingAnimation } from "@/components/ui/typing-animation";
|
||||
|
||||
// Hepsi aynı şablon: "Giriş yap, çünkü …"
|
||||
const FUNNEL_MESAJLARI = [
|
||||
", çünkü listen seni bekliyor",
|
||||
", çünkü tercihine 1 adım kaldı",
|
||||
", çünkü seçimlerin hazır",
|
||||
", çünkü 5 kredin yanında",
|
||||
", çünkü kaçırmak istemezsin",
|
||||
", çünkü listen hemen üretilsin",
|
||||
];
|
||||
|
||||
/**
|
||||
* Giriş sayfası başlığı. Kullanıcı sihirbazdan (callback /sonuc'a dönüyorsa)
|
||||
* geldiyse sabit "Giriş yap" + typewriter hook'ları; diğer girişlerde
|
||||
* yalnız sabit başlık.
|
||||
*/
|
||||
export function GirisBaslik({ funnel }: { funnel: boolean }) {
|
||||
if (!funnel) {
|
||||
return <h1 className="font-heading text-xl font-bold">Giriş yap</h1>;
|
||||
}
|
||||
|
||||
return (
|
||||
<h1 className="flex flex-wrap items-baseline font-heading text-xl font-bold leading-tight">
|
||||
<span className="shrink-0">Giriş yap</span>
|
||||
<TypingAnimation
|
||||
as="span"
|
||||
words={FUNNEL_MESAJLARI}
|
||||
loop
|
||||
startOnView={false}
|
||||
typeSpeed={42}
|
||||
deleteSpeed={22}
|
||||
pauseDelay={2400}
|
||||
className="min-h-[1.25em] font-heading text-xl font-bold leading-tight tracking-[-0.02em] text-primary"
|
||||
/>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
168
src/features/kullanici/components/giris-form.tsx
Normal file
168
src/features/kullanici/components/giris-form.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Mail } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { olay } from "@/lib/analitik";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
function GoogleIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="size-4" aria-hidden>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1Z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23Z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.1a6.6 6.6 0 0 1 0-4.2V7.06H2.18a11 11 0 0 0 0 9.88l3.66-2.84Z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15A11 11 0 0 0 2.18 7.06L5.84 9.9C6.71 7.31 9.14 5.38 12 5.38Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [busy, setBusy] = useState<"google" | "email" | null>(null);
|
||||
const [sent, setSent] = useState(false);
|
||||
// Yeniden gönderim arası bekleme (sn) — istemeden spam'lenmesin
|
||||
const [bekleme, setBekleme] = useState(0);
|
||||
|
||||
function geriSayimBaslat() {
|
||||
setBekleme(30);
|
||||
const sayac = setInterval(() => {
|
||||
setBekleme((s) => {
|
||||
if (s <= 1) {
|
||||
clearInterval(sayac);
|
||||
return 0;
|
||||
}
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function googleIleGiris() {
|
||||
setBusy("google");
|
||||
olay("giris_denendi", { yontem: "google" });
|
||||
const { error } = await authClient.signIn.social({
|
||||
provider: "google",
|
||||
callbackURL,
|
||||
});
|
||||
if (error) {
|
||||
toast.error("Google ile giriş başarısız. Tekrar dener misin?");
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function linkGonder(e?: React.FormEvent) {
|
||||
e?.preventDefault();
|
||||
if (!/^\S+@\S+\.\S+$/.test(email)) {
|
||||
toast.error("Geçerli bir e-posta adresi gir.");
|
||||
return;
|
||||
}
|
||||
setBusy("email");
|
||||
const { error } = await authClient.signIn.magicLink({
|
||||
email,
|
||||
callbackURL,
|
||||
// Geçersiz/süresi dolmuş token'da kullanıcı ham ?error=INVALID_TOKEN
|
||||
// parametresiyle baş başa kalmasın: giriş sayfası bu değeri dostça bir
|
||||
// mesaja çevirir, callback korunur (bkz. src/app/giris/page.tsx).
|
||||
errorCallbackURL: `/giris?hata=gecersiz-baglanti&callback=${encodeURIComponent(callbackURL)}`,
|
||||
});
|
||||
setBusy(null);
|
||||
if (error) {
|
||||
toast.error("Bağlantı gönderilemedi. Tekrar dener misin?");
|
||||
return;
|
||||
}
|
||||
setSent(true);
|
||||
geriSayimBaslat();
|
||||
olay("giris_denendi", { yontem: "eposta" });
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
return (
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
||||
<p className="font-semibold">Bağlantı gönderildi 📬</p>
|
||||
<p className="mt-1">
|
||||
<span className="font-medium">{email}</span> adresine bir giriş
|
||||
bağlantısı gönderdik. Gelen kutunu (ve spam klasörünü) kontrol et —
|
||||
bağlantı 15 dakika geçerli.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => linkGonder()}
|
||||
disabled={busy !== null || bekleme > 0}
|
||||
className="cursor-pointer font-medium text-primary underline-offset-2 hover:underline disabled:cursor-default disabled:text-slate-400 disabled:no-underline"
|
||||
>
|
||||
{busy === "email"
|
||||
? "Gönderiliyor…"
|
||||
: bekleme > 0
|
||||
? `Yeniden gönder (${bekleme})`
|
||||
: "Yeniden gönder"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSent(false)}
|
||||
className="cursor-pointer text-slate-500 underline-offset-2 hover:underline"
|
||||
>
|
||||
E-postayı düzelt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer"
|
||||
onClick={googleIleGiris}
|
||||
disabled={busy !== null}
|
||||
>
|
||||
<GoogleIcon />
|
||||
{busy === "google" ? "Yönlendiriliyorsun…" : "Google ile devam et"}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-slate-400">
|
||||
<div className="h-px flex-1 bg-slate-200" />
|
||||
veya
|
||||
<div className="h-px flex-1 bg-slate-200" />
|
||||
</div>
|
||||
|
||||
<form onSubmit={linkGonder} className="space-y-3">
|
||||
<Input
|
||||
type="email"
|
||||
inputMode="email"
|
||||
autoComplete="email"
|
||||
placeholder="E-posta adresin"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={busy !== null}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-11 w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
disabled={busy !== null}
|
||||
>
|
||||
<Mail className="size-4" aria-hidden />
|
||||
{busy === "email" ? "Gönderiliyor…" : "Giriş bağlantısı gönder"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
src/features/kullanici/components/giris-karti.tsx
Normal file
122
src/features/kullanici/components/giris-karti.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { PixelCorner } from "@/components/pixel-decor";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { GirisForm } from "./giris-form";
|
||||
import { GirisBaslik } from "./giris-baslik";
|
||||
|
||||
export async function GirisKarti({
|
||||
callback,
|
||||
hata,
|
||||
}: {
|
||||
callback?: string;
|
||||
hata?: string;
|
||||
}) {
|
||||
const callbackURL = callback && callback.startsWith("/") ? callback : "/";
|
||||
// Magic link doğrulaması başarısız olunca errorCallbackURL buraya düşürür
|
||||
// (bkz. giris-form.tsx ve /giris/dogrula) — ham INVALID_TOKEN yerine
|
||||
// dostça bir mesaj gösterip yeni bağlantı istemeye yönlendiriyoruz.
|
||||
const baglantiHatasi = hata === "gecersiz-baglanti";
|
||||
|
||||
const session = await getSession();
|
||||
if (session) redirect(callbackURL);
|
||||
|
||||
// Sihirbaz funnel'ı: seçimler yapıldı, listeye giriş kaldı
|
||||
const funnel = callbackURL.startsWith("/sonuc");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<div className="relative rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
{baglantiHatasi && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mb-5 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800"
|
||||
>
|
||||
<p className="font-semibold">
|
||||
Giriş bağlantısı geçersiz ya da süresi dolmuş
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Bağlantılar 15 dakika geçerli ve tek kullanımlık. Aşağıdan
|
||||
yeni bir bağlantı isteyebilirsin.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<GirisBaslik funnel={funnel} />
|
||||
<p className="mt-2 text-sm text-slate-600">
|
||||
{funnel ? (
|
||||
<>
|
||||
Seçimlerin hazır — giriş yap,{" "}
|
||||
<span className="font-semibold">5 deneme kredinle</span> listen
|
||||
hemen üretilsin.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Kayıt olduğunda{" "}
|
||||
<span className="font-semibold">5 deneme kredisi</span>{" "}
|
||||
hesabına tanımlanır — Yapay Zeka danışmana ücretsiz soru sorabilirsin.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<GirisForm callbackURL={callbackURL} />
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 z-10 text-primary"
|
||||
>
|
||||
<PixelCorner className="absolute -right-4 -top-4 size-9 [&_rect]:[fill-opacity:0.65]" />
|
||||
<PixelCorner className="absolute -bottom-4 -right-4 size-9 rotate-90 [&_rect]:[fill-opacity:0.65]" />
|
||||
<PixelCorner className="absolute -bottom-4 -left-4 size-9 rotate-180 [&_rect]:[fill-opacity:0.65]" />
|
||||
<PixelCorner className="absolute -left-4 -top-4 size-9 -rotate-90 [&_rect]:[fill-opacity:0.65]" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-6 text-center text-xs leading-relaxed text-slate-500">
|
||||
Giriş yaparak verilerinin tercih önerileri üretmek amacıyla{" "}
|
||||
<Link href="/gizlilik" className="underline hover:text-slate-700">
|
||||
gizlilik metnindeki
|
||||
</Link>{" "}
|
||||
gibi işlenmesini ve{" "}
|
||||
<Link href="/kosullar" className="underline hover:text-slate-700">
|
||||
kullanım koşullarını
|
||||
</Link>{" "}
|
||||
kabul etmiş olursun. KolayTercih yerleşme garantisi vermez.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Kart, başlık, Google butonu, ayraç, e-posta alanı ve gönder butonu aynı
|
||||
// yerde belirir — içerik gelince zıplama olmaz.
|
||||
export function GirisKartiSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
24
src/features/kullanici/components/sign-out-button.tsx
Normal file
24
src/features/kullanici/components/sign-out-button.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function SignOutButton() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="size-9 cursor-pointer rounded-full border border-slate-200 bg-white text-slate-600 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 sm:size-12"
|
||||
aria-label="Çıkış yap"
|
||||
onClick={async () => {
|
||||
await authClient.signOut();
|
||||
router.refresh();
|
||||
}}
|
||||
>
|
||||
<LogOut className="size-4 sm:size-5" aria-hidden />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
65
src/features/kullanici/components/user-nav.tsx
Normal file
65
src/features/kullanici/components/user-nav.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import Link from "next/link";
|
||||
import { Coins } from "lucide-react";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ListemButonu } from "@/components/manuel-liste/listem-butonu";
|
||||
import { ListeSenkron } from "@/components/manuel-liste/liste-senkron";
|
||||
import { RybbitIdentify } from "@/components/rybbit-identify";
|
||||
import { SignOutButton } from "./sign-out-button";
|
||||
|
||||
/**
|
||||
* Suspense fallback'i: girişsiz görünümle aynı yükseklik/genişlikte sessiz
|
||||
* bir pill — session sorgusu akarken header zıplamasın (CLS yok).
|
||||
*/
|
||||
export function UserNavFallback() {
|
||||
return (
|
||||
<div
|
||||
className="order-2 flex shrink-0 items-center gap-1.5 sm:gap-2"
|
||||
aria-hidden
|
||||
>
|
||||
<div className="h-9 w-16 rounded-full bg-slate-900/10 sm:h-12 sm:w-28" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function UserNav() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
// Manuel liste localStorage'ta yaşadığı için Listem girişsiz de çalışır
|
||||
if (!u) {
|
||||
return (
|
||||
<div className="order-2 flex shrink-0 items-center gap-1.5 sm:gap-2">
|
||||
<RybbitIdentify kullaniciId={null} />
|
||||
<ListeSenkron userId={null} />
|
||||
<ListemButonu />
|
||||
<Button
|
||||
asChild
|
||||
className="h-9 cursor-pointer rounded-full bg-slate-900 px-3.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-slate-700 sm:h-12 sm:px-7 sm:text-base"
|
||||
>
|
||||
<Link href="/giris">Giriş yap</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="order-2 flex shrink-0 items-center gap-1.5 sm:gap-2">
|
||||
<RybbitIdentify kullaniciId={u.id} eposta={u.email} isim={u.name} />
|
||||
<ListeSenkron userId={u.id} />
|
||||
<ListemButonu />
|
||||
<Link
|
||||
href="/paket"
|
||||
className="inline-flex h-9 items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50 sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
|
||||
title="Kredilerin — yüklemek için tıkla"
|
||||
>
|
||||
<Coins className="size-3.5 text-amber-500 sm:size-4" aria-hidden />
|
||||
<span className="tabular-nums">{u.creditBalance}</span>
|
||||
{/* Etiket yalnızca md–lg arası: daha darda logo kesiliyor, lg üstünde
|
||||
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
|
||||
title her genişlikte anlamı taşıyor. */}
|
||||
<span className="hidden md:inline lg:hidden">kredi</span>
|
||||
</Link>
|
||||
<SignOutButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user