From 15e2c4c9611db9118b81891a725123c582c363c0 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 9 Aug 2026 04:44:19 +0300 Subject: [PATCH] feat: implement chat history deletion functionality Added a new DELETE endpoint to allow users to clear their chat history. This functionality ensures that while the chat messages are removed, any credits spent remain intact. Additionally, updated the ListemGovde component to reflect changes in user preferences and integrated a modal for notifying users about preference changes. Enhanced the SohbetClient component to include a button for clearing chat history, improving user experience and interface clarity. --- data/app.db | Bin 585728 -> 585728 bytes src/app/api/soru/route.ts | 14 + .../rapor/components/listem-govde.tsx | 117 +++++- .../rapor/components/listem-icerik.tsx | 23 +- .../rapor/components/sohbet-client.tsx | 157 ++++++-- .../components/tercih-degisti-modali.tsx | 342 ++++++++++++++++++ .../rapor/components/uretim-mesaji.ts | 2 +- src/features/rapor/tercih-farki.ts | 25 +- 8 files changed, 591 insertions(+), 89 deletions(-) create mode 100644 src/features/rapor/components/tercih-degisti-modali.tsx diff --git a/data/app.db b/data/app.db index 6fd4f283ded935770649880daa1e61bac5708a12..e6f6f63a5f077ba6e4b9b16c9d2d2b67b81829fe 100644 GIT binary patch delta 1706 zcma)7Uu+X~9N)FqbuDA#LBxoY-^wQ2?3KH=*Y;dcHwrPqGMF|_qk-T3>2~Yg74FYS z7qP~J9BsE%-x-H+ll+{Ke3m%UpsnncPPBt(mB#`KD=7*pznoavwi-yd~U~kPq&bq zl%NG#%upFz5U8Y-NKw2fvJ@}DMkX2O3vq`aeckr8!L1cpDIyk`k}k*-vW85oVpY=6 zz`z79V`X3fm9Zwn>!@rL6&dvv-I}htwQ0B34~HcM>$r&Il`7UHQ;{?k9%JYwNdqhc zLa_pX8scI!O-B)L{lTP2dXtQ=z~NtU0Or<^Y)B{*qvL5LE8bI@O(9@`C{4sjl-w$C z$mNnAL$7+;#-+w3P+WFvibudVt|*Nq6&NHmF6(e&lr&WI?jS?b#HKkBPheEGikJdM z5rY$6Hf6o3J{%slno`#zI$wX-_Grb+j@VdQsWz61kO9$hs}>+&4ZMO#42qOBeM6c6 z*|9XsRB;q?%ddAkA?a(<|1<`+_PJHq1usnnCRU_lpif7tTr{y>K}~Y+9;5@KVo5V4 zZ4#id4#x&YMq^1orpwb_d3afo0dh#iDAFux1WBe`goXIhO4S0nO=e{fTnVx$!ah(z zK{W#vaM5gB(Hl!(Zwdp<2r4%!OVeHw|5Zzn0v!Q0F|Zazk&VZZOG>GZe+$sPl7)?MYfjm&DLbM@kv2geR7%9Kp>`}zq+c|UCDCw4}Sc+9# zD{5@=#H~+H(Y|0lx3!Wd<7}E0=t6;F8BT-|z&w=^_$0+7g+eNsPAAj!+6ese?4CZF zq2uvFMxrDp4f7%vIEv$w3?(MSRGLn+EM_tQf6g9x?z6z<&Tl*4sdqg8X6NVz=8W<8 z`SZjE)G)qX{(Q*3^H$H9L!10SQtvo)v9o6*((1R4eADR%`}RkrT_Mov>mzey$Uc4< z)qg9sZSiLVU3PHhurJV6ADd~A{hlPy4z7eW#j-*I62zw|zQFNRf@V_;FC{sam$LQ! zr#}yev&6%euo2!vJgm3R-62ve*_$VX32YuwCBwku(njoM>)mJWZT4qdDLXhn=xe3w z!}AXU_UJ+nk+rWb^wg&ox-NxclSdx>?4^3~e2$RyKybb#IKLQPCe8)hXJ02m`P{qZ zw|A4I^ZgG*k8^OD7zia9hGv*}Bw__c7YK^?tY5$4{ zZQ`$zb{YTG}4(ru5GH}`F%nqG-!Z=WS$1z@$p M3NGG0+t&Nq?-J@!%K!iX delta 1330 zcma)4U1%It6yDjN>`!Vop{+kL+M6^ryNSDwko`d|rZ#UKVsa(Cw@xjVCy z^v-N!igY8=_Q99fp&f+c)Q2Vyigg)jKtim{j3Py_A}9@oVo^wGQSj9V?~Jv1Ei-2> zm-C(PeCIo}ba(I4-M!1tGq*qN8ewiXr>^dgx2l2USli`R)oNq!CG-b{jveB{_M>kI zD0}XuP;7j3&j)>hFfVd>p;F9c=$Fgn+0t|d76p3E6%k)75As}IKlx?1esE}aT5_7C z<}^<`O@K5kAx*|WL!^RdWkj%o6`;WyqzFCEcSlpDEX%TKTn!Qf8rGfru`|Z^^35fQ(Yuf7)z_S{zLAk%*xb=JVnFbjG zi-^d`HERgCCR9|Xsd!{SL<*`PiOLu0t?X%{GQ6zlO(18%n@)2c%#*np0&6lPehPvz zQcfd6xsZfq5rIOs#IhM7D^S^GIt_)1Fbm$cFC7Jkp3`Kg{!dLRW)QJgkz}u8-|I4xWV$x&rCn32 zlNz1nC{?HcuJC_Lg?g1zJx2x>sk2y40};+bphCx#=@M!9WZTZG8i5+BVig0aPG;)9 zAlKaKZ6jN)?|A1uHW(cr-Q5@u8yiNrSLarMe*eI5s#L0=(lpO!Dn%q@@;oYJ%0(z< z1Ss$VeWwR26zB!&k|8>Q{@Gk#oGJ;X!lU81e)1Zy{+Wxlhejet^yt!YYhuX`>nuB9 z9XY#^h>Zlh0|_-TY_%=lVRUv%u(~hYdnz>2@`fJ0GGx7WWh+9#eR^i4*E+TG!fJef zb#m*E>cWqIuU#HxFo^^n1_BS)5;vGjQDeeoo;KbxgRdK7Hq&Cf@*T6s_}mOejhw@D z8k65M?JlNm%^czKrIPTtq&>m09A(d!XiYd6se8y5!?aoMF{UFf@DwuW(H{RH z+-00-jUIZ0oIag(7DTx;A6opnOHBAZ5ZIh?k-r#_9G-@F*6 Q;hilsqHCKMWBVrl1?Ly$g#Z8m diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts index d77d610..305f284 100644 --- a/src/app/api/soru/route.ts +++ b/src/app/api/soru/route.ts @@ -105,6 +105,20 @@ export async function GET() { }); } +// Kullanıcının sohbet geçmişini siler. Kredi iadesi YOK: harcanan krediler +// ledger'da kalır, yalnızca mesajlar temizlenir (bir sonraki soruda danışman +// sıfırdan başlar; liste/özet bağlamı prompt'tan geldiği için korunur). +export async function DELETE() { + const session = await getSession(); + if (!session) { + return Response.json({ error: "Oturum gerekli." }, { status: 401 }); + } + await appDb + .delete(schema.chatMessages) + .where(eq(schema.chatMessages.userId, session.user.id)); + return Response.json({ ok: true }); +} + export async function POST(request: NextRequest) { // Oturum ve gövde ayrıştırma bağımsız — paralel başlar, hata önceliği aynı // kalır (önce 401, sonra 400). diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index bf7c2ea..63050be 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -13,27 +13,37 @@ import { FileText, Lock, PencilLine, + RefreshCcw, ShieldCheck, SlidersHorizontal, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { RaporListesi } from "./rapor-listesi"; import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; +import { tercihFarki } from "@/features/rapor/tercih-farki"; +import { TercihDegistiModali } from "./tercih-degisti-modali"; import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi-lazy"; import { YurtdisiHaritalarLazy } from "@/features/liste/components/yurtdisi-haritalar-lazy"; import { pinleriTuret } from "@/lib/harita-pinler"; -import type { RaporSonuc } from "@/features/rapor/types/rapor"; +import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor"; import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client"; import { RevizyonKutusu } from "./revizyon-kutusu"; -import { profilDuzenlemeyiAc } from "@/features/liste/hooks/use-tercih-profili"; +import { + profilDuzenlemeyiAc, + useTercihProfili, +} from "@/features/liste/hooks/use-tercih-profili"; +import { UNIVERSITE_TIPI_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler"; +import { PUAN_TURLERI } from "@/types/yokatlas"; export function ListemGovde({ rapor, + params, adaySira, hasPaket, kilitAcilisi, kilitliBaslangic, kredi, + raporKredi, sohbetGecmisi, ozet, kalanHak, @@ -41,6 +51,9 @@ export function ListemGovde({ paketKredi, }: { rapor: RaporSonuc; + /** Kayıtlı listenin üretildiği tercihler — başlıktaki butonun "düzenle" mi + * "güncelle" mi olacağı bunun güncel profille farkından çıkar. */ + params: RaporParams; adaySira: number; hasPaket: boolean; /** Ödeme dönüşü: blur çözülme animasyonuyla aç */ @@ -48,6 +61,9 @@ export function ListemGovde({ /** Paketsizde ACIK_SATIR; paketlide undefined (tamamı açık) */ kilitliBaslangic?: number; kredi: number; + /** Bir liste üretiminin kredi bedeli — fark modalındaki güncelleme aynı + * kapıdan geçtiği için aynı bedeli gösterir. */ + raporKredi: number; sohbetGecmisi: Mesaj[]; ozet?: DanismanOzeti; kalanHak: number; @@ -71,6 +87,44 @@ export function ListemGovde({ const acikSatir = kilitliBaslangic ?? rapor.tercihler.length; + // Profil client'ta (localStorage) çözülür: ilk render'da null döner, yani + // SSR ile aynı "Tercihlerimi düzenle" hâli basılır — hidrasyon uyuşmazlığı + // olmaz, fark varsa buton sonraki render'da "güncelle"ye döner. + const profil = useTercihProfili(); + const farkVar = useMemo( + () => (profil ? tercihFarki(params, profil).length > 0 : false), + [params, profil], + ); + const [farkModali, setFarkModali] = useState(false); + + // Danışmanın sistem prompt'una gerçekten giren bağlam (bkz. app/api/soru). + // Sohbet başlığındaki "Danışmanın bildikleri" ipucunda gösterilir; prompt + // değişirse bu liste de güncellenmeli. + const danismaninBildikleri = useMemo(() => { + const satirlar = [ + `Başarı sıralaman ${adaySira.toLocaleString("tr-TR")} ve puan türün ${PUAN_TURLERI[params.tur]}`, + hasPaket + ? "24'lük listenin tamamı: program, üniversite, şehir, taban sıralaması ve risk etiketi" + : `Listenin açık ilk ${acikSatir} satırı — kilitli tercihleri danışman da görmüyor`, + ]; + if (params.kategoriler?.length) { + satirlar.push(`İlgi alanların: ${params.kategoriler.join(", ")}`); + } + if (params.iller?.length) { + satirlar.push(`Seçtiğin iller: ${params.iller.join(", ")}`); + } + if (params.universiteTipi && params.universiteTipi !== "farketmez") { + satirlar.push( + `Üniversite tipi tercihin: ${UNIVERSITE_TIPI_ETIKET[params.universiteTipi]}`, + ); + } + if (params.oncelikler?.length) { + satirlar.push(`Önceliklerin: ${params.oncelikler.join(", ")}`); + } + satirlar.push("Bu sohbetteki son mesajların"); + return satirlar; + }, [adaySira, params, hasPaket, acikSatir]); + // Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez). // Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları // için observer hiç kurulmaz. @@ -126,18 +180,37 @@ export function ListemGovde({
{/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte - TercihProfiliKapisi) profili günceller, /listem'e döner ve - değişiklik varsa "tercihlerin değişti" uyarısı karşılar. */} - + TercihProfiliKapisi) profili günceller, /listem'e döner. + Profil kayıtlı listeden ayrıldığı anda bu buton "Tercihlerin + değişmiş"e döner ve TercihDegistiModali'nı açar — fark + dökümü ve kredi kapısı orada tek yerde yaşar. + + Renk: varsayılan (primary) buton. Kardeşleri outline olduğu + için tek dolu buton olması dikkati zaten çekiyor; turuncu + bu sayfada paywall CTA'sının rengi, amber ise risk dilinin + "az riskli" tonu — ikisi de burada yanlış sinyal olurdu. */} + {farkVar ? ( + + ) : ( + + )} {hasPaket ? ( <>
); } diff --git a/src/features/rapor/components/listem-icerik.tsx b/src/features/rapor/components/listem-icerik.tsx index 2aa36e1..9cd06ed 100644 --- a/src/features/rapor/components/listem-icerik.tsx +++ b/src/features/rapor/components/listem-icerik.tsx @@ -21,7 +21,6 @@ import type { Mesaj } from "./sohbet-client"; import { ListemGovde } from "./listem-govde"; import { ListemBosCta } from "./listem-bos-cta"; import { ListeUretici } from "./liste-uretici"; -import { TercihDegistiUyarisi } from "./tercih-degisti-uyarisi"; const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", { maximumFractionDigits: 0, @@ -89,13 +88,11 @@ export async function ListemIcerik({ const raporParams = satir!.params as RaporParams; // Danışman özeti sohbetin açılış mesajı olarak gösterilir; sohbet // /api/soru üzerinden zaten liste + seçim bağlamıyla çalışıyor. - // Paketsizde rapor zaten maskeli: 1 uyarı açık, kalanının yalnızca sayısı var. + // Paketsizde genelDegerlendirme zaten kırpılmış gelir (bkz. raporMaskele). const danismanOzeti = rapor.genelDegerlendirme ? { metin: rapor.genelDegerlendirme, - uyarilar: rapor.uyarilar, kilitli: !user.hasPaket, - kilitliUyariSayisi: user.hasPaket ? undefined : rapor.kilitliUyariSayisi, } : undefined; @@ -112,25 +109,21 @@ export async function ListemIcerik({ PDF artık açık. ) : null} - {/* Tercihler liste kurulduktan sonra değiştiyse (sıra, alan, il, tip, - öncelik) uyarı burada karşılar ve listeyi yeni tercihlerle yeniden - kurmayı önerir — üretimle aynı kredi kapısından geçerek. Fark - hesabı client'ta yapılır: güncel profil localStorage'ta yaşar. */} - {/* Başlık ve revizyon/PDF aksiyonları listenin kendi başlık satırında - (bkz. listem-govde.tsx) — sayfa üstünde ayrı bir header yok. */} + (bkz. listem-govde.tsx) — sayfa üstünde ayrı bir header yok. + Tercihler liste kurulduktan sonra değiştiyse (sıra, alan, il, tip, + öncelik) başlıktaki buton "Tercihlerin değişmiş"e döner ve fark + modalını açar; fark hesabı client'ta yapılır (güncel profil + localStorage'ta yaşar), o yüzden params buraya prop olarak iner. */} (initialMesajlar ?? []); const [girdi, setGirdi] = useState(""); const [bekliyor, setBekliyor] = useState(false); const [yuklendi, setYuklendi] = useState(initialMesajlar != null); const [kredi, setKredi] = useState(baslangicKredi); + const [temizleAcik, setTemizleAcik] = useState(false); + const [temizleniyor, setTemizleniyor] = useState(false); const router = useRouter(); // Markdown chunk'ını sohbet görünür olur olmaz ısıt: ilk danışman cevabı @@ -206,6 +223,29 @@ export function SohbetClient({ } } + /** + * Sohbet geçmişini hem sunucudan hem ekrandan siler. Krediler iade + * edilmez; danışman bir sonraki soruda sıfır geçmişle başlar (rapor özeti + * ve liste bağlamı prompt'tan geldiği için korunur). + */ + async function sohbetiTemizle() { + if (temizleniyor) return; + setTemizleniyor(true); + try { + const res = await fetch("/api/soru", { method: "DELETE" }); + if (!res.ok) throw new Error("silinemedi"); + setMesajlar([]); + setTemizleAcik(false); + // Sunucudan gelen geçmiş (listem sayfasının initialMesajlar'ı) da boşalsın + router.refresh(); + toast.success("Sohbet temizlendi."); + } catch { + toast.error("Sohbet temizlenemedi, tekrar dener misin?"); + } finally { + setTemizleniyor(false); + } + } + const krediYok = kredi < 1; return ( @@ -213,14 +253,57 @@ export function SohbetClient({ aria-label="Danışman sohbeti" className={cn( "flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white", - className ?? "h-[calc(100dvh-16rem)] min-h-[400px]", + className ?? "h-[calc(100dvh-24rem)] min-h-[340px]", )} > -
-

