feat: kimlik doğrulama, ödeme, kredi ve AI rapor sihirbazı ekle

- better-auth ile giriş/oturum yönetimi (src/lib/auth, giris sayfası)
- iyzico ödeme entegrasyonu ve paket satın alma akışı
- kredi sistemi ve uygulama veritabanı şeması (drizzle)
- AI destekli rapor üretimi ve tercih sihirbazı
- rapor yazdırma sayfası ve site header/kullanıcı menüsü

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bilalgursen
2026-07-22 02:13:39 +03:00
parent 7c3f17b9b5
commit cee6200237
64 changed files with 7966 additions and 89 deletions

View File

@@ -0,0 +1,110 @@
import type { Metadata } from "next";
import { eq } from "drizzle-orm";
import { redirect } from "next/navigation";
import { verifySession, getCurrentUser } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { PUAN_TURLERI } from "@/lib/db";
import type { RaporSonuc } from "@/lib/ai/rapor";
import type { RaporParams } from "@/lib/rapor-havuzu";
import { YazdirButonu } from "./yazdir-butonu";
export const metadata: Metadata = {
title: "Tercih Raporu (Yazdır) — KolayTercih",
};
const DILIM_LABEL: Record<string, string> = {
hayal: "Hayal",
dengeli: "Dengeli",
garanti: "Garanti",
};
export default async function YazdirPage() {
await verifySession("/rapor/yazdir");
const user = await getCurrentUser();
if (!user) redirect("/giris");
if (!user.hasPaket) redirect("/paket");
const satir = await appDb.query.reports.findFirst({
where: eq(schema.reports.userId, user.id),
});
if (!satir?.result) redirect("/sonuc");
const rapor = satir.result as RaporSonuc;
const params = satir.params as RaporParams;
const tarih = new Date(satir.updatedAt).toLocaleDateString("tr-TR", {
day: "numeric",
month: "long",
year: "numeric",
});
return (
<div className="mx-auto max-w-3xl bg-white p-8 text-slate-900 print:p-0">
<YazdirButonu />
<header className="border-b-2 border-slate-900 pb-4">
<h1 className="font-heading text-2xl font-bold">
KolayTercih 24 Tercihlik Kişisel Liste ve Risk Raporu
</h1>
<p className="mt-1 text-sm text-slate-600">
Başarı sıralaması: {params.sira.toLocaleString("tr-TR")} · Puan
türü: {PUAN_TURLERI[params.tur]}
{params.il ? ` · İl: ${params.il}` : ""} · {tarih}
</p>
</header>
<section className="mt-5">
<h2 className="font-heading text-base font-bold">
Genel değerlendirme
</h2>
<p className="mt-1 whitespace-pre-line text-sm leading-relaxed">
{rapor.genelDegerlendirme}
</p>
</section>
<table className="mt-5 w-full border-collapse text-xs">
<thead>
<tr className="border-b-2 border-slate-900 text-left">
<th className="py-2 pr-2">#</th>
<th className="py-2 pr-2">Program / Üniversite</th>
<th className="py-2 pr-2">Dilim</th>
<th className="py-2 pr-2">Gerekçe</th>
<th className="py-2 pr-2">Risk notu</th>
<th className="py-2">Trend</th>
</tr>
</thead>
<tbody>
{rapor.tercihler.map((t) => {
const p = rapor.programlar[t.programId];
return (
<tr
key={t.sira}
className="break-inside-avoid border-b border-slate-200 align-top"
>
<td className="py-2 pr-2 font-bold">{t.sira}</td>
<td className="py-2 pr-2">
<span className="font-semibold">{p?.isim}</span>
<br />
<span className="text-slate-600">
{p?.universite} · {p?.il ?? "—"}
</span>
</td>
<td className="py-2 pr-2">{DILIM_LABEL[t.dilim]}</td>
<td className="py-2 pr-2">{t.gerekce}</td>
<td className="py-2 pr-2">{t.riskNotu}</td>
<td className="py-2">{t.trendOzeti}</td>
</tr>
);
})}
</tbody>
</table>
<footer className="mt-6 border-t border-slate-300 pt-3 text-[10px] leading-relaxed text-slate-500">
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi (20212025 taban
sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak
üretilmiştir. Taban sıralamaları her yıl değişir; bu rapor yerleşme
garantisi vermez. Tercih listesinin ÖSYM sistemine girilmesi ve nihai
sorumluluğu adaya aittir.
</footer>
</div>
);
}

View File

@@ -0,0 +1,22 @@
"use client";
import { Printer } from "lucide-react";
import { Button } from "@/components/ui/button";
export function YazdirButonu() {
return (
<div className="mb-6 flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 print:hidden">
<p className="text-sm text-slate-600">
&quot;PDF olarak kaydet&quot; seçeneğiyle raporu indirebilir veya
yazdırabilirsin.
</p>
<Button
onClick={() => window.print()}
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Printer className="size-4" aria-hidden />
Yazdır / PDF kaydet
</Button>
</div>
);
}