feat(ui): Arc bileşenleri, landing bölümleri ve bildirim yığını

Sonner/vaul yerine kendi bildirim yığını ve alt sayfa; çip grubu, kart
destesi, kaydırma eylemi, yazı dönüşümü, bölme tutamacı; landing'e program
duvarı, süzülen haplar ve veri akışı; yaylar tek dosyada (lib/hareket.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 17:12:53 +03:00
parent bc920f6111
commit 3c995402c6
38 changed files with 5474 additions and 697 deletions

View File

@@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from "react";
import { ArrowLeft, ArrowRight, Search, X } from "lucide-react";
import { useReducedMotion } from "motion/react";
import useMeasure from "react-use-measure";
import { toast } from "sonner";
import { toast } from "@/components/ui/bildirim";
import { cities } from "turkey-map-react/lib/data";
import { TransitionPanel } from "@/components/core/transition-panel";
import { Button } from "@/components/ui/button";
@@ -23,47 +23,8 @@ import { trBaslikDuzeni } from "@/lib/slug";
import type { SihirbazFacetleri } from "@/types/yokatlas";
import { ONCELIKLER, UNIVERSITE_TIPI_ETIKET, type SihirbazSecimleri, type UniversiteTipi } from "@/features/sihirbaz/sihirbaz-sabitler";
import { OZEL_DURUMLAR, OZEL_DURUM_ETIKET, type OzelDurum } from "@/lib/ozel-durum";
export function Cip({
secili,
onClick,
children,
kucuk = false,
ton = "notr",
className = "",
}: {
secili: boolean;
onClick: () => void;
children: React.ReactNode;
kucuk?: boolean;
/**
* bilgi: özel kontenjan durumu çipi (DESIGN.md §4.21) — pasif hâli bilgi
* mavisi tonunda (§2 `primary` rolü), seçili hâli nötr çiple aynı.
*/
ton?: "notr" | "bilgi";
className?: string;
}) {
// Şişko ve sade (DESIGN.md §1.1): çip 44 px dokunma hedefi; il
// çipleri (kucuk) çok sayıda olduğu için 40 px. Hover §2.1 tek turuncu.
const boyut = kucuk ? "h-10 px-3.5 text-sm" : "h-11 px-4 text-sm";
const temel = `inline-flex cursor-pointer items-center rounded-full border font-semibold transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97] ${boyut} ${className}`;
const pasif =
ton === "bilgi"
? "border-primary/30 bg-primary/10 text-primary hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500"
: "border-slate-200 bg-card text-slate-700 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500";
return (
<button
type="button"
onClick={onClick}
aria-pressed={secili}
className={
secili ? `${temel} border-primary bg-primary text-white` : `${temel} ${pasif}`
}
>
{children}
</button>
);
}
// Seçim çipi tek kaynaktan (DESIGN.md §4.24): seçilince tik büyür, yazı kayar.
import { CipGrubu, SecimCipi as Cip } from "@/components/ui/cip-grubu";
const ADIM_ADLARI = ["Alan", "Şehir", "Öncelik"] as const;
@@ -337,7 +298,7 @@ export function SihirbazAdimlar({
<p className="mt-1 text-sm text-slate-500">
Sıralamanla ulaşabileceğin alanlar. En az bir tane seç.
</p>
<div className="mt-4 flex flex-wrap gap-2">
<CipGrubu etiket="İlgi alanları" className="mt-4">
{facetler.kategoriler.map((k) => (
<Cip
key={k.ad}
@@ -348,7 +309,7 @@ export function SihirbazAdimlar({
<span className="ml-1.5 text-xs font-medium opacity-70">{k.adet}</span>
</Cip>
))}
</div>
</CipGrubu>
</div>
<div ref={panelRef}>
<h3 className="font-heading text-xl font-bold text-slate-900">
@@ -387,8 +348,9 @@ export function SihirbazAdimlar({
görünüm ilk 10 il; kalanı "+N il daha" rozetiyle açılır. Arama
yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı
devre dışı bırakır. */}
<div
className={`mt-3 flex max-h-64 flex-wrap gap-2 overflow-y-auto sm:max-h-none sm:overflow-visible ${
<CipGrubu
etiket="Şehirler"
className={`mt-3 max-h-64 overflow-y-auto sm:max-h-none sm:overflow-visible ${
facetYukleniyor ? "animate-pulse opacity-60" : ""
}`}
>
@@ -424,7 +386,7 @@ export function SihirbazAdimlar({
Daha az göster
</button>
) : null}
</div>
</CipGrubu>
{ilAramaNorm &&
gorunenTurkiyeIlleri.length === 0 &&
@@ -439,8 +401,9 @@ export function SihirbazAdimlar({
<p className="mb-2 text-sm font-semibold text-slate-700">
KKTC & Yurt Dışı
</p>
<div
className={`flex flex-wrap gap-2 ${
<CipGrubu
etiket="KKTC ve yurt dışı"
className={`${
facetYukleniyor ? "animate-pulse opacity-60" : ""
}`}
>
@@ -455,7 +418,7 @@ export function SihirbazAdimlar({
<span className="ml-1.5 text-xs font-medium opacity-70">{i.adet}</span>
</Cip>
))}
</div>
</CipGrubu>
</div>
) : null}
@@ -500,8 +463,9 @@ export function SihirbazAdimlar({
<p className="mb-2 text-sm font-semibold text-slate-700">
Üniversite tipi
</p>
<div
className={`flex flex-wrap gap-2 ${
<CipGrubu
etiket="Üniversite tipi"
className={`${
facetYukleniyor ? "animate-pulse opacity-60" : ""
}`}
>
@@ -525,7 +489,7 @@ export function SihirbazAdimlar({
>
Fark etmez
</Cip>
</div>
</CipGrubu>
</div>
<div>
<p className="mb-2 text-sm font-semibold text-slate-700">
@@ -533,7 +497,7 @@ export function SihirbazAdimlar({
</p>
{/* Telefonda iki sütun, uzun ad iki satıra sarar: tek sütunda
yedi çip 44 px'le pencereyi 812 px'in dışına itiyordu. */}
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
<CipGrubu etiket="Senin için önemli olan" className="grid grid-cols-2 sm:flex sm:flex-wrap">
{ONCELIKLER.map((o) => (
<Cip
key={o}
@@ -544,7 +508,7 @@ export function SihirbazAdimlar({
{o}
</Cip>
))}
</div>
</CipGrubu>
<p className="mt-2 text-sm text-slate-500">
Listeyi daraltmaz; programların sırasını ve gerekçelerini
yönlendirir (istihdam ve kazanç TÜİK 2025, araştırma sırası URAP,
@@ -559,7 +523,7 @@ export function SihirbazAdimlar({
Özel kontenjan durumun{" "}
<span className="font-normal text-slate-500">(isteğe bağlı)</span>
</p>
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
<CipGrubu etiket="Özel kontenjan durumun" className="grid grid-cols-2 sm:flex sm:flex-wrap">
{OZEL_DURUMLAR.map((d) => (
<Cip
key={d}
@@ -577,7 +541,7 @@ export function SihirbazAdimlar({
{OZEL_DURUM_ETIKET[d]}
</Cip>
))}
</div>
</CipGrubu>
<p className="mt-2 text-sm text-slate-500">
ÖSYM bu durumlar için ayrı kontenjan ayırır. İşaretlersen o
kontenjanı olan programlar listende öne çıkar ve danışman bunu