Danışmanla konuş

+
+ {bildikleri?.length ? ( + + + + + + +

+ Danışmanın bildikleri — cevaplar bu bağlamla üretilir: +

+
    + {bildikleri.map((b) => ( +
  • {b}
  • + ))} +
+

+ Adın, e-postan ve netlerin danışmanla paylaşılmaz. +

+
+
+
+ ) : null} + {mesajlar.length > 0 ? ( + + ) : null} @@ -228,6 +311,36 @@ export function SohbetClient({
+ + + + Sohbeti temizle + + Danışmanla olan tüm yazışman silinir ve geri getirilemez. Listen + ve kredilerin etkilenmez; yeni bir soruyla sıfırdan devam + edebilirsin. + + + + + + + + +
@@ -241,36 +354,10 @@ export function SohbetClient({ {ozet.metin} - {ozet.uyarilar?.length ? ( -
    - {ozet.uyarilar.map((uyari) => ( -
  • - - {uyari} -
  • - ))} -
- ) : null} {ozet.kilitli ? (

- {ozet.kilitliUyariSayisi ? ( - <> - {ozet.kilitliUyariSayisi} uyarı daha kilitli — - sıralamana özel uyarıların pakette. - - ) : ( - <> - Değerlendirmenin tamamı ve sıralamana özel uyarılar - pakette. - - )} + Değerlendirmenin tamamı pakette.

) : null}
diff --git a/src/features/rapor/components/tercih-degisti-modali.tsx b/src/features/rapor/components/tercih-degisti-modali.tsx new file mode 100644 index 0000000..0d81fd6 --- /dev/null +++ b/src/features/rapor/components/tercih-degisti-modali.tsx @@ -0,0 +1,342 @@ +"use client"; + +// /listem'deki "tercihlerin değişmiş" modalı: kayıtlı liste hangi tercihlerle +// kurulduysa (rapor params) kullanıcının güncel profili ondan ayrıldığında +// liste başlığındaki buton belirir; bu modal o butonun içeriğidir — hangi +// alanların değiştiğini gösterir ve listeyi yeni tercihlere göre yeniden +// kurmayı önerir. +// +// Yeniden kurulum normal üretimle AYNI kapıdan geçer (listeOlustur): 3 kredi, +// aynı requestId idempotency'si, aynı kredi-yetersiz/iade davranışı. Ayrı bir +// "güncelleme" ürünü ya da indirimli yol yok — kullanıcı yeni bir liste alıyor, +// bedeli de yeni liste bedeli. +// +// Renk: modalın kabuğu marka primary'si (bg-primary/10 ikon hücresi), ana +// aksiyon ise ürünün CTA turuncusu — kilidi açan, kredi harcayan, ödemeye +// götüren butonlar site genelinde bu tonda. Amber bilerek kullanılmıyor: +// sarı bu üründe risk dilinin "az riskli" tonu (bkz. rapor-listesi +// RISK_STIL). +// +// Fark tablosunun içi ayrı bir dil: kırmızı/yeşil burada risk değil diff +// anlamında (eski değer / yeni değer). Karışmasın diye tablo kendi kutusuna +// kapalı duruyor ve her hücrede −/+ işareti var — risk noktalarında olduğu +// gibi renk tek başına anlam taşımıyor. + +import { useMemo, useRef, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { ArrowRight, Check, RefreshCcw, SlidersHorizontal } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { olay, siraKovasi } from "@/lib/analitik"; +import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili"; +import { listeOlustur } from "@/features/rapor/rapor-actions"; +import type { RaporParams } from "@/features/rapor/types/rapor"; +import { farkSatirlari, tercihFarki } from "@/features/rapor/tercih-farki"; +import { useUretimMesaji } from "./uretim-mesaji"; + +type Durum = + | { ad: "bekliyor" } + | { ad: "uretiliyor" } + | { ad: "tamamlandi" } + | { ad: "kredi-yetersiz" } + | { ad: "hata"; mesaj: string }; + +export function TercihDegistiModali({ + open, + onOpenChange, + params, + kredi, + raporKredi, + hasPaket, +}: { + open: boolean; + onOpenChange: (acik: boolean) => void; + /** Kayıtlı listenin üretildiği tercihler */ + params: RaporParams; + kredi: number; + raporKredi: number; + hasPaket: boolean; +}) { + const router = useRouter(); + const profil = useTercihProfili(); + const [durum, setDurum] = useState({ ad: "bekliyor" }); + // Aynı deneme için sabit requestId: cevabı kaybolan istekte retry çifte + // harcamaz (bkz. liste-uretici.tsx ve spendCredits idempotency'si). + const requestIdRef = useRef(null); + + const farklar = useMemo( + () => (profil ? tercihFarki(params, profil) : []), + [params, profil], + ); + const satirlar = useMemo( + () => (profil ? farkSatirlari(params, profil, farklar) : []), + [params, profil, farklar], + ); + + // Üretim sırasında modal kapanmaz: istek 30 sn kadar sürüyor ve kredi çoktan + // harcanmış oluyor — kullanıcı yanlışlıkla ESC'leyip sonucu kaçırmasın. + const kilitli = durum.ad === "uretiliyor"; + + async function guncelle() { + if (!profil) return; + if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID(); + setDurum({ ad: "uretiliyor" }); + + try { + const sonuc = await listeOlustur({ + sira: profil.sira, + tur: profil.tur, + secimler: profil.secimler, + requestId: requestIdRef.current, + }); + requestIdRef.current = null; + if (sonuc.ok) { + olay("liste_guncellendi", { + tur: profil.tur, + sira_kovasi: siraKovasi(profil.sira), + degisen_alan: farklar.length, + }); + toast.success("Listen yeni tercihlerine göre yeniden kuruldu."); + // Sunucu params'ı tazelenince fark kalmaz ve başlıktaki buton + // kendiliğinden "Tercihlerimi düzenle"ye döner. + setDurum({ ad: "tamamlandi" }); + router.refresh(); + return; + } + if (sonuc.code === "KREDI" || sonuc.code === "PAKET") { + olay("kredi_bitti"); + setDurum({ ad: "kredi-yetersiz" }); + } else if (sonuc.code === "AUTH") { + router.push(`/giris?callback=${encodeURIComponent("/listem")}`); + } else { + setDurum({ ad: "hata", mesaj: sonuc.error }); + } + } catch { + setDurum({ + ad: "hata", + mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?", + }); + } + } + + const krediYeter = kredi >= raporKredi; + + return ( + { + if (kilitli) return; + onOpenChange(v); + }} + > + { + if (kilitli) e.preventDefault(); + }} + onPointerDownOutside={(e) => { + if (kilitli) e.preventDefault(); + }} + onInteractOutside={(e) => { + if (kilitli) e.preventDefault(); + }} + className="max-h-[92dvh] overflow-y-auto p-6 sm:max-w-md" + > + {durum.ad === "tamamlandi" ? ( +
+ + + +
+ Listen güncellendi + + Yeni tercihlerine göre yeniden kuruldu — arkadaki liste birkaç + saniye içinde tazelenir. + +
+ +
+ ) : ( +
+
+ + + +
+ + Tercihlerin değişmiş + + {/* Hangi alanların değiştiği hemen altındaki diff + tablosunda zaten duruyor; açıklama yalnızca sonucu ve + bedeli söylüyor. */} + + Listen eski tercihlerinle kuruldu. Yeni liste bunun yerine + geçer ve {raporKredi} kredi düşer. + +
+
+ + {/* Diff yerleşimi: alan adı üstte, eski ve yeni değer yan yana + iki hücrede — klasik diff dili (eski kırmızı, yeni yeşil + zemin). −/+ işaretleri ve sr-only "Eski:/Yeni:" etiketleri + sayesinde renk tek başına anlam taşımaz. */} +
+ {satirlar.map((satir, i) => ( +
0 ? "border-t border-slate-200" : undefined} + > +
+ {satir.etiket} +
+
+ + + − + + Eski: + + {satir.eski} + + + + + + + + Yeni: + + {satir.yeni} + + +
+
+ ))} +
+ + {durum.ad === "uretiliyor" ? ( + + ) : durum.ad === "kredi-yetersiz" ? ( + + ) : ( + // Aksiyon satırı: solda ikincil çıkış (outline), sağda ana + // aksiyon (turuncu). Turuncu ürünün kendi CTA rengi — kilidi + // açan, kredi harcayan, ödeme yaptıran yollar hep bu tonda. +
+ {durum.ad === "hata" ? ( +

{durum.mesaj}

+ ) : null} + {!krediYeter && durum.ad !== "hata" ? ( +

+ Güncelleme {raporKredi} kredi; bakiyen {kredi} kredi. +

+ ) : null} +
+ + {durum.ad === "hata" ? ( + + ) : krediYeter ? ( + + ) : ( + + )} +
+
+ )} +
+ )} +
+
+ ); +} + +function UretimSatiri() { + const mesaj = useUretimMesaji(); + return ( +
+

+ + Listen yeni tercihlerine göre kuruluyor… +

+

{mesaj}

+

+ Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. +

+
+ ); +} + +function KrediYetersiz({ + hasPaket, + raporKredi, +}: { + hasPaket: boolean; + raporKredi: number; +}) { + return ( +
+

+ {hasPaket + ? `Güncelleme ${raporKredi} kredi ve bakiyen yetmedi. Kredi yüklediğinde listen ve sohbet geçmişin duruyor, kaldığın yerden devam edersin.` + : `Güncelleme ${raporKredi} kredi ve bakiyen yetmedi. Tercih Dönemi Paketi'yle kredilerin, tam liste, PDF ve revizyon hakları birlikte gelir.`} +

+ +
+ ); +} diff --git a/src/features/rapor/components/uretim-mesaji.ts b/src/features/rapor/components/uretim-mesaji.ts index 11d8715..cba71a0 100644 --- a/src/features/rapor/components/uretim-mesaji.ts +++ b/src/features/rapor/components/uretim-mesaji.ts @@ -1,7 +1,7 @@ "use client"; // Liste üretiminin bekleme metinleri tek kaynakta: ilk kurulum (ListeUretici) -// ve tercih değişikliği sonrası yeniden kurulum (TercihDegistiUyarisi) aynı +// ve tercih değişikliği sonrası yeniden kurulum (TercihDegistiModali) aynı // dili ve ritmi paylaşsın diye. import { useEffect, useState } from "react"; diff --git a/src/features/rapor/tercih-farki.ts b/src/features/rapor/tercih-farki.ts index 12897ef..0ca77c2 100644 --- a/src/features/rapor/tercih-farki.ts +++ b/src/features/rapor/tercih-farki.ts @@ -1,6 +1,6 @@ // Kayıtlı listenin üretildiği tercihler (rapor params) ile kullanıcının şu -// anki tercih profilini karşılaştırır. /listem'deki "tercihlerin değişti" -// uyarısı ve güncelleme CTA'sı bu farktan doğar. +// anki tercih profilini karşılaştırır. /listem başlığındaki "Tercihlerin +// değişmiş" butonu ve açtığı güncelleme modalı bu farktan doğar. // // İzomorfik: yalnızca tip ve etiket sabitlerine bağlı — hem client bileşeni // hem sunucu tarafı tüketebilir. @@ -35,20 +35,6 @@ export const FARK_ETIKET: Record = { oncelikler: "önceliklerin", }; -/** Sıra/tür/seçim üçlüsünün kararlı imzası — uyarının "bu değişikliği - * gizle" kaydı bu imzaya bağlanır (tercih yine değişirse uyarı geri gelir). */ -export function profilImzasi(profil: TercihProfili): string { - const s = profil.secimler; - return JSON.stringify([ - Math.round(profil.sira), - profil.tur, - [...s.kategoriler].sort(), - [...s.iller].sort(), - s.universiteTipi, - [...s.oncelikler].sort(), - ]); -} - // Sıra bağımsız küme kıyası: sihirbaz seçimleri tekrarsız olarak doğrulanır // (bkz. sihirbazDogrula), aynı seçimlerin farklı sırayla kaydedilmesi // "değişiklik" değildir. @@ -88,13 +74,6 @@ export function tercihFarki( return farklar; } -/** "başarı sıralaman ve ilgi alanların" — uyarı cümlesinin ortası. */ -export function farkCumlesi(farklar: FarkAlani[]): string { - const etiketler = farklar.map((f) => FARK_ETIKET[f]); - if (etiketler.length <= 1) return etiketler[0] ?? ""; - return `${etiketler.slice(0, -1).join(", ")} ve ${etiketler[etiketler.length - 1]}`; -} - export type FarkSatiri = { alan: FarkAlani; etiket: string; eski: string; yeni: string }; /** Değişen her alanın eski → yeni değeri; uyarıda tek tek gösterilir. */