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:
@@ -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
45
src/app/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
66
src/app/gizlilik/page.tsx
Normal 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 açı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
30
src/app/iletisim/page.tsx
Normal 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
74
src/app/kosullar/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
19
src/app/listem/loading.tsx
Normal file
19
src/app/listem/loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 açı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 2024–2025 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 2024–2025 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 açı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 iç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 açı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 aç
|
||||
Analizleri aç — {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>
|
||||
);
|
||||
}
|
||||
|
||||
96
src/app/listem/revizyon-kutusu.tsx
Normal file
96
src/app/listem/revizyon-kutusu.tsx
Normal 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
13
src/app/loading.tsx
Normal 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
24
src/app/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
106
src/app/page.tsx
106
src/app/page.tsx
@@ -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 açıkk
|
||||
2026 YKS tercih dönemi açı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'ı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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
19
src/app/sonuc/loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user