From 992a01abf1121702823a4307f4536b073017f532 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Mon, 21 Sep 2026 23:34:52 +0300 Subject: [PATCH] =?UTF-8?q?feat(katalog):=20ba=C5=9Fl=C4=B1=C4=9F=C4=B1n?= =?UTF-8?q?=20alt=C4=B1nda=20kompakt=20s=C4=B1ra=20giri=C5=9F=20sat=C4=B1r?= =?UTF-8?q?=C4=B1=20(alttaki=20kart=20yerinde)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Katalog 464 oturum → 12 sıra girişi; kart tablonun altında kalıyordu. CtaSiraForm'a varyant="kompakt" + konum prop'u; olay adları aynı, konum: ust|alt özelliği eklendi; alan kimlikleri useId ile (aynı sayfada iki form). Co-Authored-By: Claude Fable 5.1 --- .../katalog/components/bolum-icerik.tsx | 12 +- .../katalog/components/universite-icerik.tsx | 51 ++- .../sihirbaz/components/cta-sira-form.tsx | 378 +++++++++++------- 3 files changed, 286 insertions(+), 155 deletions(-) diff --git a/src/features/katalog/components/bolum-icerik.tsx b/src/features/katalog/components/bolum-icerik.tsx index 5c4cbd1..a49608b 100644 --- a/src/features/katalog/components/bolum-icerik.tsx +++ b/src/features/katalog/components/bolum-icerik.tsx @@ -174,6 +174,9 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { const ilkSayfa = sayfa === 1; const aralik = siraAraligiMetni(sayfaDilim); const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`; + // Üst satır ve alt kart aynı soruyu sorar; sayfalama sayfalarında da bölüm + // adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor). + const ctaBaslik = `Sıralaman ${bolum.ad} tabanlarının neresinde?`; return ( <> @@ -258,6 +261,12 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { )} + {/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi + başlık bloğunun hemen altında, listenin üstünde. Alttaki kart kalır. */} +
+ +
+

diff --git a/src/features/katalog/components/universite-icerik.tsx b/src/features/katalog/components/universite-icerik.tsx index 90da09d..3769ae6 100644 --- a/src/features/katalog/components/universite-icerik.tsx +++ b/src/features/katalog/components/universite-icerik.tsx @@ -169,6 +169,17 @@ export function UniversiteIcerik({ ? `/universite/${slug}` : `/universite/${slug}/sayfa/${sayfa}`; const yurtdisi = uniUlkeKonum(uni.ad, uni.il); + // Üst satır ve alt kart aynı soruyu sorar; sayfalama sayfalarında da + // üniversite adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor). + const ctaBaslik = `Sıralaman ${uni.ad} tabanlarının neresinde?`; + // Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi + // özet paragrafının hemen altında, tablonun (ve ilk sayfadaki istatistik + // ızgarasının) üstünde durur. Alttaki kart yerinde kalır. + const ustSiraSatiri = ( +
+ +
+ ); return ( <> @@ -276,6 +287,8 @@ export function UniversiteIcerik({

+ {ustSiraSatiri} +
@@ -319,22 +332,25 @@ export function UniversiteIcerik({
) : ( -

- {uni.ad} taban puanları tablosunun {sayfa}. sayfası ( - {sayi(tumProgramlar.length)} programın{" "} - {sayi((sayfa - 1) * SAYFA_BOYU + 1)}– - {sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}. - satırları, fakülte sırasıyla). - {birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite - özeti ve istatistikler{" "} - - ilk sayfada - - . -

+ <> +

+ {uni.ad} taban puanları tablosunun {sayfa}. sayfası ( + {sayi(tumProgramlar.length)} programın{" "} + {sayi((sayfa - 1) * SAYFA_BOYU + 1)}– + {sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}. + satırları, fakülte sırasıyla). + {birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite + özeti ve istatistikler{" "} + + ilk sayfada + + . +

+ {ustSiraSatiri} + )} @@ -363,7 +379,8 @@ export function UniversiteIcerik({
diff --git a/src/features/sihirbaz/components/cta-sira-form.tsx b/src/features/sihirbaz/components/cta-sira-form.tsx index 738e5ce..8f69a7c 100644 --- a/src/features/sihirbaz/components/cta-sira-form.tsx +++ b/src/features/sihirbaz/components/cta-sira-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useId, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { @@ -28,6 +28,7 @@ import { } from "./sihirbaz-adimlar-lazy"; import { authClient } from "@/lib/auth-client"; import { olay, siraKovasi } from "@/lib/analitik"; +import { cn } from "@/lib/utils"; import { onbellekliJsonGetir } from "@/lib/istek-cache"; import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili"; import type { SihirbazFacetleri } from "@/types/yokatlas"; @@ -65,16 +66,34 @@ const KANITLAR = [ * profilsiz karttaki paragrafı değiştirir — katalog sayfaları kartı üstündeki * tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar * yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler. + * + * `varyant="kompakt"`: katalog sayfalarında başlığın hemen altına, tablonun + * ÜSTÜNE konan tek satırlık sürüm. Sıralama alanı çekmecede değil doğrudan + * sayfadadır (tek sayfada çıkan ziyaretçi alttaki karta hiç inmiyor); gönderince + * aynı çekmece sihirbaz adımıyla açılır. Aynı sayfada alttaki kartla birlikte + * durduğu için alan kimlikleri `useId` ile üretilir, analitik olayları da + * `konum` ("ust" | "alt") özelliğiyle ayrışır — olay adları değişmez. */ export function CtaSiraForm({ baslik, aciklama, + varyant = "kart", + konum = "alt", }: { baslik?: string; aciklama?: string; + varyant?: "kart" | "kompakt"; + /** Yalnız ölçüm içindir: formun sayfadaki yeri (tablonun üstü / altı). */ + konum?: "ust" | "alt"; }) { const profil = useTercihProfili(); const router = useRouter(); + const kompakt = varyant === "kompakt"; + // Aynı sayfada iki form (üst satır + alt kart) olabildiği için sabit id yok. + const kimlik = useId(); + const alanId = `${kimlik}-siralama`; + const hataId = `${kimlik}-siralama-hata`; + const baslikId = `${kimlik}-baslik`; const [acik, setAcik] = useState(false); const [adim, setAdim] = useState<"sira" | "sihirbaz">("sira"); @@ -87,8 +106,14 @@ export function CtaSiraForm({ const [girisli, setGirisli] = useState(false); // Huni girişi ölçümü: ref yalnızca profilsiz daldaki karta bağlanır; kayıtlı // profilli ziyaretçi formu hiç görmediğinden sayılmaz. Sayfa ayrımı için - // özellik gerekmez — Rybbit olayın pathname'ini zaten yazar. - const formRef = useGorunumOlayi("cta_form_goruntulendi"); + // özellik gerekmez — Rybbit olayın pathname'ini zaten yazar. `konum` hem + // özellik hem tekilleştirme ekidir: üstteki satır olayı atınca alttaki kartın + // gösterimi yutulmasın (oturum başına konum başına en fazla 1). + const formRef = useGorunumOlayi( + "cta_form_goruntulendi", + { konum }, + konum, + ); // Yazarken binlik ayracıyla biçimle (85000 → 85.000) function siralamaDegisti(ham: string) { @@ -118,13 +143,17 @@ export function CtaSiraForm({ setSira(value); setFacetler(veri.facetler); setAdim("sihirbaz"); + // Kompakt satırda sıralama sayfada girilir; çekmece ancak şimdi, + // doğrudan sihirbaz adımıyla açılır (kartta zaten açıktır). + setAcik(true); olay("sira_girildi", { kaynak: "cta", + konum, tur, sira_kovasi: siraKovasi(value), }); } catch { - olay("sira_form_hata", { kaynak: "cta" }); + olay("sira_form_hata", { kaynak: "cta", konum }); setHata("Bir şeyler ters gitti, tekrar dener misin?"); } finally { setYukleniyor(false); @@ -146,13 +175,219 @@ export function CtaSiraForm({ function acikDegisti(v: boolean) { setAcik(v); if (!v) { + setHata(null); + // Kompakt satırda çekmecenin sıra adımı yoktur; her gönderim adımı ve + // facet'leri zaten baştan yazar (kapanış animasyonunda içerik boşalmasın). + if (kompakt) return; // Yeniden açılışta sıfırdan başla; yazılan sıralama kaybolmasın. setAdim("sira"); setFacetler(null); - setHata(null); } } + // Sıra girişi: kartta çekmecenin ilk adımı, kompakt satırda sayfanın kendisi. + // Kompakt sürüm masaüstünde (lg) tek satırdır: çipler · alan · buton. + const siraFormu = ( +
+
+ {PUAN_TURLERI_SECENEK.map((t) => ( + + ))} +
+ + +
+
+ + siralamaDegisti(e.target.value)} + onFocus={preloadSihirbazAdimlar} + aria-invalid={hata ? true : undefined} + aria-describedby={hata ? hataId : undefined} + className="h-12 bg-background pl-11 text-base" + /> +
+ +
+ {hata ? ( + + ) : null} +
+ ); + + // Çekmece iki varyantta da aynıdır; kompakt satırda sıra adımı sayfada + // olduğundan çekmece yalnız sihirbazı taşır. + const cekmece = ( + + + {/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */} + + Sıralamana yakın programlar hangileri? + + + YKS başarı sıralamanı gir; sıralamana uygun programları bu sayfadan + ayrılmadan gör. + + + + Kapat + + +
+ {!kompakt && adim === "sira" ? ( +
+
+ Saniyede gör +

+ Sıralamana yakın programlar hangileri? +

+

+ Puan türünü seç, başarı sıralamanı yaz; hayal, dengeli ve + güvenli dilimlerindeki programları bu sayfadan ayrılmadan + gör. Ücretsiz, üyelik gerekmez. +

+
+ + {siraFormu} + +
    + {KANITLAR.map(({ ikon: Ikon, etiket }) => ( +
  • + + {etiket} +
  • + ))} +
+
+ ) : facetler && sira != null ? ( + + ) : null} +
+
+
+ ); + + if (kompakt) { + // CLS: kayıtlı profil hydration SONRASI bulunur. Satır tablonun üstünde + // olduğundan yüksekliği o anda değişmemeli — form yerinde (görünmez ve + // inert) kalır, profilli kısayol aynı ızgara hücresine bindirilir; hücrenin + // yüksekliğini her iki durumda da form belirler. + return ( +
+

+ + {baslik ?? "Sıralaman bu tabanların neresinde?"} + {" "} + Ücretsiz, üyelik gerekmez. +

+
+
+ {siraFormu} +
+ {profil ? ( +
+

+ Sıralaman kayıtlı ({profil.sira.toLocaleString("tr-TR")}. sıra). +

+ + Sıralamana uygun programları gör + + +
+ ) : null} +
+ {cekmece} +
+ ); + } + if (profil) { return (
@@ -198,138 +433,7 @@ export function CtaSiraForm({ - - - {/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */} - - Sıralamana yakın programlar hangileri? - - - YKS başarı sıralamanı gir; sıralamana uygun programları bu sayfadan - ayrılmadan gör. - - - - Kapat - - -
- {adim === "sira" ? ( -
-
- Saniyede gör -

- Sıralamana yakın programlar hangileri? -

-

- Puan türünü seç, başarı sıralamanı yaz; hayal, dengeli ve - güvenli dilimlerindeki programları bu sayfadan ayrılmadan - gör. Ücretsiz, üyelik gerekmez. -

-
- -
-
- {PUAN_TURLERI_SECENEK.map((t) => ( - - ))} -
- - -
-
- - siralamaDegisti(e.target.value)} - onFocus={preloadSihirbazAdimlar} - aria-invalid={hata ? true : undefined} - aria-describedby={ - hata ? "cta-siralama-hata" : undefined - } - className="h-12 bg-background pl-11 text-base" - /> -
- -
- {hata ? ( - - ) : null} -
- -
    - {KANITLAR.map(({ ikon: Ikon, etiket }) => ( -
  • - - {etiket} -
  • - ))} -
-
- ) : facetler && sira != null ? ( - - ) : null} -
-
-
+ {cekmece}
); }