Update Next.js configuration and package dependencies
All checks were successful
Deploy / deploy (push) Successful in 9m29s
All checks were successful
Deploy / deploy (push) Successful in 9m29s
- Modified redirects in next.config.ts to change the destination for "/sohbet" to "/listem". - Added new dependencies in package.json: marked, motion, react-markdown, remark-breaks, remark-gfm, shiki, and use-stick-to-bottom. - Updated pnpm-lock.yaml to reflect the new package versions and dependencies. - Removed obsolete stack files for Flutter, Nuxt, Nuxt.js, React Native, and Svelte from the UI/UX Pro Max skill data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,365 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
Loader2,
|
||||
Lock,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { RaporListesi } from "@/components/rapor-listesi";
|
||||
import { SihirbazAdimlar } from "@/components/sihirbaz-adimlar";
|
||||
import type { SihirbazFacetleri } from "@/lib/db";
|
||||
import type { RaporSonuc } from "@/lib/ai/rapor";
|
||||
import type { RaporParams } from "@/lib/rapor-havuzu";
|
||||
import {
|
||||
SIHIRBAZ_STORAGE_KEY,
|
||||
type SihirbazSecimleri,
|
||||
} from "@/lib/sihirbaz";
|
||||
import { listeOlustur, listeRevize } from "./actions";
|
||||
|
||||
const MAX_REVIZYON = 2;
|
||||
|
||||
export type OnizlemeSatir = {
|
||||
id: string;
|
||||
isim: string;
|
||||
universite: string;
|
||||
il: string | null;
|
||||
};
|
||||
|
||||
export type MevcutRapor = {
|
||||
rapor: RaporSonuc;
|
||||
params: RaporParams;
|
||||
revisionCount: number;
|
||||
};
|
||||
|
||||
type Faz = "adimlar" | "onizleme" | "uretiliyor" | "liste";
|
||||
|
||||
const URETIM_MESAJLARI = [
|
||||
"Sıralamana uygun programlar taranıyor…",
|
||||
"Gerçek YÖK Atlas verisiyle dengeli liste kuruluyor…",
|
||||
"Her tercih için risk ve trend analizi yazılıyor…",
|
||||
"Son rötuşlar yapılıyor, neredeyse hazır…",
|
||||
];
|
||||
import type { SihirbazSecimleri } from "@/lib/sihirbaz";
|
||||
|
||||
/**
|
||||
* Sihirbaz modalı artık yalnızca adımları toplar; üretim, önizleme ve liste
|
||||
* sonuç sayfası gövdesinde yaşar (bkz. liste-paneli.tsx). Akış yönetimi
|
||||
* sihirbaz-cagri-karti.tsx'teki SihirbazBolumu'ndadır.
|
||||
*/
|
||||
export function SihirbazModal({
|
||||
acik,
|
||||
onAcikDegisti,
|
||||
sira,
|
||||
tur,
|
||||
facetler,
|
||||
girisliMi,
|
||||
hasPaket,
|
||||
kredi: baslangicKredi,
|
||||
mevcutRapor,
|
||||
onizlemeSatirlari,
|
||||
otomatikUret,
|
||||
baslangicSecimler,
|
||||
sonButonEtiketi,
|
||||
onTamamla,
|
||||
}: {
|
||||
acik: boolean;
|
||||
onAcikDegisti: (acik: boolean) => void;
|
||||
sira: number;
|
||||
tur: string;
|
||||
facetler: SihirbazFacetleri;
|
||||
girisliMi: boolean;
|
||||
hasPaket: boolean;
|
||||
kredi: number;
|
||||
mevcutRapor: MevcutRapor | null;
|
||||
onizlemeSatirlari: OnizlemeSatir[];
|
||||
/** Giriş dönüşü: storage'daki seçimlerle otomatik üretime başla */
|
||||
otomatikUret?: boolean;
|
||||
baslangicSecimler?: SihirbazSecimleri | null;
|
||||
sonButonEtiketi: string;
|
||||
onTamamla: (secimler: SihirbazSecimleri) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const [faz, setFaz] = useState<Faz>(mevcutRapor ? "liste" : "adimlar");
|
||||
const [kredi, setKredi] = useState(baslangicKredi);
|
||||
const [rapor, setRapor] = useState<MevcutRapor | null>(mevcutRapor);
|
||||
const [uretimMesajIdx, setUretimMesajIdx] = useState(0);
|
||||
|
||||
// Adımlarda yapılan son seçimler (önizlemeden geri dönüşte prefill için)
|
||||
const [sonSecimler, setSonSecimler] = useState<SihirbazSecimleri | null>(
|
||||
baslangicSecimler ?? null,
|
||||
);
|
||||
|
||||
// Aynı üretim için sabit requestId (retry çifte harcama yapmaz)
|
||||
const requestIdRef = useRef<string | null>(null);
|
||||
const otomatikBasladi = useRef(false);
|
||||
|
||||
async function uret(secimler: SihirbazSecimleri) {
|
||||
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
||||
setFaz("uretiliyor");
|
||||
setUretimMesajIdx(0);
|
||||
const interval = setInterval(() => {
|
||||
setUretimMesajIdx((i) => Math.min(i + 1, URETIM_MESAJLARI.length - 1));
|
||||
}, 7000);
|
||||
|
||||
try {
|
||||
const sonuc = await listeOlustur({
|
||||
sira,
|
||||
tur,
|
||||
secimler,
|
||||
requestId: requestIdRef.current,
|
||||
});
|
||||
if (sonuc.ok) {
|
||||
requestIdRef.current = null;
|
||||
setRapor({
|
||||
rapor: sonuc.rapor,
|
||||
params: sonuc.params,
|
||||
revisionCount: sonuc.revisionCount,
|
||||
});
|
||||
setKredi(sonuc.kredi);
|
||||
setFaz("liste");
|
||||
return;
|
||||
}
|
||||
// Hata: aynı requestId'yi koru ki tekrar denemede çifte harcama olmasın
|
||||
if (sonuc.code === "KREDI") {
|
||||
toast.error(sonuc.error);
|
||||
setFaz("adimlar");
|
||||
} else if (sonuc.code === "PAKET") {
|
||||
setFaz("onizleme");
|
||||
} else if (sonuc.code === "AUTH") {
|
||||
secimleriKaydetVeGirise(secimler);
|
||||
} else {
|
||||
toast.error(sonuc.error);
|
||||
setFaz("adimlar");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Beklenmeyen bir hata oluştu, tekrar dener misin?");
|
||||
setFaz("adimlar");
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
function secimleriKaydetVeGirise(secimler: SihirbazSecimleri) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
SIHIRBAZ_STORAGE_KEY,
|
||||
JSON.stringify({ sira, tur, secimler }),
|
||||
);
|
||||
} catch {}
|
||||
const geri = `/sonuc?sira=${sira}&tur=${tur}&sihirbaz=1`;
|
||||
router.push(`/giris?callback=${encodeURIComponent(geri)}`);
|
||||
}
|
||||
|
||||
// Giriş dönüşü: storage'daki seçimlerle otomatik üretime başla (bir kez)
|
||||
useEffect(() => {
|
||||
if (
|
||||
otomatikUret &&
|
||||
baslangicSecimler &&
|
||||
hasPaket &&
|
||||
girisliMi &&
|
||||
!otomatikBasladi.current &&
|
||||
!mevcutRapor
|
||||
) {
|
||||
otomatikBasladi.current = true;
|
||||
void uret(baslangicSecimler);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Paketsiz kullanıcı önizlemeye geçerken seçimleri sakla ki ödeme dönüşünde
|
||||
// (aynı sıralamayla) sihirbaz kaldığı yerden devam edebilsin.
|
||||
function secimleriSakla(secimler: SihirbazSecimleri) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
SIHIRBAZ_STORAGE_KEY,
|
||||
JSON.stringify({ sira, tur, secimler }),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function ilerle(secimler: SihirbazSecimleri) {
|
||||
setSonSecimler(secimler);
|
||||
if (!girisliMi) {
|
||||
secimleriKaydetVeGirise(secimler);
|
||||
return;
|
||||
}
|
||||
if (!hasPaket) {
|
||||
secimleriSakla(secimler);
|
||||
setFaz("onizleme");
|
||||
return;
|
||||
}
|
||||
void uret(secimler);
|
||||
}
|
||||
|
||||
// ---- Faz gövdeleri ----
|
||||
|
||||
const adimlarGovde = (
|
||||
<SihirbazAdimlar
|
||||
facetler={facetler}
|
||||
baslangicSecimler={sonSecimler}
|
||||
sonButonEtiketi={
|
||||
hasPaket ? "24'lük listemi oluştur (3 kredi)" : "Listemi gör"
|
||||
}
|
||||
onTamamla={ilerle}
|
||||
/>
|
||||
);
|
||||
|
||||
const onizlemeGovde = (
|
||||
<div>
|
||||
<h3 className="font-heading text-xl font-bold">
|
||||
24 tercihlik listen hazır olmak üzere
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-slate-600">
|
||||
Aşağıda ilk birkaç satırın ön izlemesi var. Tam listeyi, gerekçeleri, risk
|
||||
analizini ve PDF çıktıyı görmek için paketi aktive et.
|
||||
</p>
|
||||
<div className="mt-6 overflow-hidden rounded-2xl border border-slate-200">
|
||||
{onizlemeSatirlari.length > 0 ? (
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{onizlemeSatirlari.map((p, i) => (
|
||||
<li key={p.id} className="flex items-center gap-4 px-5 py-3">
|
||||
<span className="w-6 text-right font-heading font-bold text-slate-400">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium">{p.isim}</p>
|
||||
<p className="truncate text-sm text-slate-500">
|
||||
{p.universite} · {p.il ?? "—"}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-700">
|
||||
Dengeli
|
||||
</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<div className="relative">
|
||||
<ul className="divide-y divide-slate-100 blur-sm select-none" aria-hidden>
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<li key={i} className="flex items-center gap-4 px-5 py-3">
|
||||
<span className="w-6 text-right font-heading font-bold text-slate-300">
|
||||
{onizlemeSatirlari.length + i + 1}
|
||||
</span>
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="h-3.5 w-2/3 rounded bg-slate-200" />
|
||||
<div className="h-3 w-1/2 rounded bg-slate-100" />
|
||||
</div>
|
||||
<div className="h-5 w-16 rounded-full bg-slate-100" />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-gradient-to-b from-white/30 via-white/80 to-white">
|
||||
<Lock className="size-8 text-slate-400" aria-hidden />
|
||||
<p className="max-w-sm text-center text-sm font-medium text-slate-700">
|
||||
+19 tercih, her satır için gerekçe ve risk analizi, PDF çıktı ve AI
|
||||
soru hakkı seni bekliyor.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href="/paket">299 TL — Paketi aktive et</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setFaz("adimlar")}
|
||||
className="mt-4 cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="size-4" aria-hidden />
|
||||
Seçimlere dön
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const uretiliyorGovde = (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-16 text-center">
|
||||
<Loader2 className="size-10 animate-spin text-primary" aria-hidden />
|
||||
<p className="font-heading text-lg font-bold">Listen hazırlanıyor</p>
|
||||
<p className="max-w-sm text-sm text-slate-500">
|
||||
{URETIM_MESAJLARI[uretimMesajIdx]}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">
|
||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const listeGovde = rapor ? (
|
||||
<div>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="font-heading text-xl font-bold">24 Tercihlik Listen</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{sira.toLocaleString("tr-TR")}. sıra · {kredi} kredin kaldı
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline" className="cursor-pointer">
|
||||
<Link href="/rapor/yazdir" target="_blank">
|
||||
<FileText className="size-4" aria-hidden />
|
||||
PDF indir
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<RaporListesi rapor={rapor.rapor} adaySira={rapor.params.sira} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<RevizyonKutusu
|
||||
kalanHak={MAX_REVIZYON - rapor.revisionCount}
|
||||
onRevize={async (feedback) => {
|
||||
const sonuc = await listeRevize({
|
||||
feedback,
|
||||
requestId: crypto.randomUUID(),
|
||||
});
|
||||
if (sonuc.ok) {
|
||||
setRapor({
|
||||
rapor: sonuc.rapor,
|
||||
params: sonuc.params,
|
||||
revisionCount: sonuc.revisionCount,
|
||||
});
|
||||
setKredi(sonuc.kredi);
|
||||
toast.success("Listen yeniden kuruldu.");
|
||||
} else {
|
||||
toast.error(sonuc.error);
|
||||
}
|
||||
return sonuc.ok;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-white px-5 py-4">
|
||||
<div>
|
||||
<p className="font-heading text-sm font-bold">
|
||||
Listen hakkında soruların mı var?
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
AI danışman listeni ve seçimlerini bilir; her mesaj 1 kredi.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href="/listem">Danışmana sor</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Dialog open={acik} onOpenChange={onAcikDegisti}>
|
||||
<DialogContent className="max-h-[92dvh] gap-0 overflow-y-auto p-6 sm:max-w-3xl">
|
||||
@@ -367,80 +37,13 @@ export function SihirbazModal({
|
||||
<DialogDescription className="sr-only">
|
||||
Sıralamana uygun 24 tercihlik listeni oluştur.
|
||||
</DialogDescription>
|
||||
{faz === "adimlar"
|
||||
? adimlarGovde
|
||||
: faz === "onizleme"
|
||||
? onizlemeGovde
|
||||
: faz === "uretiliyor"
|
||||
? uretiliyorGovde
|
||||
: listeGovde}
|
||||
<SihirbazAdimlar
|
||||
facetler={facetler}
|
||||
baslangicSecimler={baslangicSecimler}
|
||||
sonButonEtiketi={sonButonEtiketi}
|
||||
onTamamla={onTamamla}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function RevizyonKutusu({
|
||||
kalanHak,
|
||||
onRevize,
|
||||
}: {
|
||||
kalanHak: number;
|
||||
onRevize: (feedback: string) => Promise<boolean>;
|
||||
}) {
|
||||
const [feedback, setFeedback] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
if (kalanHak <= 0) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<p className="text-sm text-slate-500">
|
||||
Revizyon hakların doldu. Aşağıdan listen hakkında soru sorabilirsin.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h3 className="font-heading text-base font-bold">
|
||||
Listede değişiklik mi istiyorsun?
|
||||
</h3>
|
||||
<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={async () => {
|
||||
setPending(true);
|
||||
const ok = await onRevize(feedback.trim());
|
||||
setPending(false);
|
||||
if (ok) setFeedback("");
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user