refactor: güncellenmiş profil çerezi ile sonuç sayfası yönlendirmeleri
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Profil çerezinin SSR'da kullanılmasını sağlayarak, sonuç sayfasındaki yönlendirmeleri güncelledik. Artık profil bilgileri URL parametreleri yerine çerezden alınarak işleniyor. Ayrıca, profil çerezi olmayan kullanıcılar için yönlendirme mantığı iyileştirildi. Çeşitli bileşenlerdeki URL oluşturma fonksiyonları güncellenerek, profil bilgileri çerezden okunacak şekilde düzenlendi. Bu değişiklikler, kullanıcı deneyimini artırmayı ve daha tutarlı bir veri akışı sağlamayı hedefliyor.
This commit is contained in:
@@ -15,6 +15,7 @@ import { SiteFooter } from "@/components/site-footer";
|
||||
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
||||
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
||||
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||
@@ -94,6 +95,7 @@ export function UniversiteIcerik({
|
||||
const buYol = ilkSayfa
|
||||
? `/universite/${slug}`
|
||||
: `/universite/${slug}/sayfa/${sayfa}`;
|
||||
const yurtdisi = uniUlkeKonum(uni.ad, uni.il);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -116,12 +118,16 @@ export function UniversiteIcerik({
|
||||
...(uniLogoYolu(slug)
|
||||
? { logo: `${SITE_URL}${uniLogoYolu(slug)}` }
|
||||
: {}),
|
||||
...(uni.il
|
||||
...(uni.il || yurtdisi
|
||||
? {
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: trBaslikDuzeni(uni.il),
|
||||
addressCountry: "TR",
|
||||
...(yurtdisi?.sehir
|
||||
? { addressLocality: yurtdisi.sehir }
|
||||
: uni.il
|
||||
? { addressLocality: trBaslikDuzeni(uni.il) }
|
||||
: {}),
|
||||
addressCountry: yurtdisi ? yurtdisi.iso : "TR",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
@@ -5,6 +5,15 @@ import {
|
||||
HARITA_YUKSEKLIK,
|
||||
normalizeIlAdi,
|
||||
} from "@/lib/harita";
|
||||
import {
|
||||
ULKE_HARITA_GENISLIK,
|
||||
ULKE_HARITA_YUKSEKLIK,
|
||||
uniUlkeKonum,
|
||||
type UlkeKonum,
|
||||
} from "@/lib/harita-ulkeler";
|
||||
|
||||
const SECILI_DOLGU = "oklch(0.809 0.105 251.8)";
|
||||
const ZEMIN_DOLGU = "oklch(0.929 0.013 255.5)";
|
||||
|
||||
export function UniversiteKonumHaritasi({
|
||||
universite,
|
||||
@@ -13,8 +22,55 @@ export function UniversiteKonumHaritasi({
|
||||
universite: string;
|
||||
il: string | null;
|
||||
}) {
|
||||
const yurtdisi = uniUlkeKonum(universite, il);
|
||||
if (yurtdisi) {
|
||||
return (
|
||||
<KonumBolumu universite={universite}>
|
||||
<UlkeHaritasi universite={universite} konum={yurtdisi} />
|
||||
<p className="pointer-events-none absolute bottom-0 left-0 text-[11px] font-medium text-slate-500">
|
||||
{yurtdisi.ulkeAd}
|
||||
{yurtdisi.sehir ? ` · ${yurtdisi.sehir}` : ""}
|
||||
</p>
|
||||
</KonumBolumu>
|
||||
);
|
||||
}
|
||||
|
||||
if (!il) return null;
|
||||
|
||||
return (
|
||||
<KonumBolumu universite={universite}>
|
||||
<svg
|
||||
viewBox={`0 ${HARITA_UST_KIRPMA} ${HARITA_GENISLIK} ${HARITA_YUKSEKLIK - HARITA_UST_KIRPMA}`}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
role="img"
|
||||
aria-label={`${universite} konumunun Türkiye haritasındaki görünümü`}
|
||||
>
|
||||
{cities.map((city) => {
|
||||
const secili = il ? normalizeIlAdi(city.name) === il : false;
|
||||
return (
|
||||
<path
|
||||
key={city.id}
|
||||
d={city.path}
|
||||
fill={secili ? SECILI_DOLGU : ZEMIN_DOLGU}
|
||||
stroke="white"
|
||||
strokeWidth={secili ? 1.8 : 1}
|
||||
>
|
||||
<title>{city.name}</title>
|
||||
</path>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</KonumBolumu>
|
||||
);
|
||||
}
|
||||
|
||||
function KonumBolumu({
|
||||
universite,
|
||||
children,
|
||||
}: {
|
||||
universite: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="universite-konumu"
|
||||
@@ -23,34 +79,34 @@ export function UniversiteKonumHaritasi({
|
||||
<h2 id="universite-konumu" className="sr-only">
|
||||
{universite} kampüs konumu
|
||||
</h2>
|
||||
|
||||
<div className="relative h-full min-h-44 w-full">
|
||||
<svg
|
||||
viewBox={`0 ${HARITA_UST_KIRPMA} ${HARITA_GENISLIK} ${HARITA_YUKSEKLIK - HARITA_UST_KIRPMA}`}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
role="img"
|
||||
aria-label={`${universite} konumunun Türkiye haritasındaki görünümü`}
|
||||
>
|
||||
{cities.map((city) => {
|
||||
const secili = il ? normalizeIlAdi(city.name) === il : false;
|
||||
return (
|
||||
<path
|
||||
key={city.id}
|
||||
d={city.path}
|
||||
fill={
|
||||
secili
|
||||
? "oklch(0.809 0.105 251.8)"
|
||||
: "oklch(0.929 0.013 255.5)"
|
||||
}
|
||||
stroke="white"
|
||||
strokeWidth={secili ? 1.8 : 1}
|
||||
>
|
||||
<title>{city.name}</title>
|
||||
</path>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
<div className="relative h-full min-h-44 w-full">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function UlkeHaritasi({
|
||||
universite,
|
||||
konum,
|
||||
}: {
|
||||
universite: string;
|
||||
konum: UlkeKonum;
|
||||
}) {
|
||||
const { harita, ulkeAd, sehir } = konum;
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${ULKE_HARITA_GENISLIK} ${ULKE_HARITA_YUKSEKLIK}`}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
role="img"
|
||||
aria-label={`${universite} konumunun ${ulkeAd} haritasındaki görünümü${sehir ? ` (${sehir})` : ""}`}
|
||||
>
|
||||
{harita.baglam.map((d, i) => (
|
||||
<path key={`b${i}`} d={d} fill={ZEMIN_DOLGU} stroke="white" strokeWidth={1} />
|
||||
))}
|
||||
{harita.ulke.map((d, i) => (
|
||||
<path key={`u${i}`} d={d} fill={SECILI_DOLGU} stroke="white" strokeWidth={1.8}>
|
||||
<title>{ulkeAd}</title>
|
||||
</path>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,8 +106,8 @@ export function ProgramTablosu({
|
||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
||||
varsayilanDilim(sonuclar),
|
||||
);
|
||||
// Sihirbaz seçimlerinden URL'e yazılan filtreler (bkz. sonucHref) —
|
||||
// sayfalama isteklerinde ve veri anahtarında kullanılır.
|
||||
// Sihirbaz seçimlerinden profil çerezine yazılıp SSR'da prop olarak gelen
|
||||
// filtreler — sayfalama isteklerinde ve veri anahtarında kullanılır.
|
||||
const iller = useMemo(() => filtreIller ?? [], [filtreIller]);
|
||||
|
||||
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür/filtre
|
||||
|
||||
@@ -81,7 +81,7 @@ export function SecimlerimPaneli() {
|
||||
asChild
|
||||
className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
||||
>
|
||||
<Link href={sonucHref(profil)}>Sonuçlara git</Link>
|
||||
<Link href={sonucHref()}>Sonuçlara git</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -93,7 +93,7 @@ function baskinRisk(pinler: HaritaPin[]): RiskSeviyesi {
|
||||
}
|
||||
|
||||
/** Aynı noktaya düşen pinleri küçük bir halka üzerinde ayrıştırır. */
|
||||
function cakismaAc(pinler: HaritaPin[]): HaritaPin[] {
|
||||
function cakismaAc(pinler: HaritaPin[], halka: number): HaritaPin[] {
|
||||
const gruplar = new Map<string, HaritaPin[]>();
|
||||
for (const p of pinler) {
|
||||
const key = `${Math.round(p.x)}:${Math.round(p.y)}`;
|
||||
@@ -109,7 +109,11 @@ function cakismaAc(pinler: HaritaPin[]): HaritaPin[] {
|
||||
}
|
||||
grup.forEach((p, i) => {
|
||||
const aci = (2 * Math.PI * i) / grup.length;
|
||||
sonuc.push({ ...p, x: p.x + Math.cos(aci) * 5, y: p.y + Math.sin(aci) * 5 });
|
||||
sonuc.push({
|
||||
...p,
|
||||
x: p.x + Math.cos(aci) * halka,
|
||||
y: p.y + Math.sin(aci) * halka,
|
||||
});
|
||||
});
|
||||
}
|
||||
return sonuc;
|
||||
@@ -243,7 +247,12 @@ export function TercihHaritasi({
|
||||
() => (seciliIl ? (ilPinleri.get(seciliIl) ?? []) : []),
|
||||
[ilPinleri, seciliIl],
|
||||
);
|
||||
const acikPinler = useMemo(() => cakismaAc(seciliPinler), [seciliPinler]);
|
||||
// Halka yarıçapı zoom oranıyla ölçeklenir ki pinler ekranda hep aynı
|
||||
// mesafede ayrışsın (pin boyutları da sf ile ekran-sabit).
|
||||
const acikPinler = useMemo(
|
||||
() => cakismaAc(seciliPinler, 20 * sf),
|
||||
[seciliPinler, sf],
|
||||
);
|
||||
|
||||
// Ülke seviyesi toplu pinler: il başına adet/merkez/renk tek geçişte
|
||||
// (üç ayrı reduce + baskinRisk dördüncü geçişti) ve memo'da.
|
||||
@@ -370,7 +379,7 @@ export function TercihHaritasi({
|
||||
// Kilitli pinlerde logo da gösterilmez — üniversite kimliği
|
||||
// client'a inmediği gibi görselden de sızmamalı.
|
||||
const logo = p.kilitli ? null : uniLogoYoluFromAd(p.universite);
|
||||
const r = (p.kilitli ? 7 : logo ? 11 : 8) * sf;
|
||||
const r = (p.kilitli ? 12 : logo ? 20 : 14) * sf;
|
||||
const etiket = p.kilitli ? null : kisaUniAdi(p.universite);
|
||||
return (
|
||||
<g
|
||||
@@ -396,31 +405,31 @@ export function TercihHaritasi({
|
||||
r={r}
|
||||
fill="white"
|
||||
stroke={RISK_RENK[p.risk]}
|
||||
strokeWidth={2 * sf}
|
||||
strokeWidth={2.5 * sf}
|
||||
/>
|
||||
<image
|
||||
href={logo}
|
||||
x={p.x - 8 * sf}
|
||||
y={p.y - 8 * sf}
|
||||
width={16 * sf}
|
||||
height={16 * sf}
|
||||
x={p.x - 15 * sf}
|
||||
y={p.y - 15 * sf}
|
||||
width={30 * sf}
|
||||
height={30 * sf}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<circle
|
||||
cx={p.x + r * 0.78}
|
||||
cy={p.y - r * 0.78}
|
||||
r={4.5 * sf}
|
||||
r={7.5 * sf}
|
||||
fill={RISK_RENK[p.risk]}
|
||||
stroke="white"
|
||||
strokeWidth={1.2 * sf}
|
||||
strokeWidth={1.5 * sf}
|
||||
/>
|
||||
<text
|
||||
x={p.x + r * 0.78}
|
||||
y={p.y - r * 0.78}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={6 * sf}
|
||||
fontSize={9.5 * sf}
|
||||
fontWeight={700}
|
||||
fill="white"
|
||||
className="pointer-events-none select-none"
|
||||
@@ -437,14 +446,14 @@ export function TercihHaritasi({
|
||||
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
|
||||
stroke="white"
|
||||
strokeWidth={2 * sf}
|
||||
strokeDasharray={p.kilitli ? `${3 * sf} ${2.5 * sf}` : undefined}
|
||||
strokeDasharray={p.kilitli ? `${4 * sf} ${3 * sf}` : undefined}
|
||||
/>
|
||||
<text
|
||||
x={p.x}
|
||||
y={p.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={9 * sf}
|
||||
fontSize={13 * sf}
|
||||
fontWeight={700}
|
||||
fill="white"
|
||||
className="pointer-events-none select-none"
|
||||
@@ -456,13 +465,13 @@ export function TercihHaritasi({
|
||||
{etiket ? (
|
||||
<text
|
||||
x={p.x}
|
||||
y={p.y - r - 4 * sf}
|
||||
y={p.y - r - 6 * sf}
|
||||
textAnchor="middle"
|
||||
fontSize={10.5 * sf}
|
||||
fontSize={16 * sf}
|
||||
fontWeight={600}
|
||||
fill="oklch(0.279 0.041 260.0)" // slate-800
|
||||
stroke="white"
|
||||
strokeWidth={3 * sf}
|
||||
strokeWidth={4 * sf}
|
||||
paintOrder="stroke"
|
||||
className="pointer-events-none select-none"
|
||||
>
|
||||
|
||||
@@ -199,11 +199,14 @@ function KapıIcerik({
|
||||
if (!duzenle && !bekleyenVar && hedef !== "sihirbaz") {
|
||||
toast.success("Tercihlerin kaydedildi.");
|
||||
}
|
||||
// Düzenlemede replace: aynı /sonuc sayfasında sira/tur değişince geçmiş
|
||||
// şişmesin; ilk kurulumda push ile hedefe gitsin.
|
||||
// Düzenlemede replace (geçmiş şişmesin), ilk kurulumda push. Profil
|
||||
// artık çerezden SSR'landığı ve URL çoğu zaman değişmediği için sunucu
|
||||
// tablosunun yeni profille tazelenmesi her iki yolda da refresh ile
|
||||
// garanti edilir (zaten /sonuc'tayken aynı URL'e push no-op kalabilir).
|
||||
if (yonlendir) {
|
||||
if (duzenle) router.replace(yonlendir);
|
||||
else router.push(yonlendir);
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ export function sonucaGitIste(secenekler?: { dogrudanSira?: boolean }): {
|
||||
if (!durum.profilYuklendi) yukle();
|
||||
const profil = durum.profil ?? tercihProfiliOku();
|
||||
if (profil) {
|
||||
return { href: sonucHref(profil), kapiAcildi: false };
|
||||
return { href: sonucHref(), kapiAcildi: false };
|
||||
}
|
||||
profilToplamaAc("sonuc", secenekler?.dogrudanSira ?? false);
|
||||
return { href: null, kapiAcildi: true };
|
||||
@@ -245,7 +245,6 @@ export function profilTamamlandi(
|
||||
profil: TercihProfili,
|
||||
girisli = false,
|
||||
): string | null {
|
||||
const onceki = durum.profil;
|
||||
const duzenle = durum.duzenle;
|
||||
riskleriYenile(profil);
|
||||
const bekleyen = durum.bekleyen;
|
||||
@@ -268,20 +267,14 @@ export function profilTamamlandi(
|
||||
// Yapay Zeka niyetli giriş: kapıda sihirbazı zaten doldurdu, /sonuc'ta
|
||||
// akış kesilmesin — girişliyse üretim otomatik başlar (?sihirbaz=1),
|
||||
// girişsizse ücretsiz tablo + giriş CTA paneli (?hazir=1) karşılar.
|
||||
return sonucHref(profil, girisli ? { sihirbaz: "1" } : { hazir: "1" });
|
||||
}
|
||||
if (hedef === "sonuc") return sonucHref(profil);
|
||||
// Düzenlemede sıra/tür değişince /sonuc?sira= URL'si yenilenmezse sunucudaki
|
||||
// hayal/dengeli/garanti tablosu eski sıralamada kalır (profil localStorage'da
|
||||
// güncellenir ama sayfa searchParams'tan hesaplanır).
|
||||
if (
|
||||
duzenle &&
|
||||
typeof window !== "undefined" &&
|
||||
window.location.pathname.startsWith("/sonuc") &&
|
||||
(onceki?.sira !== profil.sira || onceki?.tur !== profil.tur)
|
||||
) {
|
||||
return sonucHref(profil);
|
||||
return sonucHref(girisli ? { sihirbaz: "1" } : { hazir: "1" });
|
||||
}
|
||||
if (hedef === "sonuc") return sonucHref();
|
||||
// Düzenleme /sonuc'a götürür: tablo profil çerezinden SSR'lanır
|
||||
// (sira/tur/il/tip) ve tercihProfiliYaz çerezi az önce güncelledi. URL
|
||||
// değişmediği için sunucu tablosunun tazelenmesi kapı bileşenindeki
|
||||
// router.refresh ile garanti edilir.
|
||||
if (duzenle) return sonucHref();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function RaporYokCta() {
|
||||
asChild
|
||||
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href={profil ? sonucHref(profil, { sihirbaz: "1" }) : "/"}>
|
||||
<Link href={profil ? sonucHref({ sihirbaz: "1" }) : "/"}>
|
||||
24'lük listeni oluştur
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function KapanisCta() {
|
||||
size="lg"
|
||||
className="mt-8 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href={sonucHref(profil)}>
|
||||
<Link href={sonucHref()}>
|
||||
Üniversitelere göz at
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
|
||||
@@ -240,7 +240,7 @@ export function DemoKapanisCta() {
|
||||
: "Kuralları öğrendin — şimdi kendi sıralamanla dene."}
|
||||
</p>
|
||||
<Link
|
||||
href={profil ? sonucHref(profil) : "/"}
|
||||
href={profil ? sonucHref() : "/"}
|
||||
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"
|
||||
>
|
||||
{profil ? "Sıralamana uygun programları gör" : "Sıralamanı gir"}
|
||||
|
||||
@@ -151,7 +151,7 @@ export function ListeUretici({
|
||||
{profil ? (
|
||||
<p className="mt-4 text-xs text-slate-500">
|
||||
Seçimlerin kayıtlı —{" "}
|
||||
<Link href={sonucHref(profil)} className="underline">
|
||||
<Link href={sonucHref()} className="underline">
|
||||
sonuç sayfasına dönebilirsin
|
||||
</Link>
|
||||
.
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ListemBosCta() {
|
||||
size="lg"
|
||||
className="mt-6 cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
||||
>
|
||||
<Link href={profil ? sonucHref(profil, { sihirbaz: "1" }) : "/"}>
|
||||
<Link href={profil ? sonucHref({ sihirbaz: "1" }) : "/"}>
|
||||
{profil ? "Yapay Zeka listeni kur" : "Sıralamanı gir, Yapay Zeka listeni kur"}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
|
||||
@@ -108,7 +108,7 @@ export function TadimlikSatiri({
|
||||
const seri = raporSiraSerisi(p.siraGecmisi);
|
||||
const taban = p.efektifSira ?? programEtkinSira(seri);
|
||||
const devlet = p.unitur === "DEVLET";
|
||||
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref({ sira, tur }))}`;
|
||||
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref())}`;
|
||||
|
||||
return (
|
||||
<section ref={gorunumRef} className="mt-16 min-w-0">
|
||||
|
||||
@@ -123,9 +123,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
const yeniProfil = { sira, tur, secimler };
|
||||
tercihProfiliYaz(yeniProfil);
|
||||
setAcik(false);
|
||||
router.push(
|
||||
sonucHref(yeniProfil, girisli ? { sihirbaz: "1" } : { hazir: "1" }),
|
||||
);
|
||||
router.push(sonucHref(girisli ? { sihirbaz: "1" } : { hazir: "1" }));
|
||||
}
|
||||
|
||||
function acikDegisti(v: boolean) {
|
||||
@@ -150,7 +148,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
programların hazır.
|
||||
</p>
|
||||
<Link
|
||||
href={sonucHref(profil)}
|
||||
href={sonucHref()}
|
||||
className="mt-4 inline-flex h-12 items-center justify-center gap-2 rounded-full bg-orange-500 px-7 text-sm font-semibold text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
Sıralamana uygun programları gör
|
||||
|
||||
@@ -100,9 +100,7 @@ export function HeroForm() {
|
||||
const profil = { sira, tur, secimler };
|
||||
tercihProfiliYaz(profil);
|
||||
setModalAcik(false);
|
||||
router.push(
|
||||
sonucHref(profil, girisli ? { sihirbaz: "1" } : { hazir: "1" }),
|
||||
);
|
||||
router.push(sonucHref(girisli ? { sihirbaz: "1" } : { hazir: "1" }));
|
||||
}
|
||||
|
||||
// Kayıtlı profil varsa sihirbazı atla, doğrudan sonuç sayfasına götür.
|
||||
@@ -116,7 +114,7 @@ export function HeroForm() {
|
||||
size="lg"
|
||||
className="w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto"
|
||||
>
|
||||
<Link href={sonucHref(kayitliProfil)}>
|
||||
<Link href={sonucHref()}>
|
||||
Üniversitelere göz at
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
|
||||
@@ -110,6 +110,9 @@ export function SihirbazAdimlar({
|
||||
// eşleşme için iki taraf da normalizeIlAdi'dan geçirilir.
|
||||
const [ilArama, setIlArama] = useState("");
|
||||
const ilAramaNorm = normalizeIlAdi(ilArama.trim());
|
||||
// 81 çip ilk bakışta boğucu; varsayılan görünüm ilk 10 il + "+N il daha"
|
||||
// rozeti. Arama açıkken kırpma yok (arama zaten görünümü daraltıyor).
|
||||
const [tumIllerAcik, setTumIllerAcik] = useState(false);
|
||||
const gorunenTurkiyeIlleri = useMemo(
|
||||
() =>
|
||||
ilAramaNorm
|
||||
@@ -119,6 +122,14 @@ export function SihirbazAdimlar({
|
||||
: turkiyeIlleri,
|
||||
[turkiyeIlleri, ilAramaNorm],
|
||||
);
|
||||
const IL_ON_IZLEME = 10;
|
||||
const ilListesiKirpik =
|
||||
!ilAramaNorm &&
|
||||
!tumIllerAcik &&
|
||||
gorunenTurkiyeIlleri.length > IL_ON_IZLEME;
|
||||
const listelenenTurkiyeIlleri = ilListesiKirpik
|
||||
? gorunenTurkiyeIlleri.slice(0, IL_ON_IZLEME)
|
||||
: gorunenTurkiyeIlleri;
|
||||
const gorunenDisIller = useMemo(
|
||||
() =>
|
||||
ilAramaNorm
|
||||
@@ -314,15 +325,16 @@ export function SihirbazAdimlar({
|
||||
|
||||
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
||||
güvenilir dokunma hedefi değil); iller seçili kategorilere göre
|
||||
süzülmüş, program sayısına göre sıralı listelenir. Penceresinde
|
||||
programı olan TÜM iller görünür (kırpma yok); arama yalnızca
|
||||
görünümü daraltır, seçimleri etkilemez. */}
|
||||
süzülmüş, program sayısına göre sıralı listelenir. Varsayılan
|
||||
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-56 flex-wrap gap-1.5 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
||||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
{gorunenTurkiyeIlleri.map((i) => (
|
||||
{listelenenTurkiyeIlleri.map((i) => (
|
||||
<Cip
|
||||
key={i.il}
|
||||
kucuk
|
||||
@@ -333,6 +345,27 @@ export function SihirbazAdimlar({
|
||||
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
||||
</Cip>
|
||||
))}
|
||||
{ilListesiKirpik ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTumIllerAcik(true)}
|
||||
className="cursor-pointer rounded-full border border-dashed border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-500 transition-colors duration-200 hover:border-slate-400 hover:bg-slate-50 hover:text-slate-700"
|
||||
>
|
||||
+{gorunenTurkiyeIlleri.length - IL_ON_IZLEME} il daha
|
||||
</button>
|
||||
) : null}
|
||||
{!ilListesiKirpik &&
|
||||
tumIllerAcik &&
|
||||
!ilAramaNorm &&
|
||||
gorunenTurkiyeIlleri.length > IL_ON_IZLEME ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTumIllerAcik(false)}
|
||||
className="cursor-pointer rounded-full border border-dashed border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-500 transition-colors duration-200 hover:border-slate-400 hover:bg-slate-50 hover:text-slate-700"
|
||||
>
|
||||
Daha az göster
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{ilAramaNorm &&
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
// /sonuc?sira= yokken: kayıtlı profil varsa oraya taşı; yoksa kapıyı aç.
|
||||
// Sıra bilgisi localStorage'daki TercihProfili ile tüm uygulamada ortak.
|
||||
// Profil çerezi yokken: localStorage'da kayıtlı profil varsa çereze taşıyıp
|
||||
// sayfayı tazele (URL değişmez); yoksa kapıyı aç. Sıra bilgisi localStorage'daki
|
||||
// TercihProfili ile tüm uygulamada ortak, sunucu tablosu çerezden okur.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -9,7 +10,7 @@ import Link from "next/link";
|
||||
import { ArrowLeft, Trophy } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import { profilCerezineYaz } from "@/features/sihirbaz/sihirbaz-profil";
|
||||
import {
|
||||
sonucaGitIste,
|
||||
useProfilKapisi,
|
||||
@@ -20,9 +21,14 @@ export function SiraGerekli() {
|
||||
const { profil, profilYuklendi } = useProfilKapisi();
|
||||
|
||||
// "Yönleniyor" durumu ayrı state gerektirmez: profil varken aşağıdaki koşul
|
||||
// zaten bekleme ekranını gösteriyor; effect yalnızca yönlendirmeyi yapar.
|
||||
// zaten bekleme ekranını gösteriyor. Çerezi olmayan eski kullanıcı (profil
|
||||
// yalnızca localStorage'da) burada çereze taşınır; refresh sunucu tablosunu
|
||||
// aynı URL'de yeni çerezle render eder.
|
||||
useEffect(() => {
|
||||
if (profilYuklendi && profil) router.replace(sonucHref(profil));
|
||||
if (profilYuklendi && profil) {
|
||||
profilCerezineYaz(profil);
|
||||
router.refresh();
|
||||
}
|
||||
}, [profil, profilYuklendi, router]);
|
||||
|
||||
if (!profilYuklendi || profil) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Şema, doğrulama ve sabitler sihirbaz-sabitler.ts'te yaşar.
|
||||
|
||||
import {
|
||||
PROFIL_CEREZ_ADI,
|
||||
PROFIL_DEGISTI_EVENT,
|
||||
SIHIRBAZ_STORAGE_KEY,
|
||||
tercihProfiliDogrula,
|
||||
@@ -42,6 +43,24 @@ export function tercihProfiliOku(): TercihProfili | null {
|
||||
return profilCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profilin sunucu aynası: /sonuc SSR'ı tabloyu bu çerezden filtreler
|
||||
* (bkz. PROFIL_CEREZ_ADI). localStorage'ı olup çerezi olmayan eski
|
||||
* kullanıcıların taşınması için SiraGerekli de bunu çağırır.
|
||||
*/
|
||||
export function profilCerezineYaz(profil: TercihProfili): void {
|
||||
if (typeof document === "undefined") return;
|
||||
const guvenli = window.location.protocol === "https:" ? "; secure" : "";
|
||||
document.cookie = `${PROFIL_CEREZ_ADI}=${encodeURIComponent(
|
||||
JSON.stringify(profil),
|
||||
)}; path=/; max-age=31536000; samesite=lax${guvenli}`;
|
||||
}
|
||||
|
||||
function profilCereziniSil(): void {
|
||||
if (typeof document === "undefined") return;
|
||||
document.cookie = `${PROFIL_CEREZ_ADI}=; path=/; max-age=0`;
|
||||
}
|
||||
|
||||
/** Ortak profili yazar; hero ve Yapay Zeka sihirbazıyla aynı anahtarı kullanır. */
|
||||
export function tercihProfiliYaz(profil: TercihProfili): void {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -50,6 +69,7 @@ export function tercihProfiliYaz(profil: TercihProfili): void {
|
||||
try {
|
||||
localStorage.setItem(SIHIRBAZ_STORAGE_KEY, JSON.stringify(dogru));
|
||||
} catch {}
|
||||
profilCerezineYaz(dogru);
|
||||
profilCache = dogru;
|
||||
profilDegistiginiYayinla();
|
||||
}
|
||||
@@ -60,6 +80,7 @@ export function tercihProfiliSil(): void {
|
||||
try {
|
||||
localStorage.removeItem(SIHIRBAZ_STORAGE_KEY);
|
||||
} catch {}
|
||||
profilCereziniSil();
|
||||
profilCache = null;
|
||||
profilDegistiginiYayinla();
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ export const PUAN_TURU_ETIKET: Record<string, string> = {
|
||||
// dönene kadar burada bekler.
|
||||
export const SIHIRBAZ_STORAGE_KEY = "kolaytercih.sihirbaz";
|
||||
|
||||
// Profilin sunucuya giden kopyası: /sonuc tablosu SSR'da bu çerezden
|
||||
// filtrelenir, URL'de sira/il/tip paramı taşınmaz. localStorage client
|
||||
// tarafındaki tek doğruluk kaynağı olmayı sürdürür; çerez onun aynasıdır
|
||||
// (tercihProfiliYaz/Sil ikisini birlikte günceller).
|
||||
export const PROFIL_CEREZ_ADI = "kolaytercih.profil";
|
||||
|
||||
// storage olayı yalnızca diğer sekmelerde tetiklenir; aynı sekmedeki
|
||||
// aboneler (ör. navbar sıralama rozeti) bu olayla haberdar edilir.
|
||||
export const PROFIL_DEGISTI_EVENT = "kolaytercih:profil-degisti";
|
||||
@@ -126,30 +132,27 @@ export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
|
||||
return { sira, tur: p.tur, secimler };
|
||||
}
|
||||
|
||||
/**
|
||||
* Profildeki sıra/tur ile sonuç sayfası URL'i — tüm uygulama aynı sorguyu
|
||||
* kullanır. Profilde sihirbaz seçimleri varsa il + üniversite tipi de URL'e
|
||||
* yazılır: /sonuc'un ham tablosu SSR'da bu paramlarla filtrelenir (tek
|
||||
* doğruluk kaynağı URL; localStorage profili yalnızca URL'i üretir).
|
||||
*/
|
||||
export function sonucHref(
|
||||
profil: Pick<TercihProfili, "sira" | "tur"> & {
|
||||
secimler?: SihirbazSecimleri;
|
||||
},
|
||||
ekstra?: Record<string, string>,
|
||||
): string {
|
||||
const q = new URLSearchParams({
|
||||
sira: String(profil.sira),
|
||||
tur: profil.tur,
|
||||
});
|
||||
if (profil.secimler) {
|
||||
for (const il of profil.secimler.iller) q.append("il", il);
|
||||
if (profil.secimler.universiteTipi !== "farketmez") {
|
||||
q.set("tip", profil.secimler.universiteTipi);
|
||||
}
|
||||
/** /sonuc çerezindeki ham değeri güvenle profile çözer (SSR tarafı). */
|
||||
export function cerezdenTercihProfili(
|
||||
ham: string | undefined | null,
|
||||
): TercihProfili | null {
|
||||
if (!ham) return null;
|
||||
try {
|
||||
return tercihProfiliDogrula(JSON.parse(decodeURIComponent(ham)));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
for (const [k, v] of Object.entries(ekstra ?? {})) q.set(k, v);
|
||||
return `/sonuc?${q}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sonuç sayfası URL'i. Profil verisi (sıra/tür/il/tip) URL'de taşınmaz —
|
||||
* /sonuc'un ham tablosu SSR'da PROFIL_CEREZ_ADI çerezinden filtrelenir.
|
||||
* `ekstra` yalnızca tek seferlik akış bayrakları içindir (sihirbaz=1, hazir=1).
|
||||
*/
|
||||
export function sonucHref(ekstra?: Record<string, string>): string {
|
||||
const girdiler = Object.entries(ekstra ?? {});
|
||||
if (girdiler.length === 0) return "/sonuc";
|
||||
return `/sonuc?${new URLSearchParams(girdiler)}`;
|
||||
}
|
||||
|
||||
/** Prompt'a enjekte edilecek Türkçe özet. */
|
||||
|
||||
Reference in New Issue
Block a user