UX/CRO denetimi uygulandı: değer-önce funnel, güven sayfaları, dev paneli

Kritik (K1-K6) sonrası kalan tüm maddeler:
- Değer önce: girişsiz kullanıcı sihirbaz bitince /giris yerine /sonuc'taki
  ücretsiz tabloya gider (?hazir=1); AI CTA'sı "5 deneme kredinle listeni
  kur — giriş yap" paneli + sabit alt karttan verilir, girişte otomatik üretim
- Ö1-Ö10: paywall kopyası, aranabilir il seçimi, "Seçtiklerim" adlandırması,
  gizlilik/koşullar/iletişim sayfaları, danışman özeti + uyarılar, revizyon
  kutusu, dilim özeti + tek şehir uyarısı, eksik veri rozetleri, magic link
  yeniden gönder, mobil sohbet kısayolu
- G1-G5: binlik ayraçlı input + inline hata, adım göstergesi, route-level
  loading/error/not-found, dürüst konumlandırma kartı, dürüst buton etiketleri
- Seçtiklerim butonu liste boşken gizli; ilk eklemede belirir
- Dev süperadmin paneli (yalnızca development): hızlı giriş, kredi/paket/rapor
  senaryoları, sahte ödeme dönüşü

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

View File

@@ -229,8 +229,12 @@ export async function POST(request: NextRequest) {
reason: "refund",
refId: kullaniciMesajId,
});
console.error("[soru] AI istemcisi kurulamadı (anahtar eksik olabilir)");
return Response.json(
{ error: "AI altyapısı yapılandırılmadı." },
{
error:
"Danışman şu anda yanıt veremiyor; kredin iade edildi. Birazdan tekrar dener misin?",
},
{ status: 503 },
);
}

45
src/app/error.tsx Normal file
View File

@@ -0,0 +1,45 @@
"use client";
// Kök hata sınırı: beklenmedik sunucu/istemci hatalarında Next'in çıplak
// hata ekranı yerine markalı, kurtarılabilir bir ekran gösterir.
import { useEffect } from "react";
import Link from "next/link";
import { TriangleAlert } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error("[hata-siniri]", error);
}, [error]);
return (
<main className="mx-auto flex min-h-[60vh] w-full max-w-md flex-col items-center justify-center px-4 py-16 text-center">
<TriangleAlert className="size-10 text-amber-500" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold">
Bir şeyler ters gitti
</h1>
<p className="mt-2 text-sm leading-6 text-slate-600">
Beklenmedik bir hata oluştu. Tekrar dener misin? Sorun devam ederse
birkaç dakika sonra yeniden gel verilerin güvende.
</p>
<div className="mt-6 flex gap-3">
<Button
onClick={reset}
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
Tekrar dene
</Button>
<Button asChild variant="outline" className="cursor-pointer">
<Link href="/">Ana sayfaya dön</Link>
</Button>
</div>
</main>
);
}

View File

@@ -34,6 +34,21 @@ 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");
@@ -47,8 +62,8 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
}
}
async function linkGonder(e: React.FormEvent) {
e.preventDefault();
async function linkGonder(e?: React.FormEvent) {
e?.preventDefault();
if (!/^\S+@\S+\.\S+$/.test(email)) {
toast.error("Geçerli bir e-posta adresi gir.");
return;
@@ -64,17 +79,41 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
return;
}
setSent(true);
geriSayimBaslat();
}
if (sent) {
return (
<div className="mt-6 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ı 5 dakika geçerli.
</p>
<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ı 5 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>
);
}

View File

@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getSession } from "@/lib/session";
import { GirisForm } from "./giris-form";
@@ -46,8 +47,15 @@ export default async function GirisPage({
<GirisForm callbackURL={callbackURL} />
</div>
<p className="mt-6 text-center text-xs leading-relaxed text-slate-500">
Giriş yaparak verilerinin tercih önerileri üretmek amacıyla
işlenmesini kabul etmiş olursun. KolayTercih yerleşme garantisi vermez.
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>
</div>

66
src/app/gizlilik/page.tsx Normal file
View File

@@ -0,0 +1,66 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Gizlilik ve KVKK — KolayTercih",
};
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
export default function GizlilikPage() {
return (
<main className="mx-auto w-full max-w-2xl px-4 py-16">
<h1 className="font-heading text-3xl font-bold">
Gizlilik ve Kişisel Verilerin Korunması
</h1>
<p className="mt-2 text-sm text-slate-500">Son güncelleme: 26 Temmuz 2026</p>
<div className="mt-8 space-y-6 text-sm leading-7 text-slate-700">
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Hangi verileri işliyoruz?
</h2>
<p className="mt-2">
KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS
başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni
üretmek için), danışman sohbetine yazdığın mesajları (sorularını
yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler.
Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında
gerçekleşir.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Ne için kullanıyoruz?
</h2>
<p className="mt-2">
Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi
bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş bağlantısı,
ödeme bilgilendirmesi) göndermek için kullanılır. Verilerini
üçüncü taraflara satmayız, reklam amacıyla paylaşmayız.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Saklama ve silme
</h2>
<p className="mt-2">
Hesabını sildirmek veya verilerinin bir kopyasını istemek için{" "}
<Link href="/iletisim" className="text-primary underline">
iletişim sayfasındaki
</Link>{" "}
adresten bize yazabilirsin; talepler 30 gün içinde sonuçlandırılır.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Çerezler
</h2>
<p className="mt-2">
Yalnızca oturumunu ık tutmak için zorunlu çerezler kullanılır;
reklam veya izleme çerezi kullanılmaz.
</p>
</section>
</div>
</main>
);
}

