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,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}
|
||||
|
||||
Reference in New Issue
Block a user