Sihirbaz akışı ana sayfaya taşındı: sıralama girilince modal orada açılır
Some checks failed
Deploy / deploy (push) Failing after 1m30s
Some checks failed
Deploy / deploy (push) Failing after 1m30s
- Ortak SihirbazAdimlar bileşeni (3 adımlık seçim UI'ı) çıkarıldı; hem hero modalı hem /sonuc modalı kullanıyor - /api/facetler: sıraya göre kategori/il/üniversite tipi facet'leri - HeroForm: "Listemi oluştur" artık modalı açar; seçimler tamamlanınca localStorage'a (SIHIRBAZ_STORAGE_KEY) yazılıp /sonuc'a yönlendirilir - /sonuc: modal yalnızca giriş dönüşünde (?sihirbaz=1) otomatik açılır; kayıtlı seçimler prefill olarak kullanılır Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,10 @@ import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
FileText,
|
||||
Loader2,
|
||||
Lock,
|
||||
RefreshCcw,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -22,16 +20,13 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { RaporListesi } from "@/components/rapor-listesi";
|
||||
import { SoruKutusu } from "./soru-kutusu";
|
||||
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 {
|
||||
ONCELIKLER,
|
||||
SIHIRBAZ_STORAGE_KEY,
|
||||
UNIVERSITE_TIPI_ETIKET,
|
||||
type SihirbazSecimleri,
|
||||
type UniversiteTipi,
|
||||
} from "@/lib/sihirbaz";
|
||||
import { listeOlustur, listeRevize } from "./actions";
|
||||
|
||||
@@ -52,31 +47,6 @@ export type MevcutRapor = {
|
||||
|
||||
type Faz = "adimlar" | "onizleme" | "uretiliyor" | "liste";
|
||||
|
||||
function Cip({
|
||||
secili,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
secili: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={secili}
|
||||
className={
|
||||
secili
|
||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3.5 py-2 text-sm font-medium text-white transition-colors duration-200"
|
||||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3.5 py-2 text-sm text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const URETIM_MESAJLARI = [
|
||||
"Sıralamana uygun programlar taranıyor…",
|
||||
"Gerçek YÖK Atlas verisiyle dengeli liste kuruluyor…",
|
||||
@@ -115,48 +85,19 @@ export function SihirbazModal({
|
||||
const router = useRouter();
|
||||
|
||||
const [faz, setFaz] = useState<Faz>(mevcutRapor ? "liste" : "adimlar");
|
||||
const [adim, setAdim] = useState(0);
|
||||
const [kredi, setKredi] = useState(baslangicKredi);
|
||||
const [rapor, setRapor] = useState<MevcutRapor | null>(mevcutRapor);
|
||||
const [uretimMesajIdx, setUretimMesajIdx] = useState(0);
|
||||
|
||||
// Seçimler
|
||||
const [kategoriler, setKategoriler] = useState<string[]>(
|
||||
baslangicSecimler?.kategoriler ?? [],
|
||||
);
|
||||
const [iller, setIller] = useState<string[]>(baslangicSecimler?.iller ?? []);
|
||||
const [universiteTipi, setUniversiteTipi] = useState<UniversiteTipi>(
|
||||
baslangicSecimler?.universiteTipi ?? "farketmez",
|
||||
);
|
||||
const [oncelikler, setOncelikler] = useState<string[]>(
|
||||
baslangicSecimler?.oncelikler ?? [],
|
||||
// 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);
|
||||
|
||||
function secimleriTopla(): SihirbazSecimleri {
|
||||
return { kategoriler, iller, universiteTipi, oncelikler };
|
||||
}
|
||||
|
||||
function listeDegistir(
|
||||
liste: string[],
|
||||
setListe: (v: string[]) => void,
|
||||
deger: string,
|
||||
maks?: number,
|
||||
) {
|
||||
if (liste.includes(deger)) {
|
||||
setListe(liste.filter((x) => x !== deger));
|
||||
} else {
|
||||
if (maks && liste.length >= maks) {
|
||||
toast.info(`En fazla ${maks} seçebilirsin.`);
|
||||
return;
|
||||
}
|
||||
setListe([...liste, deger]);
|
||||
}
|
||||
}
|
||||
|
||||
async function uret(secimler: SihirbazSecimleri) {
|
||||
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
||||
setFaz("uretiliyor");
|
||||
@@ -241,8 +182,8 @@ export function SihirbazModal({
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function ilerle() {
|
||||
const secimler = secimleriTopla();
|
||||
function ilerle(secimler: SihirbazSecimleri) {
|
||||
setSonSecimler(secimler);
|
||||
if (!girisliMi) {
|
||||
secimleriKaydetVeGirise(secimler);
|
||||
return;
|
||||
@@ -255,162 +196,17 @@ export function SihirbazModal({
|
||||
void uret(secimler);
|
||||
}
|
||||
|
||||
const adimGecerli = adim === 0 ? kategoriler.length > 0 : true;
|
||||
|
||||
// ---- Faz gövdeleri ----
|
||||
|
||||
const adimlarGovde = (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={
|
||||
i === adim
|
||||
? "h-1.5 w-6 rounded-full bg-primary transition-all"
|
||||
: "h-1.5 w-1.5 rounded-full bg-slate-200 transition-all"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<span className="ml-2 text-xs font-medium text-slate-400">
|
||||
Adım {adim + 1}/3
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{adim === 0 ? (
|
||||
<div>
|
||||
<h3 className="font-heading text-lg font-bold">
|
||||
Hangi alanlara ilgi duyuyorsun?
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Sıralamanla ulaşabileceğin alanlar. En az bir tane seç.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{facetler.kategoriler.map((k) => (
|
||||
<Cip
|
||||
key={k.ad}
|
||||
secili={kategoriler.includes(k.ad)}
|
||||
onClick={() =>
|
||||
listeDegistir(kategoriler, setKategoriler, k.ad)
|
||||
}
|
||||
>
|
||||
{k.ad}
|
||||
<span className="ml-1.5 text-xs opacity-70">{k.adet}</span>
|
||||
</Cip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : adim === 1 ? (
|
||||
<div>
|
||||
<h3 className="font-heading text-lg font-bold">
|
||||
Nerede okumak istersin?
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
İstersen boş bırak (fark etmez). En fazla 5 il.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{facetler.iller.map((i) => (
|
||||
<Cip
|
||||
key={i.il}
|
||||
secili={iller.includes(i.il)}
|
||||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||||
>
|
||||
{i.il}
|
||||
<span className="ml-1.5 text-xs opacity-70">{i.adet}</span>
|
||||
</Cip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h3 className="font-heading text-lg font-bold">
|
||||
Üniversite tipi & öncelikler
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Bunlar listenin dengesini belirler.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
||||
Üniversite tipi
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{facetler.uniturler.map((u) => (
|
||||
<Cip
|
||||
key={u.grup}
|
||||
secili={universiteTipi === u.grup}
|
||||
onClick={() =>
|
||||
setUniversiteTipi(
|
||||
universiteTipi === u.grup ? "farketmez" : u.grup,
|
||||
)
|
||||
}
|
||||
>
|
||||
{UNIVERSITE_TIPI_ETIKET[u.grup]}
|
||||
<span className="ml-1.5 text-xs opacity-70">{u.adet}</span>
|
||||
</Cip>
|
||||
))}
|
||||
<Cip
|
||||
secili={universiteTipi === "farketmez"}
|
||||
onClick={() => setUniversiteTipi("farketmez")}
|
||||
>
|
||||
Fark etmez
|
||||
</Cip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
||||
Senin için önemli olan
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ONCELIKLER.map((o) => (
|
||||
<Cip
|
||||
key={o}
|
||||
secili={oncelikler.includes(o)}
|
||||
onClick={() => listeDegistir(oncelikler, setOncelikler, o)}
|
||||
>
|
||||
{o}
|
||||
</Cip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between border-t border-slate-100 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setAdim((a) => a - 1)}
|
||||
disabled={adim === 0}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="size-4" aria-hidden />
|
||||
Geri
|
||||
</Button>
|
||||
{adim < 2 ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setAdim((a) => a + 1)}
|
||||
disabled={!adimGecerli}
|
||||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
Devam
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={ilerle}
|
||||
disabled={kategoriler.length === 0}
|
||||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden />
|
||||
{hasPaket ? "24'lük listemi oluştur (3 kredi)" : "Listemi gör"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SihirbazAdimlar
|
||||
facetler={facetler}
|
||||
baslangicSecimler={sonSecimler}
|
||||
sonButonEtiketi={
|
||||
hasPaket ? "24'lük listemi oluştur (3 kredi)" : "Listemi gör"
|
||||
}
|
||||
onTamamla={ilerle}
|
||||
/>
|
||||
);
|
||||
|
||||
const onizlemeGovde = (
|
||||
@@ -517,7 +313,7 @@ export function SihirbazModal({
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<RaporListesi rapor={rapor.rapor} />
|
||||
<RaporListesi rapor={rapor.rapor} adaySira={rapor.params.sira} />
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
@@ -544,8 +340,22 @@ export function SihirbazModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<SoruKutusu kredi={kredi} onKrediDegisti={setKredi} />
|
||||
<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;
|
||||
|
||||
Reference in New Issue
Block a user