From 30b9b41a22ed485fc5f84271407244675ad7a936 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Tue, 11 Aug 2026 12:54:46 +0300 Subject: [PATCH] refactor: update site header and chat components for improved user experience Replaced the static logo in the SiteHeader with an animated component for enhanced visual feedback. Updated the PaketSatinAl component to include a dynamic link based on user context. Enhanced the ListeUretici and ListemGovde components with improved layout and messaging features, including the addition of a new initial preference comment display in the SohbetClient. Adjusted styling for consistency across components and improved loading states with skeleton screens. --- data/app.db | Bin 712704 -> 712704 bytes src/components/animasyonlu-mark.tsx | 193 ++++++++++++++++++ src/components/site-header.tsx | 13 +- .../odeme/components/paket-satinal.tsx | 5 +- .../rapor/components/liste-uretici.tsx | 89 ++++++-- .../rapor/components/listem-govde.tsx | 16 +- .../rapor/components/listem-icerik.tsx | 39 ++-- .../rapor/components/sohbet-client.tsx | 188 +++++++++++------ .../components/tercih-degisti-modali.tsx | 2 +- 9 files changed, 437 insertions(+), 108 deletions(-) create mode 100644 src/components/animasyonlu-mark.tsx diff --git a/data/app.db b/data/app.db index 8254db58215dfceb5268a3af5d3e9089ecd757a5..5a5081f603ede9d4d973578792359a823809fb84 100644 GIT binary patch delta 4969 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 delta 2179 zcmai!X^a!+8OJ^2@$Qa~nLtU@P!g7{0|+pT?eVoLr1p4x&e$`)JyC1M$Jibp>w9b~ zunQC+A39#MD=FXujRx4 z)uZQq^?RQ8{~jyr2Upe)u6_+zesLE8EWdmF6;uPe6d-s`37&&JlRfjm8E|}V`*;RX zC=}2OIJk|rJx)J&WJEpQTIP!}5=#&wF4l)ps#_TrB(lyYJybtl93;KzXtjiKSk#aX zQe;3%xAMk_(bV^QdM#JTMtQ6G2-U@;C|>J(J^i6*sSNvmiz{wxB;7`9ddcmz7b6ns zc1uhmS?-GMfF7cVf zMxBvd)WiC`DHB~Lvm7Vz<|Rwm6(80u2LI4tB8r&FfsL6RLtwPBtM_Czkssl0@(|i)ijA`9#1IFVzfwPb)|U?Wqi z?Hl|thhPf`c~jI(4a|hKno0INea@P5^UHjpUJW_yrev|!^d<+*r3mevy2nJEF;7LP zcibHBV%oMq)WJk+7J@PH&4TEmf~l-GYxR5Cj(aI*&3CG4!o}LTurFH*3pAUE7jm^K zQ^0%Eei%zU;Ac!_v0cWSsRWl!ISidDeNqIlBLr$my=R4Z&}iKs}vbOm1`Ij1;$Jyz;;8l%zf5_o|ef4=+k-I#XzCN9sY zr{gxAPoet+xsT$g2Kg~kM8AbRif*81kz+a^`ZltvYwB+4E^a=jCw0(%WgL*#Z^HU< zgZ=;*{miIWslLp;GJ^D~FQ-SHMZHS7nH;_FqFx2=Q^w)Z?@sE^Gtk~`NJ)Xdfj*0d z(0Syq$a~25krMh88bwbbzeG2XOUOT_6>FQdjv?dHx4#N((MQfjy&8g)3Sf)7?`b^% zjy5ufwA*wx1$qTNgQn3V;~mFuYe#ohQX{^$f1Ig(20+puv#QaNrM=T24s8YZZz?wf zSi1jHa6d#t@TmFr{P;ghbC2vwg4-3yeYK#C=zPkzG*?v@)JHYXY1Ff@_O+3I<>ctK zmA_5-r?y;Zyg7AY@$|2rP$rf2(d-&Ij;vh)r}JI;JPPQ?=U+Wtg<$*4gU#*SnVCFz z42XbXL`_5VGp8gPg5sVMRxc)9b&_?u?Y&aBI1E}TQsQHMGs_7%@swsZUlnpvQz{Q! zeF-Zy^HhpxyVD(p={4NtqBSf#cLNLZ`cB}|taRsjajFTu6S%2@?;>BIUq`N?zR8~B zz-2_fb{Ggf^{|SFUP%Kow-cC`y+?pwsL=`X#guU#x%)rLu_@(YJ#A4=PAL=b(*T9Q zd&SJhe-fuhFu6FgxX0kM42KTVAlSK* z#2PB4c*Bde{K-ayh>3AiuTr7-2G3Rqj%F-9%;u(gLq3b=l6*r9n*vdjpR*Gon_aNb zE>kftN+d)2)3I>NF$@n#ccyKrl`}3WLJ$_S8#ko+g+mL4exuwfw4I%WMWfAb#f=WT zbvojQ7MA#Krxfk*ox;N6!oxPSa6%kjyeJMYxHffT1-OUK=aT>5q_eBQ*;VlDs`ADv z1Yw5`D}IBj+v=mhpEako2eik5AAx(o?`pmR=)ixXCE!_P4O-JFlplh(bXM8A4&gI% z``}BenTfa#{aXpmD82#6#!HYHvfH79^4fcl0I8M~j*ygz4HoP~}mpre}Q%`0z&wDLCrI8ML8p4kmS@^AkD=;hElaA0!x zL%o4>e>g| zs^(*ju*s%06X8wx6108#f6V{~6lfT^sxHI72gZQRdzJe}^Y{KDA9xr3&UkDj5c#v8 z!zij!Dm1^=_~$;F3#l%ul$)4OA@!*p+Sxe$s5cJD;j6HIvT_Z+t=_Jia#i4Ug$7hh ev$~m+;PuVruY!~7AHjE3lka^1w`R}YgZ~Q)OuY>N diff --git a/src/components/animasyonlu-mark.tsx b/src/components/animasyonlu-mark.tsx new file mode 100644 index 0000000..fb6ff7a --- /dev/null +++ b/src/components/animasyonlu-mark.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useEffect, useRef, useState, type CSSProperties } from "react"; +import { motion, useReducedMotion } from "motion/react"; +import { cn } from "@/lib/utils"; + +// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı (Motion). +// İki varyant: +// +// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi → +// şeftalinin yeri, şeftali → turuncunun, turuncu → mavinin), üç adımda +// herkes başlangıç formuna döner. Uçuş sırasında kare hafifçe küçülüp +// solar, varışta toparlanır. Site header'daki logo hover'da bunu oynatır. +// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi +// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli). +// +// Not: motion.rect'te `x`/`y` transform değeridir (attribute değil) — kareler +// attribute yerine translate ile konumlanır; scale, transform-box: fill-box +// sayesinde karenin kendi merkezinden uygulanır. + +const POZISYONLAR = [ + [27, 56], + [91, 29], + [91, 95], +] as const; + +const KARELER = [ + { boyut: 57, renk: "#3b82f6", baslangic: 0 }, + { boyut: 56, renk: "#fdc7a7", baslangic: 1 }, + { boyut: 56, renk: "#ff5a00", baslangic: 2 }, +] as const; + +// Rota: tam tur 3.6s — 3 etap × (%25 yol = 0.9s + %8.3 varışta soluklanma). +// 0.9s'lik yolculuk, nabız varyantının küçül-büyü temposuyla aynı hızda. +const ROTA_SURE = 3.6; +// Kalkışlar kademeli: kareler eş zamanlı değil, danışman başlığındaki gibi +// sırayla (mavi → şeftali → turuncu) yola çıkıp küçülür. +const ROTA_GECIKME_ADIMI = 0.2; +const KONUM_ZAMANLARI = [0, 0.25, 1 / 3, 0.583, 2 / 3, 0.917, 1]; +// Nabız (rota içi): her yolculuğun ortasında dibe vurur, varışta tam form. +const ROTA_NABIZ_ZAMANLARI = [ + 0, 0.125, 0.25, 1 / 3, 0.458, 0.583, 2 / 3, 0.792, 0.917, 1, +]; +const ROTA_NABIZ_SCALE = [1, 0.84, 1, 1, 0.84, 1, 1, 0.84, 1, 1]; +const ROTA_NABIZ_OPACITY = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1]; + +// Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe +// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez. +const NABIZ_SURE = 1.8; +const NABIZ_GECIKMELER = [0, 0.6, 1.2] as const; +const SINE = [0.37, 0, 0.63, 1] as const; + +/** Kare i için durak koordinatları: kendi yeri → sıradaki → ondan sonraki → + * kendi yeri. Duraklarda bekleme için her hedef iki kez tekrarlanır + * (KONUM_ZAMANLARI ile hizalı). */ +function rota(baslangic: number, eksen: 0 | 1): number[] { + const [p0, p1, p2] = [0, 1, 2].map( + (adim) => POZISYONLAR[(baslangic + adim) % 3][eksen], + ); + return [p0, p1, p1, p2, p2, p0, p0]; +} + +/** + * Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner; + * `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site + * header'daki logo linki) ve hover bitince kareler yumuşakça yerine oturur. + */ +export function AnimasyonluMark({ + className, + sadeceHover = false, + varyant = "rota", + renk = "marka", +}: { + className?: string; + sadeceHover?: boolean; + varyant?: "rota" | "nabiz"; + /** "marka": logodaki üç renk; "turuncu": tüm kareler sitenin CTA turuncusu + * (orange-500) — ör. danışman panel başlığı. */ + renk?: "marka" | "turuncu"; +}) { + const [hover, setHover] = useState(false); + const svgRef = useRef(null); + const azHareket = useReducedMotion(); + const aktif = !azHareket && (!sadeceHover || hover); + + // Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site + // header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine + // gelindiğinde de animasyon başlar. `group`'suz kullanımda svg'ye düşer. + useEffect(() => { + if (!sadeceHover) return; + const hedef = svgRef.current?.closest(".group") ?? svgRef.current; + if (!hedef) return; + const ac = () => setHover(true); + const kapat = () => setHover(false); + hedef.addEventListener("mouseenter", ac); + hedef.addEventListener("mouseleave", kapat); + return () => { + hedef.removeEventListener("mouseenter", ac); + hedef.removeEventListener("mouseleave", kapat); + }; + }, [sadeceHover]); + + return ( + + ); +} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 72c5fd3..6c54015 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -1,6 +1,6 @@ import { Suspense } from "react"; -import Image from "next/image"; import Link from "next/link"; +import { AnimasyonluMark } from "@/components/animasyonlu-mark"; import { KatalogArama } from "@/features/katalog/components/katalog-arama"; import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav"; import { getRehberAramaKayitlari } from "@/lib/rehber"; @@ -15,14 +15,9 @@ export function SiteHeader() { aria-label="KolayTercih" className="group order-1 flex min-w-0 shrink items-center font-bricolage" > - + {/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da + kareler danışman panelindeki nabız animasyonuyla canlanır. */} + Kolay diff --git a/src/features/odeme/components/paket-satinal.tsx b/src/features/odeme/components/paket-satinal.tsx index fa05aff..6693d6f 100644 --- a/src/features/odeme/components/paket-satinal.tsx +++ b/src/features/odeme/components/paket-satinal.tsx @@ -75,7 +75,10 @@ export async function PaketSatinAl() {
Paketin aktif —{" "} - + {/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok + sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim + de yoksa boş durum sihirbaza yönlendirir. */} + listeni oluştur
diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx index b189758..b5f36dc 100644 --- a/src/features/rapor/components/liste-uretici.tsx +++ b/src/features/rapor/components/liste-uretici.tsx @@ -21,6 +21,8 @@ import { listeOlustur } from "@/features/rapor/rapor-actions"; import { olay, siraKovasi } from "@/lib/analitik"; import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler"; import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil"; +import { Message } from "@/components/ui/message"; +import { DanismanAvatar, DanismanBaslik } from "./sohbet-client"; import { useUretimMesaji } from "./uretim-mesaji"; type Durum = @@ -114,7 +116,7 @@ export function ListeUretici({ // Paketli kullanıcıya paket satma: onun ürünü top-up. Liste ve sohbet // geçmişi kalıcı; kredi yükleyince kaldığı yerden devam eder. return ( -
+

Kredin bu liste için yetmiyor @@ -140,7 +142,7 @@ export function ListeUretici({ if (durum.ad === "hata") { return ( -
+

Liste kurulamadı

{durum.mesaj} @@ -173,27 +175,78 @@ function UretimGovdesi() { const mesaj = useUretimMesaji(); return ( -

-
-

Listen hazırlanıyor

-

{mesaj}

+
+
+

+ Listen hazırlanıyor +

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

-
+ - {/* Gelmekte olan tercih listesinin iskeleti */} -
-
- - + {/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda, + danışman sağda — üretim biter bitmez ekran aynı yerlere oturur. */} +
+ {/* Gelmekte olan tercih listesinin iskeleti */} +
+
+ + +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))}
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} + + {/* Danışman paneli üretim sırasında da açık: aşama mesajları danışman + balonunda shimmer ile akar; liste kurulunca aynı bölgede 1. + tercihin yorumu açılış balonu olarak bekler (bkz. sohbet-client + ilkYorum). Mobilde panel iskeletin üstüne alınır — bekleyenin + gördüğü ilk şey danışmanın çalıştığı olsun. */} +
); diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index 63050be..026a86c 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -26,7 +26,12 @@ import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi- import { YurtdisiHaritalarLazy } from "@/features/liste/components/yurtdisi-haritalar-lazy"; import { pinleriTuret } from "@/lib/harita-pinler"; import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor"; -import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client"; +import { + SohbetClient, + type DanismanOzeti, + type IlkTercihYorumu, + type Mesaj, +} from "./sohbet-client"; import { RevizyonKutusu } from "./revizyon-kutusu"; import { profilDuzenlemeyiAc, @@ -46,6 +51,7 @@ export function ListemGovde({ raporKredi, sohbetGecmisi, ozet, + ilkYorum, kalanHak, paketFiyati, paketKredi, @@ -66,6 +72,8 @@ export function ListemGovde({ raporKredi: number; sohbetGecmisi: Mesaj[]; ozet?: DanismanOzeti; + /** Sohbet boşken açılışta gösterilen 1. tercih yorumu */ + ilkYorum?: IlkTercihYorumu; kalanHak: number; paketFiyati: string; paketKredi: number; @@ -140,7 +148,7 @@ export function ListemGovde({ ); return ( -
+
{/* Harita: listenin coğrafyası — ile dokun, üniversiteleri gör */}
) : ( @@ -306,8 +315,9 @@ export function ListemGovde({ baslangicKredi={kredi} initialMesajlar={sohbetGecmisi} ozet={ozet} + ilkYorum={ilkYorum} bildikleri={danismaninBildikleri} - className="h-[360px]" + className="h-[480px]" />
-
+ {/* Üretim gövdesi nihai sayfanın iki kolonlu grid'ini kurduğu için + main de nihai sayfayla aynı genişlikte (7xl) — üretim bitince + içerik zıplamadan yerine oturur. Boş durum kendi max-w'siyle + ortalanır. */} +
{/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim burada koşar, animasyon ve hata/kredi durumları burada yaşar. */} {uret ? ( @@ -95,6 +98,23 @@ export async function ListemIcerik({ kilitli: !user.hasPaket, } : undefined; + // 1. tercihin yorumu sohbetin açılış balonu olur: üretim panelindeki + // "ilk tercihinin yorumunu burada okuyacaksın" sözünün karşılığı. İlk + // satır ACIK_SATIR maskesinde her zaman açık, yani paketsizde de gerçek + // Yapay Zeka metnidir. + const ilkTercih = + rapor.tercihler.find((t) => t.sira === 1) ?? rapor.tercihler[0]; + const ilkProgram = ilkTercih + ? rapor.programlar[ilkTercih.programId] + : undefined; + const ilkYorum = + ilkTercih && ilkProgram + ? { + program: ilkProgram.isim, + universite: ilkProgram.universite, + metin: ilkTercih.gerekce, + } + : undefined; return ( <> @@ -126,20 +146,11 @@ export async function ListemIcerik({ raporKredi={RAPOR_KREDI} sohbetGecmisi={sohbetGecmisi as Mesaj[]} ozet={danismanOzeti} + ilkYorum={ilkYorum} kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))} paketFiyati={PAKET_FIYATI} paketKredi={URUNLER.paket.credits} /> - - - - - Danışmana sor - -
@@ -152,7 +163,7 @@ export function ListemIcerikSkeleton() { -
+
{Array.from({ length: 10 }).map((_, i) => ( diff --git a/src/features/rapor/components/sohbet-client.tsx b/src/features/rapor/components/sohbet-client.tsx index e94196a..5e317a2 100644 --- a/src/features/rapor/components/sohbet-client.tsx +++ b/src/features/rapor/components/sohbet-client.tsx @@ -7,6 +7,7 @@ import Image from "next/image"; import { ArrowUp, Coins, Info, Lock, Sparkles, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; +import { AnimasyonluMark } from "@/components/animasyonlu-mark"; import { Button } from "@/components/ui/button"; import { ChatContainerContent, @@ -53,20 +54,53 @@ export interface DanismanOzeti { kilitli?: boolean; } -/** Danışman mesajlarının avatarı — KolayTercih markası. */ -function DanismanAvatar() { +/** Liste kurulunca sohbetin açılışında gösterilen 1. tercih yorumu. + * Metin ACIK_SATIR maskesinin her zaman açık bıraktığı ilk satırdan gelir, + * yani paketsiz kullanıcıda da gerçek Yapay Zeka yorumudur. */ +export interface IlkTercihYorumu { + program: string; + universite: string; + metin: string; +} + +/** Danışman panellerinin ortak başlığı: animasyonlu logo + site header'daki + * KolayTercih wordmark deseninde "DanışmanlaKonuş" (ilk kelime ince, ikinci + * kalın, boşluksuz). İşaret ortak AnimasyonluMark bileşeni — header + * logosuyla aynı nabız animasyonu, burada sürekli atar. */ +export function DanismanBaslik() { + return ( + // gap yok denecek kadar az (0.5): SVG viewBox'ının kendi iç boşluğu + // zaten nefes payı bırakıyor. +

+ + + Danışmanla + Konuş + +

+ ); +} + +/** Danışman mesajlarının avatarı — KolayTercih markası. `canli` iken (cevap + * beklenirken/yazılırken) statik görsel yerine başlıktaki nabız animasyonu + * oynar: danışmanın "düşündüğü" avatardan okunur. */ +export function DanismanAvatar({ canli = false }: { canli?: boolean }) { return ( - + {canli ? ( + + ) : ( + + )} ); } @@ -87,6 +121,7 @@ export function SohbetClient({ initialMesajlar, className, ozet, + ilkYorum, bildikleri, }: { baslangicKredi: number; @@ -96,6 +131,10 @@ export function SohbetClient({ className?: string; /** Raporun genel değerlendirmesi; sohbetin açılış mesajı olarak gösterilir */ ozet?: DanismanOzeti; + /** 1. tercihin Yapay Zeka yorumu; sohbet boşken (taze kurulmuş liste) + * özetin altında açılış balonu olarak durur — üretim panelindeki "ilk + * tercihinin yorumunu burada okuyacaksın" sözünün karşılığı. */ + ilkYorum?: IlkTercihYorumu; /** Danışmanın prompt'unda gerçekten yer alan bağlam maddeleri; başlıktaki * "Danışmanın bildikleri" ipucunda listelenir. Boşsa ipucu çizilmez. */ bildikleri?: string[]; @@ -253,62 +292,70 @@ 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-24rem)] min-h-[340px]", + // 18rem: sticky ofset (top-28 = 7rem) + geniş alt boşluk — eski 24rem + // sohbeti sıkıştırıyordu, 10-14rem denemeleri fazla yüksek geldi; + // panel görünür ama sayfaya baskın değil. + className ?? "h-[calc(100dvh-18rem)] min-h-[380px]", )} > + {/* Başlık solda, aksiyonlar sağda tek grup: dar panelde de sığar. + Temizle butonu ikon-only — metinli hâli başlıkla yarışıp taşıyordu. */}
- {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} + - - Sohbeti temizle - - ) : null} - - - {kredi} kredi - + + {kredi} kredi + +
@@ -363,6 +410,19 @@ export function SohbetClient({
) : null} + {ilkYorum && mesajlar.length === 0 ? ( + + +
+

+ 1. tercihin: {ilkYorum.program} — {ilkYorum.universite} +

+ + {ilkYorum.metin} + +
+
+ ) : null} {mesajlar.length === 0 ? (
) : ( - mesajlar.map((m) => + mesajlar.map((m, i) => m.role === "user" ? ( @@ -433,7 +493,11 @@ export function SohbetClient({ ) : ( - + {/* Cevap beklenirken/stream sürerken (son mesaj) avatar + nabız animasyonuna geçer. */} + {m.content ? ( Listen yeni tercihlerine göre kuruluyor…

-

{mesaj}

+

{mesaj}

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