feat(tasarim): funnel CTA'ları arama kartı dilinde, 24'lük piksel liste kreatifi
- PikselCtaYuzeyi: koyu slate-950 yüzey, yazılıp silinen satırlar, tek turuncu CTA (arama penceresindeki kartın dili); ana sayfa kapanışı ve /sonuc'taki iki panelde köşeye kümelenen PixelField'in yerine - PikselListe: 24 kare = 24 tercih; kendini kurar (5 hayal · 13 dengeli · 6 güvenli), imlece tepki verir, CTA'ya gelince tamamı dolar, dokunmada nabız; hareketi azalt'ta durağan; renk hep metinle - Liste çekmecesi: ilerleme çubuğunun yerinde öğrencinin kendi listesi pikselde (her kare o tercihin riski); başlık yan yana dizildi, liste alanı 150 → 234 px (375 px ölçümü) - Funnel metinleri src/lib/funnel-metinleri.ts'e taşındı Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
77
src/components/piksel-cta-yuzeyi.tsx
Normal file
77
src/components/piksel-cta-yuzeyi.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TypingAnimation } from "@/components/ui/typing-animation";
|
||||
import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri";
|
||||
import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste";
|
||||
|
||||
/** Listeyi `ofset` kadar döndürür: aynı sayfadaki yüzeyler farklı satırla
|
||||
* başlar. Rastgele değil, deterministik — sunucu ve istemci aynı çizer. */
|
||||
function dondur<T>(dizi: readonly T[], ofset: number): T[] {
|
||||
const n = ofset % dizi.length;
|
||||
return [...dizi.slice(n), ...dizi.slice(0, n)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Funnel CTA yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili:
|
||||
* koyu slate-950 zemin, ince mavi kenar, yazılıp silinen kısa satırlar,
|
||||
* turuncu CTA. Pikseller bir köşede kümelenmez; yüzeyin kreatifi 24'lük
|
||||
* listenin kendisidir (PikselListe). Kartın içindeki bir düğmeye ya da
|
||||
* linke imleç/odak gelince 24 kare birden dolar — "listen böyle hazır olur".
|
||||
*/
|
||||
export function PikselCtaYuzeyi({
|
||||
children,
|
||||
className,
|
||||
metinOfseti = 0,
|
||||
kayanMetin = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Aynı sayfadaki iki yüzey aynı satırla başlamasın */
|
||||
metinOfseti?: number;
|
||||
kayanMetin?: boolean;
|
||||
}) {
|
||||
const [tamamla, setTamamla] = useState(false);
|
||||
const [metinler] = useState(() => dondur(FUNNEL_METINLERI, metinOfseti));
|
||||
|
||||
const hedefMi = (t: EventTarget | null) =>
|
||||
t instanceof Element && t.closest("a, button") != null;
|
||||
|
||||
return (
|
||||
<section
|
||||
onPointerOver={(e) => setTamamla(hedefMi(e.target))}
|
||||
onPointerLeave={() => setTamamla(false)}
|
||||
onFocusCapture={(e) => setTamamla(hedefMi(e.target))}
|
||||
onBlurCapture={() => setTamamla(false)}
|
||||
className={cn(
|
||||
"relative isolate overflow-hidden rounded-3xl border border-blue-400/20 bg-slate-950 text-white shadow-xl shadow-slate-900/10",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="grid items-center gap-8 px-5 py-10 sm:px-10 sm:py-12 lg:grid-cols-[minmax(0,1fr)_16rem] lg:gap-12">
|
||||
<div className="flex min-w-0 flex-col items-center text-center lg:items-start lg:text-left">
|
||||
{children}
|
||||
{kayanMetin ? (
|
||||
<TypingAnimation
|
||||
as="p"
|
||||
words={metinler}
|
||||
loop
|
||||
typeSpeed={38}
|
||||
deleteSpeed={18}
|
||||
pauseDelay={2600}
|
||||
className="mt-5 min-h-[2.75em] font-heading text-base font-bold leading-snug tracking-[-0.02em] text-slate-300 sm:min-h-[1.4em]"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="order-first flex flex-col items-center lg:order-none">
|
||||
<PikselListe
|
||||
tamamla={tamamla}
|
||||
className="w-full max-w-[9rem] sm:max-w-[11rem] lg:max-w-[14rem]"
|
||||
/>
|
||||
<PikselListeAciklama className="mt-4 justify-center" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user