refactor: update site header and chat components for improved user experience
All checks were successful
Deploy / deploy (push) Successful in 12m16s
All checks were successful
Deploy / deploy (push) Successful in 12m16s
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.
This commit is contained in:
193
src/components/animasyonlu-mark.tsx
Normal file
193
src/components/animasyonlu-mark.tsx
Normal file
@@ -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<SVGSVGElement>(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 (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox="0 0 180 180"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className={cn("shrink-0", className)}
|
||||
>
|
||||
{KARELER.map((k, i) => {
|
||||
const [px, py] = POZISYONLAR[k.baslangic];
|
||||
const duruk = { x: px, y: py, scale: 1, opacity: 1 };
|
||||
|
||||
const animate = !aktif
|
||||
? duruk
|
||||
: varyant === "rota"
|
||||
? {
|
||||
x: rota(k.baslangic, 0),
|
||||
y: rota(k.baslangic, 1),
|
||||
scale: ROTA_NABIZ_SCALE,
|
||||
opacity: ROTA_NABIZ_OPACITY,
|
||||
}
|
||||
: {
|
||||
x: px,
|
||||
y: py,
|
||||
scale: [1, 0.82, 1],
|
||||
opacity: [1, 0.55, 1],
|
||||
};
|
||||
|
||||
// Kademeli kalkış: delay yalnızca ilk turdan önce uygulanır, sonraki
|
||||
// turlarda faz farkı kendiliğinden korunur.
|
||||
const rotaGecikme = i * ROTA_GECIKME_ADIMI;
|
||||
const konumGecis = {
|
||||
duration: ROTA_SURE,
|
||||
times: KONUM_ZAMANLARI,
|
||||
ease: "easeInOut" as const,
|
||||
repeat: Infinity,
|
||||
delay: rotaGecikme,
|
||||
};
|
||||
const rotaNabizGecis = {
|
||||
duration: ROTA_SURE,
|
||||
times: ROTA_NABIZ_ZAMANLARI,
|
||||
ease: "easeInOut" as const,
|
||||
repeat: Infinity,
|
||||
delay: rotaGecikme,
|
||||
};
|
||||
const nabizGecis = {
|
||||
duration: NABIZ_SURE,
|
||||
times: [0, 0.5, 1],
|
||||
ease: SINE as unknown as [number, number, number, number],
|
||||
repeat: Infinity,
|
||||
delay: NABIZ_GECIKMELER[i],
|
||||
};
|
||||
|
||||
const transition = !aktif
|
||||
? { duration: 0.4, ease: "easeOut" as const }
|
||||
: varyant === "rota"
|
||||
? {
|
||||
x: konumGecis,
|
||||
y: konumGecis,
|
||||
scale: rotaNabizGecis,
|
||||
opacity: rotaNabizGecis,
|
||||
}
|
||||
: {
|
||||
x: { duration: 0 },
|
||||
y: { duration: 0 },
|
||||
scale: nabizGecis,
|
||||
opacity: nabizGecis,
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.rect
|
||||
key={k.renk}
|
||||
width={k.boyut}
|
||||
height={k.boyut}
|
||||
rx={13}
|
||||
fill={k.renk}
|
||||
className={renk === "turuncu" ? "fill-orange-500" : undefined}
|
||||
animate={animate}
|
||||
transition={transition}
|
||||
style={
|
||||
{
|
||||
transformBox: "fill-box",
|
||||
transformOrigin: "center",
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
<Image
|
||||
src="/kolay-tercih-mark.svg"
|
||||
alt=""
|
||||
width={44}
|
||||
height={44}
|
||||
aria-hidden="true"
|
||||
className="size-8 shrink-0 sm:size-11"
|
||||
/>
|
||||
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
|
||||
kareler danışman panelindeki nabız animasyonuyla canlanır. */}
|
||||
<AnimasyonluMark sadeceHover className="size-8 sm:size-11" />
|
||||
<span className="truncate">
|
||||
<span className="text-lg font-light tracking-tighter sm:text-3xl">
|
||||
Kolay
|
||||
|
||||
Reference in New Issue
Block a user