30
src/app/iletisim/page.tsx Normal file
View File

@@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Mail } from "lucide-react";
export const metadata: Metadata = {
title: "İletişim — KolayTercih",
};
export default function IletisimPage() {
return (
<main className="mx-auto w-full max-w-2xl px-4 py-16">
<h1 className="font-heading text-3xl font-bold">İletişim</h1>
<p className="mt-4 text-sm leading-7 text-slate-700">
Ödeme, iade, hesap silme ya da başka bir konuda yardıma mı ihtiyacın
var? Tercih döneminde mesajlara aynı gün dönmeye çalışıyoruz.
</p>
<a
href="mailto:destek@kolaytercih.com"
className="mt-6 inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-semibold text-slate-900 shadow-xs transition-colors hover:border-slate-300 hover:bg-slate-50"
>
<Mail className="size-4 text-primary" aria-hidden />
destek@kolaytercih.com
</a>
<p className="mt-6 text-xs text-slate-500">
Ödemenle ilgili yazıyorsan e-postana sipariş tarihini ve kullandığın
hesap adresini eklersen daha hızlı yardımcı olabiliriz. Kart bilgini
asla e-postayla paylaşma.
</p>
</main>
);
}

74
src/app/kosullar/page.tsx Normal file
View File

@@ -0,0 +1,74 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Kullanım ve Satış Koşulları — KolayTercih",
};
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
export default function KosullarPage() {
return (
<main className="mx-auto w-full max-w-2xl px-4 py-16">
<h1 className="font-heading text-3xl font-bold">
Kullanım ve Satış Koşulları
</h1>
<p className="mt-2 text-sm text-slate-500">Son güncelleme: 26 Temmuz 2026</p>
<div className="mt-8 space-y-6 text-sm leading-7 text-slate-700">
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Hizmetin kapsamı
</h2>
<p className="mt-2">
KolayTercih, resmî YÖK Atlas verilerine dayanan bir karar destek
aracıdır. Öneriler bilgilendirme amaçlıdır; KolayTercih yerleşme
garantisi vermez ve ÖSYM ya da YÖK ile bağlantılı değildir. Tercih
listenin son hali ve başvuru sorumluluğu sana aittir.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Ücretlendirme
</h2>
<p className="mt-2">
Temel program arama ücretsizdir. Tercih Dönemi Paketi tek seferlik
bir satın almadır; abonelik değildir, otomatik yenilenmez. Paket
içeriği ve güncel fiyat{" "}
<Link href="/paket" className="text-primary underline">
paket sayfasında
</Link>{" "}
yazar.
</p>
</section>
<section id="iade" className="scroll-mt-24">
<h2 className="font-heading text-lg font-bold text-slate-900">
İade ve cayma
</h2>
<p className="mt-2">
Paket, satın alır almaz ifasına başlanan dijital bir içeriktir.
Buna rağmen kişisel listen henüz üretilmemişse (paket hiç
kullanılmamışsa) satın almadan itibaren 14 gün içinde koşulsuz
iade talep edebilirsin. Teknik bir sorun nedeniyle hizmeti
alamadıysan, kullanımdan bağımsız olarak ücretin iade edilir.
İade talepleri için{" "}
<Link href="/iletisim" className="text-primary underline">
bize yaz
</Link>
; talepler 14 gün içinde sonuçlandırılır ve iade, ödeme yaptığın
karta yapılır.
</p>
</section>
<section>
<h2 className="font-heading text-lg font-bold text-slate-900">
Hesap ve adil kullanım
</h2>
<p className="mt-2">
Krediler hesabına tanımlıdır, kişiseldir ve üçüncü kişilere
devredilemez. Hizmeti kötüye kullanan (otomatik istek, içerik
kazıma vb.) hesaplar kapatılabilir.
</p>
</section>
</div>
</main>
);
}

View File

