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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
191
src/components/piksel-liste.tsx
Normal file
191
src/components/piksel-liste.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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ç
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
36
src/lib/funnel-metinleri.ts
Normal file
36
src/lib/funnel-metinleri.ts
Normal 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.",
|
||||
];
|
||||
Reference in New Issue
Block a user