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:
bilalgursen
2026-09-24 02:12:45 +03:00
parent 96b1516e35
commit 71e6389131
9 changed files with 451 additions and 164 deletions

View File

@@ -589,3 +589,21 @@ Araştırma ve kaynaklar: `docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md`
Seri (streak), kayıp korkusu, puan/XP, rozet koleksiyonu, liderlik tablosu, maskot, sahte
aciliyet/kıtlık, utandırarak onay. Tercih günlük alışkanlık değil, tek seferlik kaygılı bir
karar.
### 10.8 Funnel CTA yüzeyi ve 24'lük piksel liste
Bilal'in isteği (25 Eyl 2026): "CTA alanlarını arama içindeki alandan ilham, piksel detaylı yap;
pikseller bir kümede kümeleşmesin, interaktif kreatif olsun."
- **Yüzey — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx`:** arama penceresindeki
kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950 text-white`, altta yazılıp
silinen satırlar (`FUNNEL_METINLERI`, `src/lib/funnel-metinleri.ts`), tek turuncu CTA.
Kullanıldığı yerler: ana sayfa kapanış CTA'sı, `/sonuc`'taki iki sihirbaz paneli. Eski
`bg-primary` + köşeye kümelenmiş `PixelField` bu yüzeylerde kullanılmaz.
- **Kreatif — `PikselListe`, `src/components/piksel-liste.tsx`:** 24 kare = 24 tercih, hero'nun
nokta ölçüsüyle (hücre 10, nokta 7.4, rx 2). Örnek modda liste kendini kurar (5 hayal · 13
dengeli · 6 güvenli, 110 ms arayla), yalnız ekrandayken. İmleç yakın kareleri büyütür; kart
içindeki düğmeye imleç/odak gelince 24 kare birden dolar; dokunmada basılan kare ve komşuları
nabız atar (`kt-piksel-nabiz`). Hareketi azalt: dolu, durağan. Renk her zaman
`PikselListeAciklama` metniyle.
- **Gerçek liste:** çekmecede ilerleme çubuğunun yerinde öğrencinin kendi listesi (`gercek`,
12×2): her kare o tercihin riskiyle, riski bilinmeyen beyaz; metin karşılığı yanındaki risk
dağılımında.

View File

@@ -303,3 +303,21 @@
mask-image: none;
}
}
/* 24'lük piksel listede dokunma nabzı (bkz. components/piksel-liste.tsx):
basılan kare ve komşuları kısa bir büyüyüp söner. Tek seferlik, 420 ms. */
@keyframes kt-piksel-nabiz {
0% {
transform: scale(1);
}
35% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}
.kt-piksel-nabiz {
animation: kt-piksel-nabiz 0.42s cubic-bezier(0.23, 1, 0.32, 1);
}

View File