@@ -5,6 +5,7 @@ import { SiteHeader } from "@/components/site-header";
import { ListeCekmecesi } from "@/components/manuel-liste/liste-cekmecesi";
import { HeaderGate } from "@/components/header-gate";
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
import { DevPanel } from "@/components/dev/dev-panel";
import "./globals.css";
const outfit = Outfit({
@@ -63,6 +64,8 @@ export default function RootLayout({
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
<ListeCekmecesi />
<Toaster position="top-center" />
{/* Dev süperadmin paneli — prod build'de hiç render edilmez */}
{process.env.NODE_ENV !== "production" ? <DevPanel /> : null}
</body>
</html>
);

View File

@@ -0,0 +1,19 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function ListemLoading() {
return (
<main className="mx-auto w-full max-w-7xl px-4 py-10">
<Skeleton className="h-6 w-40 rounded-full" />
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
<div className="mt-8 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_340px]">
<div className="flex flex-col gap-2">
{Array.from({ length: 10 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full rounded-xl" />
))}
</div>
<Skeleton className="h-[480px] w-full rounded-2xl" />
</div>
</main>
);
}

View File

@@ -3,12 +3,14 @@ import Link from "next/link";
import { eq } from "drizzle-orm";
import { redirect } from "next/navigation";
import {
AlertTriangle,
ArrowRight,
Bot,
Check,
Database,
FileText,
Lock,
MessageCircleQuestion,
PencilLine,
ShieldCheck,
Sparkles,
@@ -16,7 +18,7 @@ import {
import { verifySession, getCurrentUser } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { PUAN_TURLERI } from "@/lib/db";
import { URUNLER } from "@/lib/credits";
import { URUNLER, MAX_REVIZYON } from "@/lib/credits";
import type { RaporSonuc } from "@/lib/ai/rapor";
import type { RaporParams } from "@/lib/rapor-havuzu";
import { Button } from "@/components/ui/button";
@@ -24,6 +26,7 @@ import { Badge } from "@/components/ui/badge";
import { RaporListesi } from "@/components/rapor-listesi";
import { raporMaskele, ACIK_SATIR } from "@/lib/rapor-maske";
import { SohbetClient } from "./sohbet-client";
import { RevizyonKutusu } from "./revizyon-kutusu";
export const metadata: Metadata = {
title: "Listem — KolayTercih",
@@ -33,11 +36,19 @@ const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
maximumFractionDigits: 0,
});
export default async function ListemPage() {
export default async function ListemPage({
searchParams,
}: {
searchParams: Promise<{ acildi?: string }>;
}) {
await verifySession("/listem");
const user = await getCurrentUser();
if (!user) redirect("/giris?callback=/listem");
// ?acildi=1: ödeme dönüşü — kilidin kalktığı tek seferlik açılış deneyimi
const { acildi } = await searchParams;
const kilitAcilisi = Boolean(user.hasPaket && acildi === "1");
const satir = await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, user.id),
});
@@ -73,12 +84,29 @@ export default async function ListemPage() {
return (
<main className="mx-auto w-full max-w-7xl px-4 py-8 sm:py-10">
{kilitAcilisi ? (
<div
role="status"
className="mb-6 flex items-center gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800"
>
<Sparkles className="size-4 shrink-0" aria-hidden />
Paketin aktif 24 tercihinin tamamı, gerekçeler, riskler ve PDF
artık ık. 🎉
</div>
) : null}
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<Badge variant="secondary" className="mb-3">
<Database aria-hidden />
YÖK Atlas verileri ücretsiz
</Badge>
{user.hasPaket ? (
<Badge variant="secondary" className="mb-3">
<ShieldCheck aria-hidden />
Paketin aktif
</Badge>
) : (
<Badge variant="secondary" className="mb-3">
<Database aria-hidden />
YÖK Atlas verileri ücretsiz
</Badge>
)}
<h1 className="max-w-2xl font-heading text-2xl font-bold tracking-tight sm:text-3xl">
{user.hasPaket
? "Tercih karar merkezin"
@@ -86,8 +114,10 @@ export default async function ListemPage() {
</h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-600">
{params.sira.toLocaleString("tr-TR")}. sıra ·{" "}
{PUAN_TURLERI[params.tur]} · 24 programın 20242025 taban
sıralamalarını ve ham trendini aşağıda ücretsiz inceleyebilirsin.
{PUAN_TURLERI[params.tur]} ·{" "}
{user.hasPaket
? "24 tercihinin tamamını gerekçe, risk analizi ve trendleriyle aşağıda inceleyebilirsin."
: "24 programın 20242025 taban sıralamalarını ve ham trendini aşağıda ücretsiz inceleyebilirsin."}
</p>
</div>
{user.hasPaket ? (
@@ -98,9 +128,7 @@ export default async function ListemPage() {
size="sm"
className="cursor-pointer"
>
<Link
href={`/sonuc?sira=${params.sira}&tur=${params.tur}&sihirbaz=1`}
>
<Link href="#revizyon">
<PencilLine className="size-4" aria-hidden />
Revize et
</Link>
@@ -120,8 +148,49 @@ export default async function ListemPage() {
) : null}
</div>
{/* AI'ın genel değerlendirmesi ve uyarıları: paketliye tam,
paketsize sunucuda kırpılmış önizleme + kilit notu */}
{rapor.genelDegerlendirme ? (
<section className="mt-6 rounded-2xl border border-slate-200 bg-white p-5">
<h2 className="flex items-center gap-2 font-heading text-base font-bold">
<Bot className="size-4 text-primary" aria-hidden />
Danışman özeti
</h2>
<p className="mt-2 text-sm leading-6 text-slate-700">
{rapor.genelDegerlendirme}
</p>
{user.hasPaket && rapor.uyarilar?.length ? (
<ul className="mt-3 flex flex-col gap-2">
{rapor.uyarilar.map((uyari) => (
<li
key={uyari}
className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900"
>
<AlertTriangle
className="mt-0.5 size-4 shrink-0"
aria-hidden
/>
{uyari}
</li>
))}
</ul>
) : null}
{!user.hasPaket ? (
<p className="mt-3 flex items-center gap-1.5 text-xs text-slate-500">
<Lock className="size-3.5" aria-hidden />
Değerlendirmenin tamamı ve sıralamana özel uyarılar pakette.
</p>
) : null}
</section>
) : null}
<div className="mt-8 grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_340px]">
<section className="min-w-0 rounded-2xl border border-slate-200 bg-slate-50/60 p-3 sm:p-5">
<div className="flex min-w-0 flex-col gap-6">
<section
className={`min-w-0 rounded-2xl border border-slate-200 bg-slate-50/60 p-3 sm:p-5${
kilitAcilisi ? " kt-unblur" : ""
}`}
>
<RaporListesi
rapor={rapor}
adaySira={params.sira}
@@ -134,11 +203,12 @@ export default async function ListemPage() {
</span>
<div>
<p className="font-heading font-bold text-slate-900">
Ham veri burada bitmiyor aşağıdaki tüm satırlar ık
İlk {ACIK_SATIR} tercihte AI analizini gördün
</p>
<p className="mt-1 max-w-xl text-sm text-slate-600">
Paket; kalan {24 - ACIK_SATIR} tercih in kişisel risk,
gerekçe ve AI yorumunu açar.
Kalan {24 - ACIK_SATIR} tercihin kişisel risk, gerekçe ve
AI yorumu paketle ılır. Program adları ve taban verileri
her zaman ücretsiz.
</p>
</div>
</div>
@@ -147,7 +217,7 @@ export default async function ListemPage() {
className="h-11 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600 sm:w-auto"
>
<Link href="/paket">
Analizleri
Analizleri {PAKET_FIYATI} TL
<ArrowRight data-icon="inline-end" aria-hidden />
</Link>
</Button>
@@ -156,10 +226,18 @@ export default async function ListemPage() {
/>
</section>
<div className="lg:sticky lg:top-28">
{user.hasPaket ? (
<RevizyonKutusu
kalanHak={Math.max(0, MAX_REVIZYON - (satir.revisionCount ?? 0))}
/>
) : null}
</div>
<div id="sohbet" className="scroll-mt-24 lg:sticky lg:top-28">
{user.hasPaket ? (
<SohbetClient baslangicKredi={user.creditBalance} />
) : (
<div className="flex flex-col gap-6">
<section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<div className="border-b border-slate-100 p-6">
<div className="flex items-center gap-2 text-primary">
@@ -230,9 +308,27 @@ export default async function ListemPage() {
</div>
</div>
</section>
{/* Deneme kredileri sohbette geçerli: AI'ı satın almadan önce
tattırır (kalan kredi biterse composer paket CTA'sına döner). */}
<SohbetClient
baslangicKredi={user.creditBalance}
className="h-[480px]"
/>
</div>
)}
</div>
</div>
{/* Mobilde sohbet sağ rayla birlikte en alta düşer; yüzen kısayol
kullanıcıyı 24 satırı kaydırmadan danışmana ulaştırır. */}
<a
href="#sohbet"
className="fixed bottom-5 right-4 z-40 inline-flex h-11 items-center gap-2 rounded-full bg-primary px-4 text-sm font-semibold text-white shadow-lg transition-transform duration-200 active:scale-[0.97] lg:hidden print:hidden"
>
<MessageCircleQuestion className="size-4" aria-hidden />
Danışmana sor
</a>
</main>
);
}

View File

@@ -0,0 +1,96 @@
"use client";
// Paketlinin geri bildirime dayalı liste revizyonu (/listem üzerinde).
// listeRevize sunucu aksiyonunu çağırır; başarıda sayfayı yeniler ki
// sunucuda güncellenen rapor ve kredi bakiyesi ekrana yansısın.
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Loader2, RefreshCcw } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { listeRevize } from "@/app/sonuc/actions";
export function RevizyonKutusu({ kalanHak }: { kalanHak: number }) {
const [feedback, setFeedback] = useState("");
const [pending, setPending] = useState(false);
const router = useRouter();
if (kalanHak <= 0) {
return (
<section
id="revizyon"
className="scroll-mt-24 rounded-2xl border border-slate-200 bg-white p-5"
>
<p className="text-sm text-slate-500">
Revizyon hakların doldu. Danışman sohbetinden listen hakkında soru
sormaya devam edebilirsin.
</p>
</section>
);
}
async function revizeEt() {
setPending(true);
try {
const sonuc = await listeRevize({
feedback: feedback.trim(),
requestId: crypto.randomUUID(),
});
if (sonuc.ok) {
toast.success("Listen yeniden kuruldu.");
setFeedback("");
router.refresh();
} else {
toast.error(sonuc.error);
}
} catch {
toast.error("Beklenmeyen bir hata oluştu, tekrar dener misin?");
} finally {
setPending(false);
}
}
return (
<section
id="revizyon"
className="scroll-mt-24 rounded-2xl border border-slate-200 bg-white p-5"
>
<h2 className="font-heading text-base font-bold">
Listede değişiklik mi istiyorsun?
</h2>
<p className="mb-3 mt-1 text-sm text-slate-600">
Ne değişsin istediğini yaz; liste aynı gerçek veriyle yeniden kurulsun
(3 kredi).
</p>
<textarea
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
rows={2}
maxLength={1000}
placeholder="ör. İlk 5 tercihte daha fazla İstanbul olsun, garanti dilimini genişlet…"
disabled={pending}
className="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
<div className="mt-3 flex items-center gap-3">
<Button
type="button"
variant="outline"
disabled={pending || feedback.trim().length < 5}
onClick={revizeEt}
className="cursor-pointer"
>
{pending ? (
<Loader2 className="size-4 animate-spin" aria-hidden />
) : (
<RefreshCcw className="size-4" aria-hidden />
)}
{pending ? "Yeniden kuruluyor…" : "Revize et (3 kredi)"}
</Button>
<span className="text-xs text-slate-500">
Kalan hak: <span className="font-semibold">{kalanHak}</span>
</span>
</div>
</section>
);
}

13
src/app/loading.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Loader2 } from "lucide-react";
export default function Loading() {
return (
<main
aria-live="polite"
className="flex min-h-[60vh] flex-col items-center justify-center gap-3"
>
<Loader2 className="size-8 animate-spin text-primary" aria-hidden />
<p className="text-sm text-slate-500">Yükleniyor</p>
</main>
);
}

24
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,24 @@
import Link from "next/link";
import { Compass } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function NotFound() {
return (
<main className="mx-auto flex min-h-[60vh] w-full max-w-md flex-col items-center justify-center px-4 py-16 text-center">
<Compass className="size-10 text-primary" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold">
Böyle bir sayfa yok
</h1>
<p className="mt-2 text-sm leading-6 text-slate-600">
Aradığın sayfa taşınmış ya da hiç var olmamış olabilir. Sıralamanı
girip listeni kurmaya ana sayfadan devam edebilirsin.
</p>
<Button
asChild
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href="/">Ana sayfaya dön</Link>
</Button>
</main>
);
}

View File

@@ -39,20 +39,14 @@ export default async function OdemeSonucPage({
const basarili = durum === "paid";
// Rapor zaten üretildiyse ödül bir tık uzakta: kullanıcıyı doğrudan
// sonuç sayfasına döndür ki blur çözülme animasyonuyla tam listeyi görsün.
// rapor sayfasına döndür ki blur çözülme animasyonuyla tam listeyi görsün.
const rapor = basarili
? await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, session.user.id),
columns: { params: true },
columns: { id: true },
})
: null;
const raporParams = rapor?.params as
| { sira?: number; tur?: string }
| undefined;
const listeHref =
raporParams?.sira && raporParams?.tur
? `/sonuc?sira=${raporParams.sira}&tur=${raporParams.tur}&acildi=1`
: null;
const listeHref = rapor ? "/listem?acildi=1" : null;
return (
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
@@ -101,9 +95,18 @@ export default async function OdemeSonucPage({
Ödeme tamamlanamadı
</h1>
<p className="mt-2 text-sm text-slate-600">
{durum === "pending"
? "Ödemen hâlâ işleniyor. Birkaç saniye sonra bu sayfayı yenile — kartından çekim yapıldıysa krediler mutlaka tanımlanır."
: "Çekim yapılmadı ya da işlem iptal edildi. Kartından para çekildiğini düşünüyorsan bizimle iletişime geç."}
{durum === "pending" ? (
"Ödemen hâlâ işleniyor. Birkaç saniye sonra bu sayfayı yenile — kartından çekim yapıldıysa krediler mutlaka tanımlanır."
) : (
<>
Çekim yapılmadı ya da işlem iptal edildi. Kartından para
çekildiğini düşünüyorsan{" "}
<Link href="/iletisim" className="underline">
bizimle iletişime geç
</Link>
.
</>
)}
</p>
<Button asChild variant="outline" className="mt-6 h-11 cursor-pointer">
<Link href="/paket">Tekrar dene</Link>

View File

@@ -9,7 +9,10 @@ import {
Wallet,
X,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { HeroForm } from "@/components/hero-form";
import { URUNLER } from "@/lib/credits";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -33,6 +36,13 @@ import {
TableRow,
} from "@/components/ui/table";
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
maximumFractionDigits: 0,
});
const TOPUP_FIYATI = (URUNLER.topup.amountKurus / 100).toLocaleString("tr-TR", {
maximumFractionDigits: 0,
});
const problems = [
{
icon: ListChecks,
@@ -96,8 +106,7 @@ const faqs = [
},
{
question: "Fiyatlandırma nasıl çalışıyor?",
answer:
"Abonelik yok. Tercih dönemi boyunca geçerli tek seferlik paket alırsın: yapay zekâ destekli kişisel 24'lük liste, risk analizi, liste revizyonları ve listen hakkında soru hakkı dahil. Temel program arama ise herkes için ücretsiz.",
answer: `Abonelik yok. Tercih dönemi boyunca geçerli tek seferlik ${PAKET_FIYATI} TL'lik paket alırsın: yapay zekâ destekli kişisel 24'lük liste, risk analizi, liste revizyonları ve ${URUNLER.paket.credits} soru hakkı dahil. Soru hakların biterse ${TOPUP_FIYATI} TL'ye +${URUNLER.topup.credits} soru ekleyebilirsin. Temel program arama ise herkes için ücretsiz.`,
},
];
@@ -155,7 +164,7 @@ export default function Home() {
variant="secondary"
className="mb-6 bg-primary/10 text-primary"
>
2026 YKS tercih dönemi ıkk
2026 YKS tercih dönemi ık
</Badge>
<h1 className="max-w-3xl font-heading text-4xl font-bold tracking-tight sm:text-6xl">
Dört yıllık geleceğini{" "}
@@ -173,27 +182,25 @@ export default function Home() {
<HeroForm />
</div>
<p className="mt-4 text-sm text-slate-500">
Temel arama her zaman ücretsiz · Kredi kartı gerekmez
Temel arama her zaman ücretsiz · Kredi kartı gerekmez · Tam analiz
paketi tek seferlik {PAKET_FIYATI} TL
</p>
{/* Sosyal kanıt: henüz kullanıcı yok — dürüstçe veri istatistikleri */}
<dl className="mt-16 grid w-full max-w-3xl grid-cols-1 gap-4 sm:grid-cols-3">
{[
{ value: "23.000+", label: "Taranan lisans programı" },
{ value: "4 yıl", label: "Taban sıralaması trend verisi" },
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
].map((stat) => (
<div
key={stat.label}
className="rounded-2xl border border-slate-200 bg-white p-6"
>
<dt className="text-sm text-slate-500">{stat.label}</dt>
<dd className="mt-1 font-heading text-3xl font-bold text-primary">
{stat.value}
</dd>
</div>
))}
</dl>
<div className="mt-3 inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-left text-xs text-slate-700 shadow-xs">
<Image
src="/yok-logo.svg"
alt="Yükseköğretim Kurulu"
width={68}
height={33}
className="h-7 w-auto shrink-0"
/>
<span className="h-5 w-px shrink-0 bg-slate-200" aria-hidden />
<span>
<strong className="font-semibold text-slate-900">
YÖK Atlas&apos;ın gerçek verileriyle
</strong>{" "}
güvenilir sonuçlar
</span>
</div>
</section>
{/* Problem */}
@@ -247,6 +254,43 @@ export default function Home() {
</div>
))}
</div>
{/* Veriye dayalı güven + garanti vaadine karşı dürüst konumlandırma */}
<div className="mx-auto mt-12 w-full max-w-3xl overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<dl className="grid grid-cols-1 divide-y divide-slate-200 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
{[
{ value: "23.000+", label: "Taranan lisans programı" },
{ value: "4 yıl", label: "Taban sıralaması trend verisi" },
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
].map((stat) => (
<div key={stat.label} className="p-6 text-center">
<dt className="text-sm text-slate-500">{stat.label}</dt>
<dd className="mt-1 font-heading text-3xl font-bold text-primary">
{stat.value}
</dd>
</div>
))}
</dl>
<div className="flex gap-4 border-t border-slate-200 bg-primary/5 p-6 text-left sm:items-center sm:p-7">
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary text-white shadow-sm">
<ShieldCheck className="size-5" aria-hidden />
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-primary">
Dürüstlük notu
</p>
<p className="mt-1 font-heading text-lg font-bold text-slate-900 sm:text-xl">
Yerleşme garantisi vermiyoruz çünkü kimse veremez.
</p>
<p className="mt-1.5 text-sm leading-6 text-slate-600">
Taban sıralamaları her yıl değişir. Biz sonuç vaadi satmak
yerine riski satır satır gösterir, daha güvenli bir liste
kurmana yardımcı oluruz.
</p>
</div>
</div>
</div>
</div>
</section>
@@ -341,7 +385,7 @@ export default function Home() {
</section>
</main>
<footer className="border-t border-slate-200 bg-white py-10">
<footer className="border-t border-slate-200 bg-white pb-32 pt-10">
<div className="mx-auto max-w-6xl px-4 text-center text-sm leading-relaxed text-slate-500">
<p>
© 2026 KolayTercih. Veriler resmî YÖK Atlas kaynağından derlenir;
@@ -351,6 +395,20 @@ export default function Home() {
KolayTercih bir karar destek aracıdır, yerleşme garantisi vermez.
Tercih listenizin son hali ve başvuru sorumluluğu size aittir.
</p>
<nav
aria-label="Yasal sayfalar"
className="mt-4 flex items-center justify-center gap-4"
>
<Link href="/gizlilik" className="hover:text-slate-700 hover:underline">
Gizlilik & KVKK
</Link>
<Link href="/kosullar" className="hover:text-slate-700 hover:underline">
Kullanım & iade koşulları
</Link>
<Link href="/iletisim" className="hover:text-slate-700 hover:underline">
İletişim
</Link>
</nav>
</div>
</footer>
</div>

View File

@@ -50,9 +50,11 @@ export async function baslatOdeme(
});
} catch (err) {
if (err instanceof Error && err.message === "IYZICO_KEYS_MISSING") {
// Yapılandırma detayı log'a; kullanıcıya altyapı sızdırmayan mesaj
console.error("[odeme] iyzico anahtarları eksik — ödeme başlatılamadı");
return {
error:
"Ödeme altyapısı henüz yapılandırılmadı (iyzico anahtarları eksik).",
"Ödeme şu anda başlatılamıyor; kartından çekim yapılmadı. Birazdan tekrar dener misin?",
};
}
throw err;

View File

@@ -40,12 +40,29 @@ const OZELLIKLER = [
},
] as const;
export default async function PaketPage() {
export default async function PaketPage({
searchParams,
}: {
searchParams: Promise<{ hata?: string }>;
}) {
const u = await getCurrentUser();
const { hata } = await searchParams;
// Ödeme callback'i doğrulama hatasında buraya ?hata=token|siparis ile döner
const odemeHatasi = hata === "token" || hata === "siparis";
return (
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-16">
{odemeHatasi ? (
<div
role="alert"
className="mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800"
>
Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin birkaç
dakika içinde otomatik tanımlanır; tanımlanmazsa aşağıdan tekrar
deneyebilirsin çekim yapılmadıysa hiçbir ücret ödemedin.
</div>
) : null}
<div className="text-center">
<Badge variant="secondary" className="bg-primary/10 text-primary">
Tek seferlik ödeme · Abonelik yok
@@ -111,7 +128,13 @@ export default async function PaketPage() {
)}
</div>
<p className="mt-4 text-center text-xs text-slate-500">
iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz
iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "}
<Link
href="/kosullar#iade"
className="underline hover:text-slate-700"
>
İade koşulları
</Link>
</p>
</section>
@@ -149,7 +172,11 @@ export default async function PaketPage() {
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas verisine
dayanır ancak yerleşme garantisi verilmez. Tercih listesinin nihai
sorumluluğu adaya aittir.
sorumluluğu adaya aittir. Sorun mu var?{" "}
<Link href="/iletisim" className="underline hover:text-slate-700">
Bize yaz
</Link>
.
</p>
</main>
</div>

View File

@@ -4,7 +4,12 @@ import { eq, sql } from "drizzle-orm";
import { getSession, getCurrentUser } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { PUAN_TURLERI, type PuanTuruKey } from "@/lib/db";
import { spendCredits, grantCredits, RAPOR_KREDI } from "@/lib/credits";
import {
spendCredits,
grantCredits,
RAPOR_KREDI,
MAX_REVIZYON,
} from "@/lib/credits";
import {
raporUret,
listeOzetiCikar,
@@ -15,7 +20,6 @@ import type { RaporParams } from "@/lib/rapor-havuzu";
import { sihirbazDogrula, type SihirbazSecimleri } from "@/lib/sihirbaz";
import { raporMaskele } from "@/lib/rapor-maske";
const MAX_REVIZYON = 2;
// Modal yerinde render ettiği için action redirect etmez; sonucu döner.
export type ListeSonuc =
@@ -38,7 +42,9 @@ function siraTurGecerli(sira: number, tur: string): tur is PuanTuruKey {
function aiHataMesaji(err: unknown): string {
if (err instanceof RaporUretimHatasi) return err.message;
if (err instanceof Error && err.message === "AI_KEY_MISSING") {
return "AI altyapısı henüz yapılandırılmadı (ANTHROPIC_API_KEY eksik).";
// Yapılandırma detayı log'a; kullanıcıya altyapı sızdırmayan mesaj
console.error("[liste] ANTHROPIC_API_KEY eksik — üretim başlatılamadı");
return "Liste üretimi şu anda başlatılamıyor. Birazdan tekrar dener misin? Harcanan kredin otomatik iade edilir.";
}
console.error("[liste] üretim hatası:", err);
return "Liste üretilirken bir sorun oluştu. Birazdan tekrar dener misin?";

19
src/app/sonuc/loading.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Skeleton } from "@/components/ui/skeleton";
// /sonuc senkron SQLite sorgularıyla açılır; yavaş bağlantıda beyaz ekran
// yerine sayfanın iskeletini göster.
export default function SonucLoading() {
return (
<main className="mx-auto w-full max-w-6xl px-4 py-10">
<Skeleton className="h-6 w-40 rounded-full" />
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
<Skeleton className="mt-8 h-36 w-full rounded-2xl" />
<div className="mt-8 flex flex-col gap-2">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full rounded-xl" />
))}
</div>
</main>
);
}

View File

@@ -40,6 +40,7 @@ export default async function SonucPage({
tur?: string;
sihirbaz?: string;
acildi?: string;
hazir?: string;
}>;
}) {
const params = await searchParams;
@@ -145,6 +146,7 @@ export default async function SonucPage({
mevcutRapor={mevcutRapor}
otomatikAc={params.sihirbaz === "1"}
acilis={params.acildi === "1"}
secimlerHazir={params.hazir === "1"}
/>
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">

View File

@@ -40,8 +40,12 @@ type BekleyenSecim = {
* - Sayfa ortasındaki büyük CTA + sabit alt çağrı kartı (rapor yokken)
* - Adımları toplayan modal
* - Üretim/önizleme/liste paneli (sayfa gövdesinde)
* Akış: adımlar → (girişsizse /giris) → kredi ile üretim → paketsizse
* progressive-blur önizleme, paketliyse tam liste.
* Akış: adımlar → kredi ile üretim → paketsizse progressive-blur önizleme,
* paketliyse tam liste. Girişsiz kullanıcı hero'dan seçimleri tamamlanmış
* gelirse önce ücretsiz tabloyu görür; giriş CTA'sı "hazır seçimlerin
* kaydedilmesi" olarak panelde sunulur, girişten dönüşte üretim otomatik
* başlar. Modal içinden ilerleyen girişsiz kullanıcı ise butonun söylediği
* gibi doğrudan girişe gider.
*/
export function SihirbazBolumu({
sira,
@@ -53,6 +57,7 @@ export function SihirbazBolumu({
mevcutRapor,
otomatikAc,
acilis,
secimlerHazir,
}: {
sira: number;
tur: string;
@@ -65,6 +70,10 @@ export function SihirbazBolumu({
otomatikAc?: boolean;
/** ?acildi=1 (ödeme dönüşü): blur çözülme animasyonuyla aç */
acilis?: boolean;
/** ?hazir=1 (hero sihirbazından girişsiz geliş): seçimler storage'da,
panel giriş CTA'sına döner. URL'den gelir ki SSR'da doğru panel
render edilsin — mount sonrası anahtar değişimi animasyonu donduruyor. */
secimlerHazir?: boolean;
}) {
const router = useRouter();
const monteEdildi = useMonteEdildi();
@@ -280,8 +289,25 @@ export function SihirbazBolumu({
}
const panelde = Boolean(rapor) || durum !== "bos";
// Değer önce: sihirbazı bitirmiş ama girişsiz kullanıcı ücretsiz tabloyu
// zaten görüyor; giriş burada bariyer değil, hazır seçimlerin (ödülün)
// kaydedilmesi olarak sunulur. Girişten dönüşte otomatik üretim başlar.
// Sinyal URL'den (?hazir=1) gelir; storage'a bakılsaydı panel anahtarı
// mount sonrası değişir ve AnimatePresence geçişi donardı.
const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !rapor);
const girisUrl = `/giris?callback=${encodeURIComponent(
`/sonuc?sira=${sira}&tur=${tur}&sihirbaz=1`,
)}`;
const panelAnahtari =
durum !== "bos" ? durum : rapor ? "rapor-hazir" : "sihirbaz-cta";
durum !== "bos"
? durum
: rapor
? "rapor-hazir"
: girisBekliyor
? "giris-cta"
: "sihirbaz-cta";
return (
<>
@@ -370,6 +396,31 @@ export function SihirbazBolumu({
<Link href="/listem">Listeni gör</Link>
</Button>
</section>
) : girisBekliyor ? (
<section className="mt-16 rounded-2xl bg-primary p-8 text-center text-white sm:p-12">
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
Seçimlerin hazır AI listen bir adım uzakta
</h2>
<p className="mx-auto mt-3 max-w-xl text-white/90">
Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
tercihlik AI listeni de kurabilirsin: ücretsiz giriş yap,
seçimlerin kayıtlı listen otomatik kurulsun (üretim 3
kredi, kalan 2 kredinle danışmana soru sorarsın).
</p>
<Button
asChild
size="lg"
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href={girisUrl}>
<Sparkles className="size-4" aria-hidden />5 deneme kredinle
AI listeni kur giriş yap
</Link>
</Button>
<p className="mt-3 text-xs text-white/70">
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
</p>
</section>
) : (
<section className="mt-16 rounded-2xl bg-primary p-8 text-center text-white sm:p-12">
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
@@ -400,19 +451,33 @@ export function SihirbazBolumu({
<div className="pointer-events-auto mx-auto flex max-w-xl items-center gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-xl">
<div className="flex-1">
<p className="font-heading text-sm font-bold text-slate-900">
3 adımda 24 tercihlik listen
{girisBekliyor
? "Seçimlerin kayıtlı — AI listen hazır bekliyor"
: "3 adımda 24 tercihlik listen"}
</p>
<p className="text-xs text-slate-500">
Sıralamana uygun bölümleri seç, yapay zekâ listeni kursun.
{girisBekliyor
? "Ücretsiz giriş yap, 5 deneme kredinle listen otomatik kurulsun."
: "Sıralamana uygun bölümleri seç, yapay zekâ listeni kursun."}
</p>
</div>
<Button
size="sm"
onClick={() => acikDegisti(true)}
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
Başla
</Button>
{girisBekliyor ? (
<Button
asChild
size="sm"
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href={girisUrl}>Giriş yap</Link>
</Button>
) : (
<Button
size="sm"
onClick={() => acikDegisti(true)}
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
Başla
</Button>
)}
<button
type="button"
aria-label="Kapat"
@@ -441,7 +506,7 @@ export function SihirbazBolumu({
sonSecimler ?? (bekleyen?.eslesir ? bekleyen.secimler : null)
}
sonButonEtiketi={
girisliMi ? "Listemi oluştur (3 kredi)" : "Listemi gör"
girisliMi ? "Listemi oluştur (3 kredi)" : "Giriş yap ve listeni gör"
}
onTamamla={ilerle}
/>