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:
@@ -1,30 +1,16 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import {
|
||||||
import { ArrowRight } from "lucide-react";
|
DogrulaKarti,
|
||||||
import { PixelCorner } from "@/components/pixel-decor";
|
DogrulaKartiSkeleton,
|
||||||
import { Button } from "@/components/ui/button";
|
} from "@/features/kullanici/components/dogrula-karti";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Girişini onayla",
|
title: "Girişini onayla",
|
||||||
robots: { index: false, follow: false },
|
robots: { index: false, follow: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Yalnızca site içi (göreli) callback'lere izin ver — open redirect kalkanı. */
|
export default function DogrulaPage({
|
||||||
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 sayfası. 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 default async function DogrulaPage({
|
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
@@ -34,68 +20,19 @@ export default async function DogrulaPage({
|
|||||||
newUserCallbackURL?: string;
|
newUserCallbackURL?: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const { token, callbackURL, errorCallbackURL, newUserCallbackURL } =
|
|
||||||
await searchParams;
|
|
||||||
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 (
|
return (
|
||||||
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
||||||
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">
|
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">
|
||||||
<div className="relative">
|
<Suspense fallback={<DogrulaKartiSkeleton />}>
|
||||||
<div className="relative rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
{searchParams.then((sp) => (
|
||||||
<h1 className="font-heading text-xl font-bold">Girişini onayla</h1>
|
<DogrulaKarti
|
||||||
<p className="mt-2 text-sm text-slate-600">
|
token={sp.token}
|
||||||
Bu bağlantı senin için üretildi ve{" "}
|
callbackURL={sp.callbackURL}
|
||||||
<span className="font-semibold">tek kullanımlık</span>. Aşağıdaki
|
errorCallbackURL={sp.errorCallbackURL}
|
||||||
butona tıkladığında KolayTercih hesabına giriş yapacaksın.
|
newUserCallbackURL={sp.newUserCallbackURL}
|
||||||
</p>
|
/>
|
||||||
<form
|
))}
|
||||||
method="GET"
|
</Suspense>
|
||||||
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>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
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="-mb-28 flex min-h-dvh flex-col bg-slate-50">
|
|
||||||
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,95 +1,30 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import {
|
||||||
import { redirect } from "next/navigation";
|
GirisKarti,
|
||||||
import { getSession } from "@/lib/session";
|
GirisKartiSkeleton,
|
||||||
import { PixelCorner } from "@/components/pixel-decor";
|
} from "@/features/kullanici/components/giris-karti";
|
||||||
import { GirisForm } from "./giris-form";
|
|
||||||
import { GirisBaslik } from "./giris-baslik";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Giriş",
|
title: "Giriş",
|
||||||
robots: { index: false, follow: true },
|
robots: { index: false, follow: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function GirisPage({
|
export default function GirisPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ callback?: string; hata?: string }>;
|
searchParams: Promise<{ callback?: string; hata?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { callback, hata } = await searchParams;
|
|
||||||
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 (
|
return (
|
||||||
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler —
|
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler —
|
||||||
// kart tam dvh içinde ortalanır, sayfa scroll etmez (sığmazsa scroll açılır).
|
// kart tam dvh içinde ortalanır, sayfa scroll etmez (sığmazsa scroll açılır).
|
||||||
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
||||||
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">
|
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">
|
||||||
<div className="relative">
|
<Suspense fallback={<GirisKartiSkeleton />}>
|
||||||
<div className="relative rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
{searchParams.then(({ callback, hata }) => (
|
||||||
{baglantiHatasi && (
|
<GirisKarti callback={callback} hata={hata} />
|
||||||
<div
|
))}
|
||||||
role="alert"
|
</Suspense>
|
||||||
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>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Suspense } from "react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
|
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
|
||||||
import { UserNav, UserNavFallback } from "@/components/user-nav";
|
import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav";
|
||||||
import { getAllRehberler } from "@/lib/rehber";
|
import { getAllRehberler } from "@/lib/rehber";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user