@@ -25,9 +25,9 @@ import { TurkeyPixelMap } from "@/features/pazarlama/components/turkey-pixel-map
import {
PixelCorner,
PixelDivider,
PixelField,
SectionEyebrow,
} from "@/components/pixel-decor";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
import {
@@ -681,28 +681,14 @@ export default function Home() {
<section className="px-4 pb-24 pt-6 sm:px-6">
<Parallax strength={12} className="mx-auto max-w-6xl">
<Reveal>
<div className="relative isolate overflow-hidden rounded-3xl bg-primary px-4 py-20 text-white shadow-xl shadow-primary/20 sm:py-24">
{/* Hero'nun piksel dokusu koyu zeminde: ortası boş, kenarlara doğru yoğun */}
{/* Piksel zemin panel içinde geride kalır; taşma payı için dikeyde büyütülür */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
>
<Parallax strength={-22} className="h-full">
<PixelField seed={8} />
</Parallax>
</div>
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
<MessageCircleQuestion
className="size-10 opacity-80"
aria-hidden
/>
<h2 className="mt-6 font-heading text-3xl font-bold sm:text-4xl">
Tercih listen, pişmanlık listesi olmasın
</h2>
<KapanisCta />
</div>
</div>
{/* Funnel CTA yüzeyi (DESIGN.md §10.8): köşede kümelenen
piksel zemini yerine 24'lük listenin interaktif kreatifi */}
<PikselCtaYuzeyi metinOfseti={7}>
<h2 className="font-heading text-3xl font-bold text-balance sm:text-4xl">
Tercih listen, pişmanlık listesi olmasın
</h2>
<KapanisCta />
</PikselCtaYuzeyi>
</Reveal>
</Parallax>
</section>

View File

@@ -9,6 +9,7 @@ import {
ListChecks,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri";
import { TypingAnimation } from "@/components/ui/typing-animation";
import { SIHIRBAZ_AC_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
import {
@@ -16,39 +17,6 @@ import {
useTercihProfili,
} from "@/features/liste/hooks/use-tercih-profili";
const FUNNEL_METINLERI = [
"Sıralamana en uygun listeyi birlikte kuralım.",
"Geleceğin 24 satırlık bir plana sığar.",
"Gerçek YÖK Atlas verisiyle güvenle seç.",
"Her tercihinin bir nedeni olsun.",
"Riskleri biz hesaplayalım, kararı sen ver.",
"Sana uygun bölümler bir adım uzağında.",
"Dengeli bir liste, rahat bir tercih dönemi.",
"Tercihlerini şansa bırakma.",
"Doğru strateji, doğru yerleşme.",
"Hayalindeki bölüm için doğru plan.",
"Verilerle konuşan bir tercih listesi.",
"Üç adımda sana özel 24 tercih.",
"Tercih dönemini stressiz geçir.",
"Geçen yılın verisi, senin avantajın.",
"Güvenli ve iddialı tercihleri dengele.",
"Sıralaman ne söylüyor? Birlikte bakalım.",
"Her satırı senin için hesaplıyoruz.",
"Akıllı tercih, içi rahat bir bekleyiş.",
"Yerleşme riskini görerek seç.",
"Sana özel liste birkaç dakikada hazır.",
"İlgi alanların ve şehrin, tek listede.",
"Boş kalan tercih, kaçan fırsattır.",
"Uzman işi bir liste, senin onayınla.",
"Kararını veriye dayandır.",
"Doğru tercih, doğru bilgiyle başlar.",
"Listeni bugün kur, için rahat olsun.",
"Yerleşme riskin renklerle önünde.",
"En önemli 24 kararını birlikte verelim.",
"Senin sıralaman, sana özel plan.",
"İyi bir liste, iyi bir başlangıçtır.",
];
/** Fisher-Yates ile kopya üzerinde karıştırır; her açılışta farklı sıra. */
function karistir<T>(dizi: readonly T[]): T[] {
const kopya = [...dizi];

View 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>
);
}

View File

@@ -0,0 +1,191 @@
"use client";
import {
useCallback,
useEffect,
useRef,
useState,
type PointerEvent as ReactPointerEvent,
type ReactNode,
} from "react";
import { useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { DILIM_ETIKET } from "@/lib/risk";
/**
* 24'lük tercih listesinin piksel hâli — CTA yüzeylerinin interaktif
* kreatifi (Bilal'in isteği, 25 Eyl 2026: "pikseller bir köşede
* kümelenmesin, interaktif kreatif olsun"). Her kare bir tercih; hero'daki
* dot-matrix ölçüsüyle (hücre 10, nokta 7.4, rx 2).
*
* Davranış (DESIGN.md §10.8):
* - Örnek modda liste kendini kurar: kareler sırayla dolar (5 hayal ·
* 13 dengeli · 6 güvenli), bir süre durur, sakin söner, yeniden başlar.
* Yalnız ekrandayken çalışır.
* - İmleç ızgaranın üstündeyken yakındaki kareler hafifçe büyür.
* - `tamamla` true ise (ör. CTA düğmesine gelince) 24 kare birden dolar.
* - Dokunmada kareye basınca o kare ve komşuları kısa bir nabız atar.
* - Hareketi azalt: dolu ve durağan ızgara.
* - `gercek` verilirse döngü yok; kareler öğrencinin kendi listesidir
* (her kare o tercihin riskiyle).
* Renk tek başına anlam taşımaz: çağıran `PikselListeAciklama` ile metni
* yanına koyar.
*/
export type PikselKare = "hayal" | "dengeli" | "guvenli" | "belirsiz" | "bos";
const SUTUN = 6;
const SATIR = 4;
const TOPLAM = SUTUN * SATIR;
const CELL = 10;
const DOT = 7.4;
const INSET = (CELL - DOT) / 2;
const ORNEK: PikselKare[] = Array.from({ length: TOPLAM }, (_, i) =>
i < 5 ? "hayal" : i < 18 ? "dengeli" : "guvenli",
);
const RENK: Record<PikselKare, string> = {
hayal: "fill-red-500/85",
dengeli: "fill-amber-400/85",
guvenli: "fill-emerald-500/85",
belirsiz: "fill-white/55",
bos: "fill-white/15",
};
const ADIM_MS = 110;
const BEKLE_MS = 2600;
export function PikselListe({
tamamla = false,
gercek,
sutun = SUTUN,
className,
}: {
tamamla?: boolean;
/** Öğrencinin gerçek listesi (sırasıyla); verilirse örnek döngü kapanır */
gercek?: PikselKare[];
sutun?: number;
className?: string;
}) {
const hareketAzalt = useReducedMotion();
const satir = Math.ceil(TOPLAM / sutun);
const svgRef = useRef<SVGSVGElement>(null);
const [dolu, setDolu] = useState(hareketAzalt || gercek ? TOPLAM : 0);
const [gorunur, setGorunur] = useState(false);
const [imlec, setImlec] = useState<{ x: number; y: number } | null>(null);
const [nabiz, setNabiz] = useState<{ i: number; t: number } | null>(null);
// Yalnız ekrandayken çalış (IntersectionObserver, DESIGN.md §6)
useEffect(() => {
const el = svgRef.current;
if (!el) return;
const io = new IntersectionObserver(([e]) => setGorunur(e.isIntersecting), {
rootMargin: "0px 0px -10% 0px",
});
io.observe(el);
return () => io.disconnect();
}, []);
// Örnek döngü: 0 → 24 sırayla, bekle, sıfırla
useEffect(() => {
if (gercek || hareketAzalt || !gorunur || tamamla) return;
const id = window.setTimeout(
() => setDolu((d) => (d >= TOPLAM ? 0 : d + 1)),
dolu >= TOPLAM ? BEKLE_MS : dolu === 0 ? 500 : ADIM_MS,
);
return () => window.clearTimeout(id);
}, [dolu, gorunur, gercek, hareketAzalt, tamamla]);
const kareler: PikselKare[] = gercek
? Array.from({ length: TOPLAM }, (_, i) => gercek[i] ?? "bos")
: ORNEK.map((k, i) => (tamamla || i < dolu ? k : "bos"));
const imlecGuncelle = useCallback((e: ReactPointerEvent<SVGSVGElement>) => {
if (e.pointerType !== "mouse") return;
const svg = svgRef.current;
if (!svg) return;
const r = svg.getBoundingClientRect();
setImlec({
x: ((e.clientX - r.left) / r.width) * sutun * CELL,
y: ((e.clientY - r.top) / r.height) * satir * CELL,
});
}, [sutun, satir]);
return (
<svg
ref={svgRef}
viewBox={`0 0 ${sutun * CELL} ${satir * CELL}`}
aria-hidden="true"
focusable="false"
onPointerMove={imlecGuncelle}
onPointerLeave={() => setImlec(null)}
className={cn("h-auto w-full touch-manipulation", className)}
>
{kareler.map((k, i) => {
const x = (i % sutun) * CELL;
const y = Math.floor(i / sutun) * CELL;
const cx = x + CELL / 2;
const cy = y + CELL / 2;
// İmlece yakın kare büyür (en çok %18), uzaklaştıkça etkisi söner
let olcek = 1;
if (imlec && !hareketAzalt) {
const d = Math.hypot(imlec.x - cx, imlec.y - cy);
olcek = 1 + Math.max(0, 1 - d / 16) * 0.18;
}
const nabizda =
nabiz &&
Math.abs((nabiz.i % sutun) - (i % sutun)) <= 1 &&
Math.abs(Math.floor(nabiz.i / sutun) - Math.floor(i / sutun)) <= 1;
return (
<rect
key={nabizda ? `${i}-${nabiz.t}` : i}
x={x + INSET}
y={y + INSET}
width={DOT}
height={DOT}
rx={2}
onPointerDown={(e) => {
if (e.pointerType === "mouse" || hareketAzalt) return;
setNabiz({ i, t: Date.now() });
}}
className={cn(
RENK[k],
"origin-center transition-[fill,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] [transform-box:fill-box] motion-reduce:transition-none",
nabizda && "kt-piksel-nabiz",
)}
style={{ transform: olcek !== 1 ? `scale(${olcek})` : undefined }}
/>
);
})}
</svg>
);
}
/** Renklerin metin karşılığı (§2.2: renk tek başına anlam taşımaz). */
export function PikselListeAciklama({
className,
sayilar = { hayal: 5, dengeli: 13, guvenli: 6 },
}: {
className?: string;
sayilar?: { hayal: number; dengeli: number; guvenli: number };
}) {
const oge = (renk: string, sayi: number, etiket: string): ReactNode => (
<span className="inline-flex items-center gap-1.5">
<span className={cn("size-2 shrink-0 rounded-[2px]", renk)} aria-hidden />
{sayi} {etiket.toLocaleLowerCase("tr-TR")}
</span>
);
return (
<p
className={cn(
"flex flex-wrap items-center gap-x-3 gap-y-1 text-sm font-medium text-slate-300",
className,
)}
>
{oge("bg-red-500", sayilar.hayal, DILIM_ETIKET.hayal)}
{oge("bg-amber-400", sayilar.dengeli, DILIM_ETIKET.dengeli)}
{oge("bg-emerald-500", sayilar.guvenli, DILIM_ETIKET.garanti)}
</p>
);
}

View File

@@ -15,7 +15,7 @@ import { useState } from "react";
import { toast } from "sonner";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { PixelField } from "@/components/pixel-decor";
import { PikselListe } from "@/components/piksel-liste";
import { UniLogo } from "@/components/uni-logo";
import { RiskEtiketi } from "@/components/risk-etiketi";
import { Button } from "@/components/ui/button";
@@ -122,7 +122,6 @@ export function ListeCekmecesi() {
// drawer içinde erken ölçüm satırları kaydırıyordu
const [oturdu, setOturdu] = useState(false);
const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100);
// Risk dağılımı: riskliden güvenliye, sıfır olanlar yazılmaz
const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const)
.map((r) => [r, liste.filter((t) => t.risk === r).length] as const)
@@ -185,55 +184,66 @@ export function ListeCekmecesi() {
<DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
Kendi Listem
</DrawerTitle>
{/* Durum ekran okuyucuya da duyurulur (WCAG 4.1.3) */}
<p role="status" className="mt-1 px-10 text-center text-sm text-slate-500">
{liste.length === MANUEL_LISTE_MAX ? (
// Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık,
// güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık.
<span className="inline-flex items-center gap-1.5 font-semibold text-primary transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] starting:scale-95 starting:opacity-0 motion-reduce:starting:scale-100">
<Check className="size-4" aria-hidden />
Listen tamam · {liste.length} / {MANUEL_LISTE_MAX}
</span>
) : (
<>
{liste.length} / {MANUEL_LISTE_MAX} tercih
</>
)}
</p>
<div
className="mx-auto mt-3 h-2 w-full max-w-xs overflow-hidden rounded-full bg-slate-100"
role="progressbar"
aria-valuenow={liste.length}
aria-valuemin={0}
aria-valuemax={MANUEL_LISTE_MAX}
aria-label="Liste doluluğu"
>
<motion.div
className="h-full rounded-full bg-primary"
animate={{ width: `${doluluk}%` }}
transition={{ type: "spring", duration: 0.5, bounce: 0 }}
/>
</div>
{/* Listenin dengesi metinle (renk tek başına değil, §2.2) */}
{riskSayilari.length > 0 ? (
<p className="mt-2.5 flex flex-wrap items-center justify-center gap-x-3 gap-y-1">
{riskSayilari.map(([seviye, adet]) => (
<span key={seviye} className="inline-flex items-center gap-1.5">
<RiskEtiketi risk={seviye} />
<span className="text-sm font-semibold text-slate-900">
{adet}
{/* Durum satırı: solda listenin piksel hâli (ilerleme çubuğunun
yerine), sağda sayı ve risk dağılımı — dikine uzamasın diye
yan yana (DESIGN.md §5.3). Durum ekran okuyucuya duyurulur. */}
<div className="mt-3 flex items-center justify-center gap-3">
<div
className="w-40 shrink-0 rounded-xl bg-slate-950 px-2.5 py-2"
role="progressbar"
aria-valuenow={liste.length}
aria-valuemin={0}
aria-valuemax={MANUEL_LISTE_MAX}
aria-label="Liste doluluğu"
>
<PikselListe
gercek={liste.map((t) =>
t.risk === "riskli"
? "hayal"
: t.risk === "az-riskli"
? "dengeli"
: t.risk === "guvenli"
? "guvenli"
: "belirsiz",
)}
sutun={12}
/>
</div>
<div className="min-w-0 text-left">
<p role="status" className="text-sm text-slate-500">
{liste.length === MANUEL_LISTE_MAX ? (
// Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık,
// güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık.
<span className="inline-flex items-center gap-1.5 font-semibold text-primary transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] starting:scale-95 starting:opacity-0 motion-reduce:starting:scale-100">
<Check className="size-4" aria-hidden />
Listen tamam · {liste.length} / {MANUEL_LISTE_MAX}
</span>
</span>
))}
</p>
) : null}
) : (
<>
{liste.length} / {MANUEL_LISTE_MAX} tercih
</>
)}
</p>
{riskSayilari.length > 0 ? (
<p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
{riskSayilari.map(([seviye, adet]) => (
<span key={seviye} className="inline-flex items-center gap-1.5">
<RiskEtiketi risk={seviye} />
<span className="text-sm font-semibold text-slate-900">
{adet}
</span>
</span>
))}
</p>
) : null}
</div>
</div>
{/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli
programlar). Yalnız riskleri bilinen ve en az 6 tercihli
listede; vaat değil, gerekçe. */}
{guvenliYok ? (
<p className="mx-auto mt-2 max-w-sm text-center text-sm leading-relaxed text-slate-600">
Güvenli program yok; alta birkaç tane eklemek açıkta kalma riskini
azaltır.
Güvenli yok; alta birkaç tane eklemek riski azaltır.
</p>
) : null}
</DrawerHeader>
@@ -325,18 +335,14 @@ export function ListeCekmecesi() {
{/* Telefonda alt alta (başlık tek satır, buton tam genişlik):
yan yana dizince başlık üç satıra kırılıp listenin yerini
yiyordu (24 Eyl 2026, 375 px ölçümü). */}
<div className="relative isolate flex flex-col gap-3 overflow-hidden rounded-2xl bg-primary px-5 py-4 text-left text-white sm:flex-row sm:gap-4 sm:py-5 sm:items-center sm:justify-between">
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white"
>
<PixelField seed={8} />
</div>
<div className="min-w-0">
{/* Funnel yüzeyi (DESIGN.md §10.8): arama penceresindeki kartın
koyu zemini; listenin piksel hâli başlıkta. */}
<div className="flex flex-col gap-3 rounded-2xl border border-blue-400/20 bg-slate-950 px-5 py-4 text-left text-white sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:py-5">
<div className="min-w-0 sm:order-1 sm:flex-1">
<p className="font-heading text-xl font-bold sm:text-2xl">
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
</p>
<p className="mt-1 hidden truncate text-sm leading-relaxed text-white sm:block">
<p className="mt-1 hidden truncate text-sm leading-relaxed text-slate-300 sm:block">
{riskEksik
? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)."
: "Her seçimin yerleşme riski hazır — son kararı sen ver."}
@@ -345,7 +351,7 @@ export function ListeCekmecesi() {
{riskEksik ? (
<Button
size="lg"
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:order-3 sm:w-auto"
onClick={() => {
cekmeceKapat();
// Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam
@@ -361,7 +367,7 @@ export function ListeCekmecesi() {
<Button
asChild
size="lg"
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:order-3 sm:w-auto"
>
<Link href="/listem" onClick={cekmeceKapat}>
Listemde aç

View File

@@ -7,7 +7,7 @@ import { ArrowRight } from "lucide-react";
import { animate } from "motion/mini";
import { useReducedMotion } from "motion/react";
import { Button } from "@/components/ui/button";
import { PixelField } from "@/components/pixel-decor";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
import type { SihirbazFacetleri } from "@/types/yokatlas";
@@ -279,70 +279,57 @@ export function SihirbazBolumu({
</Button>
</section>
) : girisBekliyor ? (
<section
ref={hazirPanelRef}
className="relative isolate mt-16 min-w-0 overflow-hidden rounded-2xl bg-primary px-5 py-8 text-center text-white sm:p-12"
>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
>
<PixelField seed={13} />
</div>
<h2 className="relative font-heading text-2xl font-bold sm:text-3xl">
Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
</h2>
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3
kredi, kalan 2 kredinle danışmana soru sorarsın).
</p>
<Button
asChild
size="lg"
className="relative mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto sm:px-8"
>
<Link
href={girisUrl}
onClick={() =>
olay("giris_cta_tiklandi", { kaynak: "hazir_panel" })
}
<div ref={hazirPanelRef} className="mt-16 min-w-0">
<PikselCtaYuzeyi metinOfseti={3}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
</h2>
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-300">
Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 kredi,
kalan 2 kredinle danışmana soru sorarsın).
</p>
<Button
asChild
size="lg"
className="mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto sm:px-8"
>
5 deneme kredinle Yapay Zeka listeni kur — giriş yap
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
<p className="relative mt-3 text-xs text-white/70">
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
</p>
</section>
<Link
href={girisUrl}
onClick={() =>
olay("giris_cta_tiklandi", { kaynak: "hazir_panel" })
}
>
5 deneme kredinle Yapay Zeka listeni kur — giriş yap
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
<p className="mt-3 text-sm text-slate-400">
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
</p>
</PikselCtaYuzeyi>
</div>
) : (
<section className="relative isolate mt-16 min-w-0 overflow-hidden rounded-2xl bg-primary px-5 py-8 text-center text-white sm:p-12">
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
>
<PixelField seed={19} />
</div>
<h2 className="relative font-heading text-2xl font-bold sm:text-3xl">
<PikselCtaYuzeyi className="mt-16 min-w-0" metinOfseti={11}>
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
24 tercihi tek tek elemek yerine, yapay zekâ kursun
</h2>
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-300">
Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi
alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli
ve güvenli dilimlerine dağıtılmış 24 tercihi — her satırı
yerleşme riski ve gerekçesiyle — senin yerine dizer.
alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve
güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme
riski ve gerekçesiyle — senin yerine dizer.
</p>
<Button
size="lg"
onClick={sihirbaziBaslat}
className="relative mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto sm:px-8"
className="mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto sm:px-8"
>
Yapay Zeka listemi kur
<ArrowRight className="size-4" aria-hidden />
</Button>
</section>
</PikselCtaYuzeyi>
)}
{monteEdildi ? (

View File

@@ -0,0 +1,36 @@
/**
* CTA yüzeylerinde yazılıp silinen kısa satırlar (arama penceresindeki
* kart ve PikselCtaYuzeyi). "use client" yok: sunucu da okuyabilsin (§9.3).
*/
export const FUNNEL_METINLERI: readonly string[] = [
"Sıralamana en uygun listeyi birlikte kuralım.",
"Geleceğin 24 satırlık bir plana sığar.",
"Gerçek YÖK Atlas verisiyle güvenle seç.",
"Her tercihinin bir nedeni olsun.",
"Riskleri biz hesaplayalım, kararı sen ver.",
"Sana uygun bölümler bir adım uzağında.",
"Dengeli bir liste, rahat bir tercih dönemi.",
"Tercihlerini şansa bırakma.",
"Doğru strateji, doğru yerleşme.",
"Hayalindeki bölüm için doğru plan.",
"Verilerle konuşan bir tercih listesi.",
"Üç adımda sana özel 24 tercih.",
"Tercih dönemini stressiz geçir.",
"Geçen yılın verisi, senin avantajın.",
"Güvenli ve iddialı tercihleri dengele.",
"Sıralaman ne söylüyor? Birlikte bakalım.",
"Her satırı senin için hesaplıyoruz.",
"Akıllı tercih, içi rahat bir bekleyiş.",
"Yerleşme riskini görerek seç.",
"Sana özel liste birkaç dakikada hazır.",
"İlgi alanların ve şehrin, tek listede.",
"Boş kalan tercih, kaçan fırsattır.",
"Uzman işi bir liste, senin onayınla.",
"Kararını veriye dayandır.",
"Doğru tercih, doğru bilgiyle başlar.",
"Listeni bugün kur, için rahat olsun.",
"Yerleşme riskin renklerle önünde.",
"En önemli 24 kararını birlikte verelim.",
"Senin sıralaman, sana özel plan.",
"İyi bir liste, iyi bir başlangıçtır.",
];