Update UI components and database schema
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:
bilalgursen
2026-07-26 05:17:17 +03:00
parent 209d203fe8
commit d93cf742ff
16 changed files with 1062 additions and 621 deletions

View File

@@ -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}