Update UI components and database schema
All checks were successful
Deploy / deploy (push) Successful in 2m38s
All checks were successful
Deploy / deploy (push) Successful in 2m38s
- Updated the `page.tsx` component to enhance user experience with new UI elements and improved layout. - Modified the `funnel-banner.tsx` to include dynamic text and improved styling for better engagement. - Adjusted the `liste-paneli.tsx` to provide clearer information on user credits and package details. - Enhanced the `rapor-listesi.tsx` to display additional data points for better insights. - Updated the database schema in `app.db` to reflect recent changes in the application logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,68 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Sparkles } from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChartNoAxesCombined,
|
||||
Database,
|
||||
ListChecks,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TypingAnimation } from "@/components/ui/typing-animation";
|
||||
|
||||
/** Banner CTA'sının sihirbaz modalını açmak için yayınladığı olay. */
|
||||
export const SIHIRBAZ_AC_EVENT = "kolaytercih:sihirbaz-ac";
|
||||
|
||||
const CUMLELER = [
|
||||
"Seçmekte zorlanıyor musunuz?",
|
||||
"Hangi bölüm size uygun, emin değil misiniz?",
|
||||
"Yapay zekâ 24 tercihlik listenizi kursun.",
|
||||
"Her satırda gerekçe ve yerleşme riski analizi.",
|
||||
const FUNNEL_METINLERI = [
|
||||
"Sıralamana en uygun listeyi birlikte kuralım.",
|
||||
"Geleceğin 24 satırlık bir plana sığar.",
|
||||
"Gerçek YÖK Atlas verisiyle güvenle seç.",
|
||||
"Her tercihinin bir nedeni olsun.",
|
||||
"Riskleri biz hesaplayalım, kararı sen ver.",
|
||||
"Sana uygun bölümler bir adım uzağında.",
|
||||
"Dengeli bir liste, rahat bir tercih dönemi.",
|
||||
"Tercihlerini şansa bırakma.",
|
||||
"Doğru strateji, doğru yerleşme.",
|
||||
"Hayalindeki bölüm için doğru plan.",
|
||||
"Verilerle konuşan bir tercih listesi.",
|
||||
"Üç adımda sana özel 24 tercih.",
|
||||
"Tercih dönemini stressiz geçir.",
|
||||
"Geçen yılın verisi, senin avantajın.",
|
||||
"Güvenli ve iddialı tercihleri dengele.",
|
||||
"Sıralaman ne söylüyor? Birlikte bakalım.",
|
||||
"Her satırı senin için hesaplıyoruz.",
|
||||
"Akıllı tercih, içi rahat bir bekleyiş.",
|
||||
"Yerleşme ihtimalini görerek seç.",
|
||||
"Sana özel liste birkaç dakikada hazır.",
|
||||
"İlgi alanların ve şehrin, tek listede.",
|
||||
"Boş kalan tercih, kaçan fırsattır.",
|
||||
"Uzman işi bir liste, senin onayınla.",
|
||||
"Kararını veriye dayandır.",
|
||||
"Doğru tercih, doğru bilgiyle başlar.",
|
||||
"Listeni bugün kur, için rahat olsun.",
|
||||
"Yerleşme riskin renklerle önünde.",
|
||||
"En önemli 24 kararını birlikte verelim.",
|
||||
"Senin sıralaman, sana özel plan.",
|
||||
"İyi bir liste, iyi bir başlangıçtır.",
|
||||
];
|
||||
|
||||
/** Fisher-Yates ile kopya üzerinde karıştırır; her ziyarette farklı sıra. */
|
||||
function karistir<T>(dizi: readonly T[]): T[] {
|
||||
const kopya = [...dizi];
|
||||
for (let i = kopya.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[kopya[i], kopya[j]] = [kopya[j], kopya[i]];
|
||||
}
|
||||
return kopya;
|
||||
}
|
||||
|
||||
const KANITLAR = [
|
||||
{ ikon: ListChecks, etiket: "24 tercih" },
|
||||
{ ikon: ChartNoAxesCombined, etiket: "Risk analizi" },
|
||||
{ ikon: Database, etiket: "YÖK Atlas verisi" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Puan türü badge'lerinin hemen altında yaşayan satın alım funnel banner'ı.
|
||||
* Sürekli değişen (yazılıp silinen) cümlelerle adayı sihirbaza; rapor zaten
|
||||
* varsa kilitli listenin durduğu /listem paywall'ına iter.
|
||||
* Tasarım: flat indigo — gradient/gölge yok, CTA funnel turuncusu.
|
||||
* Rastgele sırayla yazılıp silinen samimi ve profesyonel funnel
|
||||
* metinleriyle adayı sihirbaza; rapor zaten varsa kilitli listenin
|
||||
* durduğu /listem paywall'ına iter.
|
||||
* Tasarım: güven veren lacivert yüzey, veri kanıtları ve turuncu CTA.
|
||||
*/
|
||||
export function FunnelBanner({ hedefListem }: { hedefListem: boolean }) {
|
||||
// İlk render'da bir kez karıştırılır; SSR boş metinle başladığı için
|
||||
// hydration uyuşmazlığı yaratmaz.
|
||||
const [metinler] = useState(() => karistir(FUNNEL_METINLERI));
|
||||
|
||||
const ctaSinif =
|
||||
"h-10 shrink-0 cursor-pointer bg-orange-500 px-5 font-semibold text-white transition-colors duration-200 hover:bg-orange-600 focus-visible:ring-white/70";
|
||||
"h-12 w-full shrink-0 cursor-pointer rounded-xl bg-orange-500 px-5 font-semibold text-white shadow-[0_8px_24px_rgba(249,115,22,0.28)] transition-[background-color,transform,box-shadow] duration-200 hover:bg-orange-400 hover:shadow-[0_10px_28px_rgba(249,115,22,0.36)] active:scale-[0.98] focus-visible:ring-white/70 sm:w-auto";
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Tercih listeni kur"
|
||||
className="mt-6 flex flex-col gap-4 rounded-2xl bg-indigo-600 p-5 sm:flex-row sm:items-center sm:gap-6"
|
||||
className="relative mt-6 overflow-hidden rounded-3xl border border-blue-400/20 bg-[#0b1f3a] px-5 py-6 text-white shadow-[0_20px_50px_-28px_rgba(15,71,150,0.65)] sm:px-7 sm:py-7"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-4">
|
||||
<div
|
||||
className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-indigo-500"
|
||||
aria-hidden
|
||||
>
|
||||
<Sparkles className="size-5 text-white" />
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -right-16 -top-24 size-64 rounded-full bg-blue-500/25 blur-3xl"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-20 left-1/3 size-48 rounded-full bg-cyan-400/10 blur-3xl"
|
||||
/>
|
||||
|
||||
<div className="relative grid items-center gap-6 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-8">
|
||||
<div className="min-w-0">
|
||||
<TypingAnimation
|
||||
as="h2"
|
||||
words={metinler}
|
||||
loop
|
||||
typeSpeed={38}
|
||||
deleteSpeed={18}
|
||||
pauseDelay={2600}
|
||||
className="min-h-[2.5em] max-w-2xl font-heading text-2xl font-bold leading-tight tracking-[-0.02em] sm:min-h-[1.25em] sm:text-3xl"
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-x-4 gap-y-2">
|
||||
{KANITLAR.map(({ ikon: Ikon, etiket }) => (
|
||||
<span
|
||||
key={etiket}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-200"
|
||||
>
|
||||
<Ikon className="size-3.5 text-cyan-300" aria-hidden />
|
||||
{etiket}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<TypingAnimation
|
||||
as="p"
|
||||
words={CUMLELER}
|
||||
loop
|
||||
typeSpeed={45}
|
||||
deleteSpeed={20}
|
||||
pauseDelay={2400}
|
||||
className="min-h-14 min-w-0 flex-1 font-heading text-lg font-bold leading-snug text-white sm:min-h-8 sm:text-2xl"
|
||||
/>
|
||||
</div>
|
||||
{hedefListem ? (
|
||||
<Button asChild className={ctaSinif}>
|
||||
<Link href="/listem">
|
||||
Listeni aç
|
||||
|
||||
{hedefListem ? (
|
||||
<Button asChild size="lg" className={ctaSinif}>
|
||||
<Link href="/listem">
|
||||
Listeni aç
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => window.dispatchEvent(new Event(SIHIRBAZ_AC_EVENT))}
|
||||
className={ctaSinif}
|
||||
>
|
||||
Listemi oluştur
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => window.dispatchEvent(new Event(SIHIRBAZ_AC_EVENT))}
|
||||
className={ctaSinif}
|
||||
>
|
||||
Listemi oluştur
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,8 +47,7 @@ function pinleriTuret(
|
||||
rapor.tercihler.forEach((t, i) => {
|
||||
const p = rapor.programlar[t.programId];
|
||||
if (!p) return;
|
||||
const risk =
|
||||
riskHesapla(p.efektifSira, adaySira) ?? dilimdenRisk(t.dilim);
|
||||
const risk = riskHesapla(p.efektifSira, adaySira) ?? dilimdenRisk(t.dilim);
|
||||
const kilitli = i >= kilitliBaslangic;
|
||||
|
||||
if (kilitli) {
|
||||
@@ -192,7 +191,7 @@ export function ListePaneli({
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{sira.toLocaleString("tr-TR")}. sıra ·{" "}
|
||||
{kilitli
|
||||
? `ilk ${ACIK_SATIR} tercih açık`
|
||||
? `tüm taban verileri açık · ${ACIK_SATIR} AI analiz örneği`
|
||||
: `${kredi} kredin kaldı`}
|
||||
</p>
|
||||
</div>
|
||||
@@ -236,29 +235,35 @@ export function ListePaneli({
|
||||
}}
|
||||
kilitliBaslangic={kilitli ? ACIK_SATIR : undefined}
|
||||
kilitOverlay={
|
||||
<>
|
||||
<Lock className="size-8 text-slate-500" aria-hidden />
|
||||
<p className="max-w-sm text-center font-heading text-lg font-bold text-slate-900">
|
||||
24 tercihin {24 - ACIK_SATIR}'i kilidin ardında
|
||||
</p>
|
||||
<p className="max-w-sm text-center text-sm text-slate-600">
|
||||
Her satır için gerekçe, risk notu ve 4 yıllık trend + PDF çıktı
|
||||
+ 2 revizyon hakkı + 60 kredi seni bekliyor.
|
||||
</p>
|
||||
<div className="flex flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex gap-3">
|
||||
<Lock
|
||||
className="mt-1 size-5 shrink-0 text-orange-700"
|
||||
aria-hidden
|
||||
/>
|
||||
<div>
|
||||
<p className="font-heading font-bold text-slate-900">
|
||||
Tüm program ve taban verileri ücretsiz
|
||||
</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, PDF, 2 revizyon ve 60 AI danışman sorusunu açar.
|
||||
</p>
|
||||
{kredi > 0 ? (
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Kalan {kredi} deneme kredin hesabında duruyor.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto"
|
||||
>
|
||||
<Link href="/paket">299 TL — Tüm listeyi aç</Link>
|
||||
<Link href="/paket">299 TL — Analizleri aç</Link>
|
||||
</Button>
|
||||
{kredi > 0 ? (
|
||||
<p className="text-xs text-slate-500">
|
||||
Kalan {kredi} kredin AI danışmana soru sormak için cebinde
|
||||
duruyor.
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -280,6 +285,7 @@ export function ListePaneli({
|
||||
|
||||
function UretimGovdesi() {
|
||||
const [mesajIdx, setMesajIdx] = useState(0);
|
||||
const govdeRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -288,8 +294,19 @@ function UretimGovdesi() {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Modal geçiş anında unmount edildiğinden Radix odağı tetikleyiciye geri
|
||||
// veremiyor; odağı panele alıp klavye ve ekran okuyucu akışını sürdürüyoruz.
|
||||
useEffect(() => {
|
||||
govdeRef.current?.focus({ preventScroll: true });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="mt-16 flex flex-col items-center justify-center gap-4 rounded-2xl border border-slate-200 bg-white py-16 text-center">
|
||||
<section
|
||||
ref={govdeRef}
|
||||
tabIndex={-1}
|
||||
aria-live="polite"
|
||||
className="mt-16 flex flex-col items-center justify-center gap-4 rounded-2xl border border-slate-200 bg-white py-16 text-center outline-none"
|
||||
>
|
||||
<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">
|
||||
|
||||
@@ -124,31 +124,14 @@ export default async function SonucPage({
|
||||
<span className="font-semibold text-red-600">kırmızı riskli</span>.
|
||||
</p>
|
||||
|
||||
{/* Puan türü seçimi */}
|
||||
<nav className="mt-6 flex flex-wrap gap-2" aria-label="Puan türü">
|
||||
{(Object.keys(PUAN_TURLERI) as PuanTuruKey[]).map((key) => (
|
||||
<Button
|
||||
key={key}
|
||||
asChild
|
||||
size="sm"
|
||||
variant={key === turKey ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Link href={`/sonuc?sira=${sira}&tur=${key}`}>
|
||||
{TUR_LABELS[key]}
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Satın alım funnel'ı: değişen cümlelerle adayı sihirbaza (rapor
|
||||
varsa /listem paywall'ına) iten banner — paketli kullanıcıya
|
||||
satacak bir şey kalmadığından gösterilmez */}
|
||||
{!hasPaket ? <FunnelBanner hedefListem={Boolean(mevcutRapor)} /> : null}
|
||||
|
||||
{/* AI'dan bağımsız ham tablo + manuel liste haritası — sayfanın ana
|
||||
gövdesi; + ile manuel 24'lük liste kurulur */}
|
||||
<ProgramTablosu sonuclar={hamSonuclar} adaySira={sira} />
|
||||
gövdesi; puan türü seçici de listenin başında yaşar */}
|
||||
<ProgramTablosu sonuclar={hamSonuclar} adaySira={sira} tur={turKey} />
|
||||
|
||||
{/* Sihirbaz akışı (CTA + modal + AI listesi) — blurlu AI paneli
|
||||
sayfanın en altında yaşar */}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Sparkles, X } from "lucide-react";
|
||||
import { animate } from "motion";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { SihirbazFacetleri } from "@/lib/db";
|
||||
@@ -19,11 +16,7 @@ import {
|
||||
} from "@/lib/sihirbaz";
|
||||
import { SihirbazModal } from "./sihirbaz-modal";
|
||||
import { SIHIRBAZ_AC_EVENT } from "./funnel-banner";
|
||||
import {
|
||||
ListePaneli,
|
||||
type MevcutRapor,
|
||||
type PanelDurum,
|
||||
} from "./liste-paneli";
|
||||
import { ListePaneli, type MevcutRapor, type PanelDurum } from "./liste-paneli";
|
||||
import { listeOlustur, listeRevize } from "./actions";
|
||||
|
||||
const KAPATMA_KEY = "kolaytercih.sihirbaz-karti-kapali";
|
||||
@@ -75,6 +68,7 @@ export function SihirbazBolumu({
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const monteEdildi = useMonteEdildi();
|
||||
const hareketiAzalt = useReducedMotion();
|
||||
|
||||
const [elleAcik, setElleAcik] = useState(false);
|
||||
const [elleKapandi, setElleKapandi] = useState(false);
|
||||
@@ -94,6 +88,7 @@ export function SihirbazBolumu({
|
||||
// Aynı üretim için sabit requestId (retry çifte harcama yapmaz)
|
||||
const requestIdRef = useRef<string | null>(null);
|
||||
const otomatikBasladi = useRef(false);
|
||||
const bolumRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Mount sonrası storage'dan bekleyen seçimleri + kalıcı kart kapatmayı oku
|
||||
useEffect(() => {
|
||||
@@ -131,8 +126,61 @@ export function SihirbazBolumu({
|
||||
return () => window.removeEventListener(SIHIRBAZ_AC_EVENT, ac);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Bölüm ekran dışındaysa (kullanıcı sihirbazı üstteki banner'dan ya da
|
||||
* sabit alt karttan açmış olabilir) modal hâlâ perdeyi kaplarken kaydırırız;
|
||||
* böylece Motion ile giren hedef panel görünür alanda kalır.
|
||||
*/
|
||||
function bolumuGorunureAl() {
|
||||
const el = bolumRef.current;
|
||||
if (!el) return;
|
||||
const kutu = el.getBoundingClientRect();
|
||||
if (kutu.top >= 0 && kutu.bottom <= window.innerHeight) return;
|
||||
el.scrollIntoView({ block: "center", behavior: "instant" });
|
||||
}
|
||||
|
||||
async function modalCikisiniOynat() {
|
||||
if (hareketiAzalt) return;
|
||||
|
||||
const modal = document.querySelector<HTMLElement>("[data-sihirbaz-modal]");
|
||||
if (!modal) return;
|
||||
|
||||
const mevcutTransform = getComputedStyle(modal).transform;
|
||||
const hedefTransform =
|
||||
mevcutTransform === "none"
|
||||
? "translate3d(0, 48px, 0)"
|
||||
: new DOMMatrixReadOnly(mevcutTransform).translate(0, 48).toString();
|
||||
const perde = document.querySelector<HTMLElement>(
|
||||
'[data-slot="dialog-overlay"]',
|
||||
);
|
||||
|
||||
const modalAnimasyonu = animate(
|
||||
modal,
|
||||
{
|
||||
opacity: 0,
|
||||
transform: hedefTransform,
|
||||
},
|
||||
{
|
||||
duration: 0.24,
|
||||
ease: [0.4, 0, 0.7, 0.2],
|
||||
},
|
||||
);
|
||||
|
||||
if (perde) {
|
||||
await Promise.all([
|
||||
modalAnimasyonu,
|
||||
animate(perde, { opacity: 0 }, { duration: 0.2, ease: "easeOut" }),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
await modalAnimasyonu;
|
||||
}
|
||||
|
||||
async function uret(secimler: SihirbazSecimleri) {
|
||||
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
||||
bolumuGorunureAl();
|
||||
await modalCikisiniOynat();
|
||||
setElleAcik(false);
|
||||
setDurum("uretiliyor");
|
||||
|
||||
@@ -167,16 +215,20 @@ export function SihirbazBolumu({
|
||||
secimleriKaydetVeGirise(secimler);
|
||||
} else {
|
||||
toast.error(sonuc.error);
|
||||
setDurum("bos");
|
||||
setElleAcik(true);
|
||||
modalaDon();
|
||||
}
|
||||
} catch {
|
||||
toast.error("Beklenmeyen bir hata oluştu, tekrar dener misin?");
|
||||
setDurum("bos");
|
||||
setElleAcik(true);
|
||||
modalaDon();
|
||||
}
|
||||
}
|
||||
|
||||
/** Hata sonrası panelden modala dönüş. */
|
||||
function modalaDon() {
|
||||
setDurum("bos");
|
||||
setElleAcik(true);
|
||||
}
|
||||
|
||||
function secimleriKaydetVeGirise(secimler: SihirbazSecimleri) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
@@ -188,13 +240,14 @@ export function SihirbazBolumu({
|
||||
router.push(`/giris?callback=${encodeURIComponent(geri)}`);
|
||||
}
|
||||
|
||||
function ilerle(secimler: SihirbazSecimleri) {
|
||||
async function ilerle(secimler: SihirbazSecimleri) {
|
||||
setSonSecimler(secimler);
|
||||
if (!girisliMi) {
|
||||
await modalCikisiniOynat();
|
||||
secimleriKaydetVeGirise(secimler);
|
||||
return;
|
||||
}
|
||||
void uret(secimler);
|
||||
await uret(secimler);
|
||||
}
|
||||
|
||||
// Giriş dönüşü: storage'daki eşleşen seçimlerle otomatik üretime başla
|
||||
@@ -227,76 +280,119 @@ export function SihirbazBolumu({
|
||||
}
|
||||
|
||||
const panelde = Boolean(rapor) || durum !== "bos";
|
||||
const panelAnahtari =
|
||||
durum !== "bos" ? durum : rapor ? "rapor-hazir" : "sihirbaz-cta";
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* AI çıktısı (rapor listesi/haritası) bu sayfada render edilmez;
|
||||
panel yalnızca üretim ve kredi-yetersiz gibi geçici funnel
|
||||
durumlarını gösterir, listenin kendisi /listem'de yaşar. */}
|
||||
{durum !== "bos" ? (
|
||||
<ListePaneli
|
||||
durum={durum}
|
||||
rapor={null}
|
||||
hasPaket={paketli}
|
||||
kredi={kredi}
|
||||
sira={sira}
|
||||
acilis={acilis}
|
||||
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);
|
||||
<div ref={bolumRef} className="relative">
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<motion.div
|
||||
key={panelAnahtari}
|
||||
initial={
|
||||
hareketiAzalt
|
||||
? { opacity: 0 }
|
||||
: {
|
||||
opacity: 0,
|
||||
transform: "translate3d(0, 8px, 0)",
|
||||
}
|
||||
}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
transform: "translate3d(0, 0, 0)",
|
||||
transition: hareketiAzalt
|
||||
? { duration: 0.12, ease: "easeOut" }
|
||||
: {
|
||||
duration: 0.28,
|
||||
ease: [0.23, 1, 0.32, 1],
|
||||
},
|
||||
}}
|
||||
exit={
|
||||
hareketiAzalt
|
||||
? {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.08, ease: "easeOut" },
|
||||
}
|
||||
: {
|
||||
opacity: 0,
|
||||
transform: "translate3d(0, -4px, 0)",
|
||||
transition: {
|
||||
duration: 0.14,
|
||||
ease: [0.4, 0, 0.7, 0.2],
|
||||
},
|
||||
}
|
||||
}
|
||||
return sonuc.ok;
|
||||
}}
|
||||
/>
|
||||
) : rapor ? (
|
||||
<section className="mt-16 rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
|
||||
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
|
||||
24 tercihlik listen hazır
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
||||
Yapay zekânın kurduğu risk analizli listen Listem sayfasında seni
|
||||
bekliyor.
|
||||
</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="/listem">Listeni gör</Link>
|
||||
</Button>
|
||||
</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">
|
||||
Bu listeyi 24 tercihlik plana çevirelim mi?
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-white/90">
|
||||
Birkaç adımda ilgi alanların ve şehir tercihine göre gerekçeli,
|
||||
dengeli bir tercih listesi kuralım — her satır için yerleşme riski
|
||||
analiziyle.
|
||||
</p>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => acikDegisti(true)}
|
||||
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden />
|
||||
Listemi oluştur
|
||||
</Button>
|
||||
</section>
|
||||
)}
|
||||
{durum !== "bos" ? (
|
||||
<ListePaneli
|
||||
durum={durum}
|
||||
rapor={null}
|
||||
hasPaket={paketli}
|
||||
kredi={kredi}
|
||||
sira={sira}
|
||||
acilis={acilis}
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
) : rapor ? (
|
||||
<section className="mt-16 rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
|
||||
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
|
||||
24 tercihlik listen hazır
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
||||
Yapay zekânın kurduğu risk analizli listen Listem sayfasında
|
||||
seni bekliyor.
|
||||
</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="/listem">Listeni gör</Link>
|
||||
</Button>
|
||||
</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">
|
||||
Bu listeyi 24 tercihlik plana çevirelim mi?
|
||||
</h2>
|
||||
<p className="mx-auto mt-3 max-w-xl text-white/90">
|
||||
Birkaç adımda ilgi alanların ve şehir tercihine göre
|
||||
gerekçeli, dengeli bir tercih listesi kuralım — her satır için
|
||||
yerleşme riski analiziyle.
|
||||
</p>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => acikDegisti(true)}
|
||||
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden />
|
||||
Listemi oluştur
|
||||
</Button>
|
||||
</section>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Sabit alt çağrı kartı — liste/panel yokken ve kart kapatılmadıysa */}
|
||||
{monteEdildi && !panelde && !kartGizle && !acik ? (
|
||||
@@ -334,7 +430,9 @@ export function SihirbazBolumu({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{monteEdildi ? (
|
||||
{/* Üretim başladığında modalı kaldır; yeni panelin geçişini Motion
|
||||
üstlenir ve odağı panel içeriğine taşır. */}
|
||||
{monteEdildi && durum === "bos" ? (
|
||||
<SihirbazModal
|
||||
acik={acik}
|
||||
onAcikDegisti={acikDegisti}
|
||||
|
||||
@@ -32,7 +32,10 @@ export function SihirbazModal({
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={acik} onOpenChange={onAcikDegisti}>
|
||||
<DialogContent className="max-h-[92dvh] gap-0 overflow-y-auto p-6 sm:max-w-3xl">
|
||||
<DialogContent
|
||||
data-sihirbaz-modal
|
||||
className="max-h-[92dvh] gap-0 overflow-y-auto p-6 sm:max-w-3xl"
|
||||
>
|
||||
<DialogTitle className="sr-only">Tercih listesi sihirbazı</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Sıralamana uygun 24 tercihlik listeni oluştur.
|
||||
|
||||
Reference in New Issue
Block a user