Refactor UI components for consistency and clarity across the application
All checks were successful
Deploy / deploy (push) Successful in 18m31s
All checks were successful
Deploy / deploy (push) Successful in 18m31s
- Updated AGENTS.md with new guidelines for UI consistency, including badge and modal styles. - Refactored layout.tsx to improve the placement of the progressive blur effect. - Enhanced NotFound component text for better user guidance. - Replaced HeroFocusButton with HeroBaslik and KapanisCta in the Home component for improved clarity. - Added SiteFooter to multiple pages for consistent footer display. - Removed ListemSekmeleri component and adjusted ListemPage to streamline functionality. - Updated various components to ensure consistent styling and user experience.
This commit is contained in:
@@ -1,28 +1,25 @@
|
||||
"use client";
|
||||
|
||||
// /meraklisina'nın canlı anlatım katmanı: kullanıcı kendi sıralamasını girer,
|
||||
// dilim şeridi ve kademeli filtre hunisi o sıralamanın GERÇEK sayılarıyla
|
||||
// güncellenir. Provider sayfadaki metin bölümlerini (RSC children) sarar;
|
||||
// görseller context'ten okur, böylece tek girişle iki diyagram birden yaşar.
|
||||
// /meraklisina'nın canlı anlatım katmanı: kullanıcının kayıtlı tercih
|
||||
// profilindeki sıralama + puan türü okunur, dilim şeridi o sıralamanın GERÇEK
|
||||
// sayılarıyla güncellenir. Profil yoksa sayfa SSR'daki örnek sıralamayla kalır.
|
||||
// Provider sayfadaki metin bölümlerini (RSC children) sarar; görseller
|
||||
// context'ten okur. (Sihirbaz hunisi statik bir şemayla anlatılır — bkz.
|
||||
// huni-semasi.tsx.)
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { useTercihProfili } from "@/components/manuel-liste/profil-kapisi-store";
|
||||
import { PUAN_TURU_ETIKET, sonucHref } from "@/lib/sihirbaz";
|
||||
import type { DilimKey, SihirbazFacetleri } from "@/lib/db";
|
||||
|
||||
const TUR_SECENEKLERI = [
|
||||
{ deger: "say", etiket: "Sayısal" },
|
||||
{ deger: "ea", etiket: "Eşit Ağırlık" },
|
||||
{ deger: "soz", etiket: "Sözel" },
|
||||
{ deger: "dil", etiket: "Dil" },
|
||||
{ deger: "tyt", etiket: "TYT" },
|
||||
] as const;
|
||||
|
||||
export type DemoVeri = {
|
||||
facetler: SihirbazFacetleri;
|
||||
dilimToplam: Record<DilimKey, number>;
|
||||
@@ -33,18 +30,7 @@ type DemoDurum = {
|
||||
tur: string;
|
||||
veri: DemoVeri;
|
||||
yukleniyor: boolean;
|
||||
// Huni seçimleri ve kademeli facet cevapları
|
||||
kategori: string | null;
|
||||
il: string | null;
|
||||
tip: "devlet" | "vakif" | null;
|
||||
kategoriFacet: SihirbazFacetleri | null; // ?kategori= cevabı (iller buradan)
|
||||
ilFacet: SihirbazFacetleri | null; // ?kategori&il cevabı (tipler buradan)
|
||||
kategoriSec: (k: string | null) => void;
|
||||
ilSec: (i: string | null) => void;
|
||||
tipSec: (t: "devlet" | "vakif" | null) => void;
|
||||
siraDegistir: (ham: string) => void;
|
||||
turDegistir: (t: string) => void;
|
||||
siraMetni: string;
|
||||
profilVar: boolean;
|
||||
};
|
||||
|
||||
const DemoContext = createContext<DemoDurum | null>(null);
|
||||
@@ -58,13 +44,9 @@ function useDemo(): DemoDurum {
|
||||
async function facetGetir(
|
||||
sira: number,
|
||||
tur: string,
|
||||
secim: { kategori?: string; il?: string; dilimler?: boolean },
|
||||
signal: AbortSignal,
|
||||
): Promise<{ facetler: SihirbazFacetleri; dilimToplam?: Record<DilimKey, number> } | null> {
|
||||
const params = new URLSearchParams({ sira: String(sira), tur });
|
||||
if (secim.kategori) params.append("kategori", secim.kategori);
|
||||
if (secim.il) params.append("il", secim.il);
|
||||
if (secim.dilimler) params.set("dilimler", "1");
|
||||
const params = new URLSearchParams({ sira: String(sira), tur, dilimler: "1" });
|
||||
try {
|
||||
const res = await fetch(`/api/facetler?${params}`, { signal });
|
||||
if (!res.ok) return null;
|
||||
@@ -86,36 +68,19 @@ export function SiralamaDemo({
|
||||
baslangicVeri: DemoVeri;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [siraMetni, setSiraMetni] = useState(
|
||||
baslangicSira.toLocaleString("tr-TR"),
|
||||
);
|
||||
const [sira, setSira] = useState(baslangicSira);
|
||||
const [tur, setTur] = useState("say");
|
||||
const profil = useTercihProfili();
|
||||
const sira = profil?.sira ?? baslangicSira;
|
||||
const tur = profil?.tur ?? "say";
|
||||
const [veri, setVeri] = useState(baslangicVeri);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const [kategori, setKategori] = useState<string | null>(null);
|
||||
const [il, setIl] = useState<string | null>(null);
|
||||
const [tip, setTip] = useState<"devlet" | "vakif" | null>(null);
|
||||
const [kategoriFacet, setKategoriFacet] = useState<SihirbazFacetleri | null>(
|
||||
null,
|
||||
);
|
||||
const [ilFacet, setIlFacet] = useState<SihirbazFacetleri | null>(null);
|
||||
const zamanlayici = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Sıralama/tür değişti → ana veriyi tazele, huni seçimlerini sıfırla
|
||||
// Profil yüklendi ve SSR örneğinden farklı → ana veriyi tazele
|
||||
useEffect(() => {
|
||||
if (sira === baslangicSira && tur === "say" && veri === baslangicVeri) {
|
||||
return; // ilk render: sunucudan gelen veri zaten güncel
|
||||
}
|
||||
if (sira === baslangicSira && tur === "say") return;
|
||||
const ctrl = new AbortController();
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- fetch başlangıç işareti
|
||||
setYukleniyor(true);
|
||||
setKategori(null);
|
||||
setIl(null);
|
||||
setTip(null);
|
||||
setKategoriFacet(null);
|
||||
setIlFacet(null);
|
||||
facetGetir(sira, tur, { dilimler: true }, ctrl.signal)
|
||||
facetGetir(sira, tur, ctrl.signal)
|
||||
.then((cevap) => {
|
||||
if (!cevap?.dilimToplam) return;
|
||||
setVeri({ facetler: cevap.facetler, dilimToplam: cevap.dilimToplam });
|
||||
@@ -125,126 +90,51 @@ export function SiralamaDemo({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sira, tur]);
|
||||
|
||||
// Kategori seçildi → o kategorinin illerini getir
|
||||
useEffect(() => {
|
||||
if (!kategori) return;
|
||||
const ctrl = new AbortController();
|
||||
facetGetir(sira, tur, { kategori }, ctrl.signal).then((cevap) => {
|
||||
if (cevap) setKategoriFacet(cevap.facetler);
|
||||
});
|
||||
return () => ctrl.abort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kategori]);
|
||||
|
||||
// İl seçildi → kategori+il ile tipleri getir
|
||||
useEffect(() => {
|
||||
if (!kategori || !il) return;
|
||||
const ctrl = new AbortController();
|
||||
facetGetir(sira, tur, { kategori, il }, ctrl.signal).then((cevap) => {
|
||||
if (cevap) setIlFacet(cevap.facetler);
|
||||
});
|
||||
return () => ctrl.abort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kategori, il]);
|
||||
|
||||
function siraDegistir(ham: string) {
|
||||
const rakamlar = ham.replace(/\D/g, "").slice(0, 7);
|
||||
setSiraMetni(rakamlar ? Number(rakamlar).toLocaleString("tr-TR") : "");
|
||||
if (zamanlayici.current) clearTimeout(zamanlayici.current);
|
||||
const deger = Number(rakamlar);
|
||||
if (!deger || deger < 1 || deger > 4_000_000) return;
|
||||
zamanlayici.current = setTimeout(() => setSira(deger), 450);
|
||||
}
|
||||
|
||||
const durum: DemoDurum = {
|
||||
sira,
|
||||
tur,
|
||||
veri,
|
||||
yukleniyor,
|
||||
kategori,
|
||||
il,
|
||||
tip,
|
||||
kategoriFacet,
|
||||
ilFacet,
|
||||
kategoriSec: (k) => {
|
||||
setKategori(k);
|
||||
setIl(null);
|
||||
setTip(null);
|
||||
setKategoriFacet(null);
|
||||
setIlFacet(null);
|
||||
},
|
||||
ilSec: (i) => {
|
||||
setIl(i);
|
||||
setTip(null);
|
||||
setIlFacet(null);
|
||||
},
|
||||
tipSec: setTip,
|
||||
siraDegistir,
|
||||
turDegistir: setTur,
|
||||
siraMetni,
|
||||
profilVar: profil !== null,
|
||||
};
|
||||
|
||||
return <DemoContext.Provider value={durum}>{children}</DemoContext.Provider>;
|
||||
}
|
||||
|
||||
/** Sıralama girişi + puan türü — iki diyagramı birden süren tek kumanda. */
|
||||
/** Görsellerin hangi sıralamayla hesaplandığını söyleyen bilgi kartı. */
|
||||
export function DemoKumanda() {
|
||||
const { siraMetni, siraDegistir, tur, turDegistir, sira, yukleniyor } =
|
||||
useDemo();
|
||||
const { sira, tur, yukleniyor, profilVar } = useDemo();
|
||||
const turEtiket = PUAN_TURU_ETIKET[tur] ?? tur;
|
||||
return (
|
||||
<div className="rounded-2xl border border-primary/20 bg-primary/5 p-5">
|
||||
<label
|
||||
htmlFor="demo-sira"
|
||||
className="font-heading text-sm font-bold text-slate-900"
|
||||
>
|
||||
Kendi sıralamanı gir, aşağıdaki görseller sana göre değişsin
|
||||
</label>
|
||||
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<input
|
||||
id="demo-sira"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
value={siraMetni}
|
||||
onChange={(e) => siraDegistir(e.target.value)}
|
||||
placeholder="ör. 85.000"
|
||||
className="h-11 w-full rounded-xl border border-slate-200 bg-white px-4 text-base font-medium tabular-nums outline-none transition-colors focus:border-primary sm:w-44"
|
||||
/>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className="flex flex-wrap gap-1.5"
|
||||
>
|
||||
{TUR_SECENEKLERI.map((t) => (
|
||||
<button
|
||||
key={t.deger}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={tur === t.deger}
|
||||
onClick={() => turDegistir(t.deger)}
|
||||
className={
|
||||
tur === t.deger
|
||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white"
|
||||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300"
|
||||
}
|
||||
>
|
||||
{t.etiket}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-heading text-sm font-bold text-slate-900">
|
||||
{profilVar
|
||||
? "Görseller senin sıralamana göre hesaplandı"
|
||||
: "Görseller örnek bir sıralamayla gösteriliyor"}
|
||||
</p>
|
||||
<p
|
||||
className={`mt-2 text-xs text-slate-500 ${yukleniyor ? "animate-pulse" : ""}`}
|
||||
className={`mt-2 text-sm text-slate-600 ${yukleniyor ? "animate-pulse" : ""}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{yukleniyor
|
||||
? "Hesaplanıyor…"
|
||||
: `Aşağıdaki tüm sayılar ${sira.toLocaleString("tr-TR")}. sıradaki bir aday için gerçek YÖK Atlas verisidir.`}
|
||||
{yukleniyor ? (
|
||||
"Hesaplanıyor…"
|
||||
) : (
|
||||
<>
|
||||
<span className="font-semibold tabular-nums text-slate-900">
|
||||
{sira.toLocaleString("tr-TR")}.
|
||||
</span>{" "}
|
||||
sıra · {turEtiket} — aşağıdaki tüm sayılar bu aday için gerçek YÖK
|
||||
Atlas verisidir.
|
||||
{!profilVar &&
|
||||
" Ana sayfadan kendi sıralamanı girersen bu sayfa sana göre güncellenir."}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Sıralama ekseni: girilen sıraya göre dilim sınırları ve gerçek adetler. */
|
||||
/** Sıralama ekseni: adayın sırasına göre dilim sınırları ve gerçek adetler. */
|
||||
export function DilimSeridiCanli() {
|
||||
const { sira, veri, yukleniyor } = useDemo();
|
||||
const t = veri.dilimToplam;
|
||||
@@ -334,307 +224,28 @@ export function DilimSeridiCanli() {
|
||||
);
|
||||
}
|
||||
|
||||
function HuniCubugu({
|
||||
etiket,
|
||||
adet,
|
||||
oran,
|
||||
}: {
|
||||
etiket: string;
|
||||
adet: number;
|
||||
oran: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="flex h-9 min-w-36 items-center justify-between gap-2 rounded-lg bg-primary/85 px-3 transition-[width] duration-300"
|
||||
style={{ width: `${Math.min(Math.max(oran * 100, 36), 100)}%` }}
|
||||
>
|
||||
<span className="truncate text-xs font-medium text-white">{etiket}</span>
|
||||
<span className="shrink-0 text-xs font-bold tabular-nums text-white">
|
||||
{adet.toLocaleString("tr-TR")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Sayfanın kapanış kartı: profili olmayan ziyaretçiyi ana sayfadaki sıralama
|
||||
* formuna, sıralamasını zaten girmiş adayı ise doğrudan sonuç sayfasına
|
||||
* gönderir — "sıralamanı gir" CTA'sı profil varken gösterilmez.
|
||||
*/
|
||||
export function DemoKapanisCta() {
|
||||
const profil = useTercihProfili();
|
||||
|
||||
function SecimCipleri<T extends { anahtar: string; etiket: string; adet: number }>({
|
||||
secenekler,
|
||||
secili,
|
||||
onSec,
|
||||
}: {
|
||||
secenekler: T[];
|
||||
secili: string | null;
|
||||
onSec: (anahtar: string | null) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{secenekler.map((s) => (
|
||||
<button
|
||||
key={s.anahtar}
|
||||
type="button"
|
||||
aria-pressed={secili === s.anahtar}
|
||||
onClick={() => onSec(secili === s.anahtar ? null : s.anahtar)}
|
||||
className={
|
||||
secili === s.anahtar
|
||||
? "cursor-pointer rounded-full border border-orange-500 bg-orange-500 px-3 py-1.5 text-xs font-medium text-white"
|
||||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300"
|
||||
}
|
||||
>
|
||||
{s.etiket}
|
||||
<span className="ml-1 text-[10px] opacity-70 tabular-nums">
|
||||
{s.adet.toLocaleString("tr-TR")}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// AI listesinin dilim iskeleti: üstte hayal, ortada dengeli, sonda garanti.
|
||||
// Ürünün risk renkleriyle boyanır; sayılar prompt'taki kuralın orta değeri.
|
||||
const LISTE_ISKELETI: { dilim: string; adet: number; renk: string }[] = [
|
||||
{ dilim: "hayal", adet: 5, renk: "bg-red-400" },
|
||||
{ dilim: "dengeli", adet: 13, renk: "bg-amber-400" },
|
||||
{ dilim: "garanti", adet: 6, renk: "bg-emerald-500" },
|
||||
];
|
||||
|
||||
/** Huninin finali: kalan havuzun yapay zekâya gidişi ve 24'lük listenin
|
||||
iskeleti. Havuz 24'ün altına düşerse gevşetme kuralını canlı gösterir. */
|
||||
function YapayZekaAsamasi({ havuz }: { havuz: number }) {
|
||||
const dar = havuz < 24;
|
||||
return (
|
||||
<div className="border-t border-slate-100 pt-4">
|
||||
<p className="text-xs font-semibold text-slate-500">
|
||||
Sonra sıra yapay zekâda
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-6 text-center">
|
||||
<p className="font-heading text-base font-bold text-slate-900">
|
||||
{profil
|
||||
? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı."
|
||||
: "Kuralları öğrendin — şimdi kendi sıralamanla dene."}
|
||||
</p>
|
||||
<p className="mt-1.5 text-xs leading-5 text-slate-600">
|
||||
Seçimlerinden geriye kalan{" "}
|
||||
<span className="font-bold tabular-nums text-slate-900">
|
||||
{havuz.toLocaleString("tr-TR")} programlık havuz
|
||||
</span>{" "}
|
||||
yapay zekâya gider; o da bu havuzdan tam 24 tercih seçer ve şu iskelete
|
||||
oturtur:
|
||||
</p>
|
||||
|
||||
{/* 24 hücrelik liste şeridi — renk + metin birlikte (renk tek başına
|
||||
anlam taşımaz) */}
|
||||
<div
|
||||
role="img"
|
||||
aria-label="24 tercihlik listenin yapısı: en üstte yaklaşık 5 hayal, ortada 13 dengeli, sonda 6 garanti tercih."
|
||||
className="mt-3"
|
||||
<Link
|
||||
href={profil ? sonucHref(profil) : "/"}
|
||||
className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{LISTE_ISKELETI.flatMap((b) =>
|
||||
Array.from({ length: b.adet }, (_, i) => (
|
||||
<span
|
||||
key={`${b.dilim}-${i}`}
|
||||
className={`h-6 min-w-0 flex-1 rounded ${b.renk}`}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1.5 flex justify-between text-[11px] text-slate-500">
|
||||
<span>
|
||||
üstte 4–6 <span className="font-semibold text-red-600">hayal</span>
|
||||
</span>
|
||||
<span>
|
||||
ortada 12–14{" "}
|
||||
<span className="font-semibold text-amber-600">dengeli</span>
|
||||
</span>
|
||||
<span>
|
||||
sonda 5–7{" "}
|
||||
<span className="font-semibold text-emerald-600">garanti</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="mt-3 space-y-1 text-xs leading-5 text-slate-600">
|
||||
<li>
|
||||
· Havuzda olmayan bir bölümü yazamaz — listedeki her satırın kimliği
|
||||
havuzla karşılaştırılır, uymayan liste yeniden ürettirilir.
|
||||
</li>
|
||||
<li>
|
||||
· Her satıra tek cümlelik gerekçe ve bir risk notu ekler; sıraya neyi
|
||||
neden koyduğu görünür.
|
||||
</li>
|
||||
<li>
|
||||
· Önceliklerin (iş garantisi, maaş, prestij…) hangi programın öne
|
||||
geçeceğini belirler.
|
||||
</li>
|
||||
<li>
|
||||
· Havuz 24'ün altına düşerse filtreler adım adım gevşetilir —
|
||||
önce il, sonra alan — ve bu, listende açıkça belirtilir.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{dar ? (
|
||||
<p className="mt-3 rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
|
||||
Bu seçimle havuz 24'ün altına düştü. Gerçek sihirbaz bu durumda
|
||||
filtreleri adım adım gevşetir — önce il, sonra alan tercihi esnetilir
|
||||
— ve bunu listende açıkça belirtir; liste her zaman 24'e
|
||||
tamamlanır.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Kademeli filtre hunisi: gerçek sihirbaz mantığının oynanabilir kopyası. */
|
||||
export function HuniCanli() {
|
||||
const {
|
||||
veri,
|
||||
yukleniyor,
|
||||
kategori,
|
||||
il,
|
||||
tip,
|
||||
kategoriFacet,
|
||||
ilFacet,
|
||||
kategoriSec,
|
||||
ilSec,
|
||||
tipSec,
|
||||
} = useDemo();
|
||||
|
||||
const toplam = veri.facetler.toplam;
|
||||
const kategoriAdet =
|
||||
kategori != null
|
||||
? (veri.facetler.kategoriler.find((k) => k.ad === kategori)?.adet ?? 0)
|
||||
: null;
|
||||
const ilAdet =
|
||||
il != null && kategoriFacet
|
||||
? (kategoriFacet.iller.find((i) => i.il === il)?.adet ?? 0)
|
||||
: null;
|
||||
const tipAdet =
|
||||
tip != null && ilFacet
|
||||
? (ilFacet.uniturler.find((u) => u.grup === tip)?.adet ?? 0)
|
||||
: null;
|
||||
|
||||
const kategoriSecenekleri = veri.facetler.kategoriler.slice(0, 6).map((k) => ({
|
||||
anahtar: k.ad,
|
||||
etiket: k.ad,
|
||||
adet: k.adet,
|
||||
}));
|
||||
const ilSecenekleri = (kategoriFacet?.iller ?? []).slice(0, 6).map((i) => ({
|
||||
anahtar: i.il,
|
||||
etiket: i.il.toLocaleLowerCase("tr-TR"),
|
||||
adet: i.adet,
|
||||
}));
|
||||
const tipSecenekleri = (ilFacet?.uniturler ?? []).map((u) => ({
|
||||
anahtar: u.grup,
|
||||
etiket: u.grup === "devlet" ? "Devlet" : "Vakıf",
|
||||
adet: u.adet,
|
||||
}));
|
||||
|
||||
// Yapay zekâya giden havuz: huninin en derin (en son seçilmiş) katmanı
|
||||
const havuz = tipAdet ?? ilAdet ?? kategoriAdet ?? toplam;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-5 ${
|
||||
yukleniyor ? "animate-pulse opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Huni çubukları — her seçim bir çubuk daha ekler */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 shrink-0" />
|
||||
<HuniCubugu
|
||||
etiket="Sıralamanla açılan tüm programlar"
|
||||
adet={toplam}
|
||||
oran={1}
|
||||
/>
|
||||
</div>
|
||||
{kategori != null && kategoriAdet != null ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
|
||||
1.
|
||||
</span>
|
||||
<HuniCubugu
|
||||
etiket={`“${kategori}” seçince`}
|
||||
adet={kategoriAdet}
|
||||
oran={toplam > 0 ? kategoriAdet / toplam : 0}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{il != null && ilAdet != null ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
|
||||
2.
|
||||
</span>
|
||||
<HuniCubugu
|
||||
etiket={`+ “${il.toLocaleLowerCase("tr-TR")}” seçince`}
|
||||
adet={ilAdet}
|
||||
oran={toplam > 0 ? ilAdet / toplam : 0}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{tip != null && tipAdet != null ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
|
||||
3.
|
||||
</span>
|
||||
<HuniCubugu
|
||||
etiket={`+ “${tip === "devlet" ? "Devlet" : "Vakıf"}” seçince`}
|
||||
adet={tipAdet}
|
||||
oran={toplam > 0 ? tipAdet / toplam : 0}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Seçim adımları — sihirbazın küçük, oynanabilir hali */}
|
||||
<div className="space-y-3 border-t border-slate-100 pt-4">
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs font-semibold text-slate-500">
|
||||
1. Bir alan seç
|
||||
</p>
|
||||
<SecimCipleri
|
||||
secenekler={kategoriSecenekleri}
|
||||
secili={kategori}
|
||||
onSec={kategoriSec}
|
||||
/>
|
||||
</div>
|
||||
{kategori != null ? (
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs font-semibold text-slate-500">
|
||||
2. Bir il seç{" "}
|
||||
<span className="font-normal text-slate-400">
|
||||
(iller artık yalnızca “{kategori}” programlarını sayıyor)
|
||||
</span>
|
||||
</p>
|
||||
{ilSecenekleri.length > 0 ? (
|
||||
<SecimCipleri
|
||||
secenekler={ilSecenekleri}
|
||||
secili={il}
|
||||
onSec={ilSec}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400">Yükleniyor…</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{kategori != null && il != null ? (
|
||||
<div>
|
||||
<p className="mb-1.5 text-xs font-semibold text-slate-500">
|
||||
3. Üniversite tipi seç{" "}
|
||||
<span className="font-normal text-slate-400">
|
||||
(sayılar hem alanı hem ili hesaba katıyor)
|
||||
</span>
|
||||
</p>
|
||||
{tipSecenekleri.length > 0 ? (
|
||||
<SecimCipleri
|
||||
secenekler={tipSecenekleri}
|
||||
secili={tip}
|
||||
onSec={(t) => tipSec(t as "devlet" | "vakif" | null)}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400">Yükleniyor…</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<YapayZekaAsamasi havuz={havuz} />
|
||||
{profil ? "Sıralamana uygun programları gör" : "Sıralamanı gir"}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
54
src/app/meraklisina/huni-semasi.tsx
Normal file
54
src/app/meraklisina/huni-semasi.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
// Sihirbaz hunisinin statik anlatımı: interaktif demo yerine akışın tamamını
|
||||
// tek bakışta gösteren bir mermaid şeması. Çizim, ürün genelindeki ortak
|
||||
// "el çizimi" bileşeniyle yapılır (bkz. components/el-cizimi-sema.tsx).
|
||||
|
||||
import { ElCizimiSema } from "@/components/el-cizimi-sema";
|
||||
|
||||
// Renkler ürünün risk dilinden (kırmızı/sarı/yeşil) ve slate paletinden;
|
||||
// renk tek başına anlam taşımaz, her düğüm metinle etiketlidir.
|
||||
const SEMA = `---
|
||||
config:
|
||||
look: handDrawn
|
||||
---
|
||||
flowchart TD
|
||||
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>yalnızca ulaşabildiğin alanlar listelenir")
|
||||
A --> B("2. adım · İl seçimi<br/>iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
||||
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
||||
C --> H("Aday havuzu<br/>elde kalan gerçek programlar")
|
||||
H --> S{"Havuzda 24'ten az program mı kaldı?"}
|
||||
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce il, sonra alan — listede açıkça belirtilir")
|
||||
G --> H
|
||||
S -- "hayır" --> Y("Yapay zekâ havuzdan tam 24 tercih seçer<br/>havuzda olmayan bölümü yazamaz, her satıra gerekçe + risk notu ekler")
|
||||
Y --> L("24 tercihlik liste")
|
||||
L --> L1("üstte 4–6 hayal")
|
||||
L --> L2("ortada 12–14 dengeli")
|
||||
L --> L3("sonda 5–7 garanti")
|
||||
|
||||
classDef havuz fill:#eff6ff,stroke:#3b82f6,color:#1e3a8a
|
||||
classDef uyari fill:#fffbeb,stroke:#f59e0b,color:#92400e
|
||||
classDef hayal fill:#fee2e2,stroke:#ef4444,color:#991b1b
|
||||
classDef dengeli fill:#fef3c7,stroke:#f59e0b,color:#92400e
|
||||
classDef garanti fill:#d1fae5,stroke:#10b981,color:#065f46
|
||||
class H,Y havuz
|
||||
class G uyari
|
||||
class L1 hayal
|
||||
class L2 dengeli
|
||||
class L3 garanti`;
|
||||
|
||||
export function HuniSemasi() {
|
||||
return (
|
||||
<ElCizimiSema
|
||||
tanim={SEMA}
|
||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuz 24'ün altına düşerse filtreler gevşetilir, sonra yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda garanti olacak şekilde 24 tercih seçer."
|
||||
figcaption={
|
||||
<>
|
||||
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
||||
<span className="font-medium text-slate-700">
|
||||
dışına asla çıkmadan
|
||||
</span>{" "}
|
||||
kurar.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,37 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { PagePixelDivider, SectionEyebrow } from "@/components/pixel-decor";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { dilimAra, rankWindowFacets, type DilimKey } from "@/lib/db";
|
||||
import {
|
||||
DemoKapanisCta,
|
||||
DemoKumanda,
|
||||
DilimSeridiCanli,
|
||||
HuniCanli,
|
||||
SiralamaDemo,
|
||||
type DemoVeri,
|
||||
} from "./demo";
|
||||
import { HuniSemasi } from "./huni-semasi";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Meraklısına: KolayTercih nasıl çalışır?",
|
||||
alternates: { canonical: "/meraklisina" },
|
||||
description:
|
||||
"Sıralamanı girdiğinde arka planda ne oluyor? Dilimler, risk renkleri ve sihirbazın adım adım daralan filtreleri — öğrenci diliyle, kendi sıralamanla deneyebileceğin canlı görsellerle.",
|
||||
"Sıralamanı girdiğinde arka planda ne oluyor? Dilimler, risk renkleri ve sihirbazın adım adım daralan filtreleri — öğrenci diliyle, sıralamana göre hesaplanan canlı görsellerle.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Algoritmanın öğrenci diline çevrilmiş anlatımı. Teknik detay (SQL, çarpan
|
||||
* mutfağı) bilerek verilmez; amaç güven: veri gerçek, kurallar şeffaf.
|
||||
* Görseller canlıdır: kullanıcı sıralamasını girer, dilim şeridi ve filtre
|
||||
* hunisi o sıralamanın gerçek sayılarıyla güncellenir (bkz. demo.tsx).
|
||||
* Dilim şeridi canlıdır: kullanıcının kayıtlı tercih profilindeki sıralama
|
||||
* okunur, şerit o sıralamanın gerçek sayılarıyla güncellenir (bkz. demo.tsx);
|
||||
* profil yoksa örnek sıralama gösterilir. Sihirbaz hunisi ise statik
|
||||
* bir mermaid şemasıyla anlatılır (bkz. huni-semasi.tsx).
|
||||
* Diyagramlar ürünün risk renk dilini kullanır (yeşil/sarı/kırmızı) ve her
|
||||
* bölge metinle de etiketlenir — renk tek başına anlam taşımaz.
|
||||
*/
|
||||
|
||||
// Demo ilk açılışta bu sıralamayla dolu gelir; SSR'da gerçek veriyle render
|
||||
// edilir ki kullanıcı girmeden de sayfa anlamlı olsun.
|
||||
// Profili olmayan ziyaretçi bu örnek sıralamayı görür; SSR'da gerçek veriyle
|
||||
// render edilir ki profil olmadan da sayfa anlamlı olsun.
|
||||
const VARSAYILAN_SIRA = 100_000;
|
||||
|
||||
export default function MeraklisinaPage() {
|
||||
@@ -45,19 +47,18 @@ export default function MeraklisinaPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||
<Parallax fromStart strength={10}>
|
||||
<p className="text-sm font-semibold tracking-wide text-primary">
|
||||
Meraklısına
|
||||
</p>
|
||||
<h1 className="mt-1 font-heading text-3xl font-bold">
|
||||
<SectionEyebrow>Meraklısına</SectionEyebrow>
|
||||
<h1 className="mt-3 font-heading text-3xl font-bold">
|
||||
Sıralamanı girince arka planda ne oluyor?
|
||||
</h1>
|
||||
<p className="mt-3 text-slate-600">
|
||||
Sihir yok, kara kutu yok: KolayTercih'in önerdiği her satır resmî
|
||||
YÖK Atlas verisinden gelir ve hangi kurala göre önüne geldiği bellidir.
|
||||
Bu sayfa o kuralları öğrenci diliyle anlatır — üstelik görseller
|
||||
canlı: kendi sıralamanı gir, sayılar sana göre değişsin.
|
||||
canlı: sıralamanı girdiysen sayılar sana göre hesaplanır.
|
||||
</p>
|
||||
</Parallax>
|
||||
|
||||
@@ -139,7 +140,7 @@ export default function MeraklisinaPage() {
|
||||
4. Sihirbaz süzer, yapay zekâ kurar
|
||||
</h2>
|
||||
<p className="mt-2">
|
||||
“Listemi oluştur” sihirbazındaki üç adım birbirinden
|
||||
“AI listemi kur” sihirbazındaki üç adım birbirinden
|
||||
bağımsız değildir. İlk adımda yalnızca{" "}
|
||||
<strong>sıralamanla ulaşabildiğin</strong>{" "}
|
||||
alanlar listelenir; ikinci adımdaki iller, seçtiğin alanlardaki
|
||||
@@ -148,11 +149,10 @@ export default function MeraklisinaPage() {
|
||||
Adımlar bitince elde kalan gerçek programlar bir{" "}
|
||||
<strong>aday havuzu</strong>{" "}
|
||||
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
||||
verilir. Aşağıda kendin dene — her seçimin havuzu nasıl
|
||||
daralttığını ve havuzun 24'lük listeye nasıl dönüştüğünü huni
|
||||
canlı gösterir.
|
||||
verilir. Aşağıdaki şema akışın tamamını gösterir — havuz
|
||||
24'ün altına düşerse filtrelerin nasıl gevşetildiği de dahil.
|
||||
</p>
|
||||
<HuniCanli />
|
||||
<HuniSemasi />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -170,20 +170,11 @@ export default function MeraklisinaPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-6 text-center">
|
||||
<p className="font-heading text-base font-bold text-slate-900">
|
||||
Kuralları öğrendin — şimdi kendi sıralamanla dene.
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
Sıralamanı gir
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
<DemoKapanisCta />
|
||||
</Parallax>
|
||||
</SiralamaDemo>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user