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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user