diff --git a/src/app/giris/dogrula/page.tsx b/src/app/giris/dogrula/page.tsx index 9be3acc..5242a9d 100644 --- a/src/app/giris/dogrula/page.tsx +++ b/src/app/giris/dogrula/page.tsx @@ -1,30 +1,16 @@ +import { Suspense } from "react"; import type { Metadata } from "next"; -import { redirect } from "next/navigation"; -import { ArrowRight } from "lucide-react"; -import { PixelCorner } from "@/components/pixel-decor"; -import { Button } from "@/components/ui/button"; +import { + DogrulaKarti, + DogrulaKartiSkeleton, +} from "@/features/kullanici/components/dogrula-karti"; export const metadata: Metadata = { title: "Girişini onayla", robots: { index: false, follow: false }, }; -/** 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 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({ +export default function DogrulaPage({ searchParams, }: { searchParams: Promise<{ @@ -34,68 +20,19 @@ export default async function DogrulaPage({ 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 (
-
-
-

Girişini onayla

-

- Bu bağlantı senin için üretildi ve{" "} - tek kullanımlık. Aşağıdaki - butona tıkladığında KolayTercih hesabına giriş yapacaksın. -

-
- - - - - -
-

- Bu isteği sen yapmadıysan bu sayfayı kapatabilirsin — butona - tıklanmadığı sürece hesabında hiçbir şey değişmez. -

-
-
- - - - -
-
+ }> + {searchParams.then((sp) => ( + + ))} +
); diff --git a/src/app/giris/loading.tsx b/src/app/giris/loading.tsx deleted file mode 100644 index 0b363f7..0000000 --- a/src/app/giris/loading.tsx +++ /dev/null @@ -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 ( -
-
-
- {/* Başlık + alt metin */} - - - - - {/* Google ile devam et */} - - - {/* "veya" ayracı */} -
- - - -
- - {/* E-posta alanı + gönder butonu */} - - -
- - {/* Kart altındaki yasal metin */} -
- - -
-
-
- ); -} diff --git a/src/app/giris/page.tsx b/src/app/giris/page.tsx index db2bfe3..c8d2b10 100644 --- a/src/app/giris/page.tsx +++ b/src/app/giris/page.tsx @@ -1,95 +1,30 @@ +import { Suspense } from "react"; import type { Metadata } from "next"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { getSession } from "@/lib/session"; -import { PixelCorner } from "@/components/pixel-decor"; -import { GirisForm } from "./giris-form"; -import { GirisBaslik } from "./giris-baslik"; +import { + GirisKarti, + GirisKartiSkeleton, +} from "@/features/kullanici/components/giris-karti"; export const metadata: Metadata = { title: "Giriş", robots: { index: false, follow: true }, }; -export default async function GirisPage({ +export default function GirisPage({ searchParams, }: { 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 ( // -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).
-
-
- {baglantiHatasi && ( -
-

- Giriş bağlantısı geçersiz ya da süresi dolmuş -

-

- Bağlantılar 15 dakika geçerli ve tek kullanımlık. Aşağıdan - yeni bir bağlantı isteyebilirsin. -

-
- )} - -

- {funnel ? ( - <> - Seçimlerin hazır — giriş yap,{" "} - 5 deneme kredinle listen - hemen üretilsin. - - ) : ( - <> - Kayıt olduğunda{" "} - 5 deneme kredisi{" "} - hesabına tanımlanır — Yapay Zeka danışmana ücretsiz soru sorabilirsin. - - )} -

- -
-
- - - - -
-
-

- Giriş yaparak verilerinin tercih önerileri üretmek amacıyla{" "} - - gizlilik metnindeki - {" "} - gibi işlenmesini ve{" "} - - kullanım koşullarını - {" "} - kabul etmiş olursun. KolayTercih yerleşme garantisi vermez. -

+ }> + {searchParams.then(({ callback, hata }) => ( + + ))} +
); diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index d48bf18..18c75dd 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -2,7 +2,7 @@ import { Suspense } from "react"; import Image from "next/image"; import Link from "next/link"; 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"; export function SiteHeader() { diff --git a/src/features/kullanici/components/dogrula-karti.tsx b/src/features/kullanici/components/dogrula-karti.tsx new file mode 100644 index 0000000..d9f56ee --- /dev/null +++ b/src/features/kullanici/components/dogrula-karti.tsx @@ -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 ( +
+
+

Girişini onayla

+

+ Bu bağlantı senin için üretildi ve{" "} + tek kullanımlık. Aşağıdaki + butona tıkladığında KolayTercih hesabına giriş yapacaksın. +

+
+ + + + + +
+

+ Bu isteği sen yapmadıysan bu sayfayı kapatabilirsin — butona + tıklanmadığı sürece hesabında hiçbir şey değişmez. +

+
+
+ + + + +
+
+ ); +} + +export function DogrulaKartiSkeleton() { + return ( +
+ + + + + +
+ ); +} diff --git a/src/app/giris/giris-baslik.tsx b/src/features/kullanici/components/giris-baslik.tsx similarity index 100% rename from src/app/giris/giris-baslik.tsx rename to src/features/kullanici/components/giris-baslik.tsx diff --git a/src/app/giris/giris-form.tsx b/src/features/kullanici/components/giris-form.tsx similarity index 100% rename from src/app/giris/giris-form.tsx rename to src/features/kullanici/components/giris-form.tsx diff --git a/src/features/kullanici/components/giris-karti.tsx b/src/features/kullanici/components/giris-karti.tsx new file mode 100644 index 0000000..e93f716 --- /dev/null +++ b/src/features/kullanici/components/giris-karti.tsx @@ -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 ( + <> +
+
+ {baglantiHatasi && ( +
+

+ Giriş bağlantısı geçersiz ya da süresi dolmuş +

+

+ Bağlantılar 15 dakika geçerli ve tek kullanımlık. Aşağıdan + yeni bir bağlantı isteyebilirsin. +

+
+ )} + +

+ {funnel ? ( + <> + Seçimlerin hazır — giriş yap,{" "} + 5 deneme kredinle listen + hemen üretilsin. + + ) : ( + <> + Kayıt olduğunda{" "} + 5 deneme kredisi{" "} + hesabına tanımlanır — Yapay Zeka danışmana ücretsiz soru sorabilirsin. + + )} +

+ +
+
+ + + + +
+
+

+ Giriş yaparak verilerinin tercih önerileri üretmek amacıyla{" "} + + gizlilik metnindeki + {" "} + gibi işlenmesini ve{" "} + + kullanım koşullarını + {" "} + kabul etmiş olursun. KolayTercih yerleşme garantisi vermez. +

+ + ); +} + +// 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 ( + <> +
+ {/* Başlık + alt metin */} + + + + + {/* Google ile devam et */} + + + {/* "veya" ayracı */} +
+ + + +
+ + {/* E-posta alanı + gönder butonu */} + + +
+ + {/* Kart altındaki yasal metin */} +
+ + +
+ + ); +} diff --git a/src/components/sign-out-button.tsx b/src/features/kullanici/components/sign-out-button.tsx similarity index 100% rename from src/components/sign-out-button.tsx rename to src/features/kullanici/components/sign-out-button.tsx diff --git a/src/components/user-nav.tsx b/src/features/kullanici/components/user-nav.tsx similarity index 100% rename from src/components/user-nav.tsx rename to src/features/kullanici/components/user-nav.tsx