Compare commits
5 Commits
9e9de84a7e
...
507bf53a95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
507bf53a95 | ||
|
|
992a01abf1 | ||
|
|
ea3310ac5b | ||
|
|
c2ec6d3102 | ||
|
|
95bc87e5f7 |
@@ -130,6 +130,28 @@ Neden gerekli: arşivlenmezse eski kodlu satırlar listelerde bir yıl önceki s
|
||||
görünmeye devam eder; öğrenci o kodu tercih ekranında bulamaz, kodu değişen program
|
||||
iki kez listelenir.
|
||||
|
||||
### 5b. Kapanan sayfaları yönlendir (5–15 dk)
|
||||
|
||||
`--kuru` raporundaki `KAPANACAK /bolum/<slug>` ve `KAPANACAK /universite/<slug>`
|
||||
listelerindeki her adres (ve `--kimlik-onar` sonrası kapanan sayfa olduysa onlar da)
|
||||
`src/lib/kapanan-sluglar.ts` içindeki `KAPANAN_SLUGLAR` listesine eklenir; site bu
|
||||
adresleri yaşayan en yakın sayfaya **kalıcı (308)** yönlendirir (`next.config.ts` →
|
||||
`redirects()`, `/sayfa/N` alt yolları dahil). Eklemezsen kapanan adres Google'da
|
||||
"sayfa yok" gövdesiyle kalır.
|
||||
|
||||
- Hedef sırası: (1) program/kurum **ad değiştirdiyse yeni adı** — kanıt: aynı
|
||||
üniversitede geçmiş yıl sıraları yeni adlı satıra birebir taşınmış, ya da Resmî
|
||||
Gazete kararı; (2) aynı alanın lisans/önlisans karşılığı; (3) emin değilsen hub:
|
||||
`/bolumler` ya da `/universiteler`. Uydurma eşleme yapma; `gerekce` alanına kanıtı yaz.
|
||||
- Hedef slug `programs` tablosundan türemeli ve kendisi listede **kaynak** olmamalı
|
||||
(zincir). Zinciri dosyadaki `kapananYonlendirmeleri()` build'de yakalar; hedefin
|
||||
yaşadığını sen kontrol et: 8. adımda hedef adresi tarayıcıda aç.
|
||||
- Bu yıl **yeniden açılan** bir sayfa listede kaynak olarak duruyorsa kaydını sil —
|
||||
yönlendirme sayfadan önce çalışır, silmezsen yeni sayfa hiç görünmez. Kontrol:
|
||||
8. adımda listedeki `kaynak` adlarını `/bolumler` ve `/universiteler` sayfasında ara;
|
||||
çıkan varsa kaydını sil.
|
||||
- Hedef seçimi SEO'yu etkilediği için **[BİLAL] kararıdır**; listeyi commit'ten önce göster.
|
||||
|
||||
## 6. İndeks + dosyayı toparla (1 dk)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { HTML_LIMITED_BOT_UA_RE } from "next/dist/shared/lib/router/utils/html-bots";
|
||||
// Bağımlılıksız veri dosyası; config uygulama paketinden önce yüklendiği için
|
||||
// göreli yolla alınır (Docker builder'da src/ build'den önce kopyalanır).
|
||||
import { kapananYonlendirmeleri } from "./src/lib/kapanan-sluglar";
|
||||
|
||||
// Dockerfile builder katmanı BUILD_KAYNAK_KISITLI=1 verir: imaj build'i prod
|
||||
// VPS'te çalışan uygulamayla aynı makinede koştuğu için bellek/paralellik kısılır.
|
||||
@@ -75,6 +78,10 @@ const nextConfig: NextConfig = {
|
||||
{ source: "/sohbet", destination: "/listem", permanent: false },
|
||||
// Ayrı robot sayfası kaldırıldı; rehber içi linkler hero formuna düşer.
|
||||
{ source: "/tercih-robotu", destination: "/#hero-form", permanent: true },
|
||||
// Veri güncellemesiyle tamamen kapanan /bolum ve /universite sayfaları
|
||||
// (sayfalama alt yolları dahil) yaşayan en yakın sayfaya kalıcı (308)
|
||||
// gider. Eşlemenin tek kaynağı src/lib/kapanan-sluglar.ts.
|
||||
...kapananYonlendirmeleri(),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ const problems = [
|
||||
icon: Wallet,
|
||||
title: "Danışman lüks haline geldi",
|
||||
description:
|
||||
"İyi bir tercih danışmanı 1.000–3.000 TL istiyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.",
|
||||
"İyi bir tercih danışmanı 1.000–3.000 TL istiyor. Ücretsiz robotların çoğu ise yalnızca liste filtreliyor; listeyi kurup gerekçesini yazmıyor.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -124,7 +124,7 @@ const faqs = [
|
||||
{
|
||||
question: "Ücretsiz tercih robotlarından farkı ne?",
|
||||
answer:
|
||||
"Ücretsiz robotlar puan aralığına göre bölüm listeler ve gerisini sana bırakır. KolayTercih ise listenin kendisini kurar: hangi tercihi kaçıncı sıraya, neden koyduğunu açıklar, riskini söyler ve sorularını cevaplar. Yani filtre değil, danışmandır.",
|
||||
"Ücretsiz robotların çoğu puan aralığına göre bölüm listeler ve gerisini sana bırakır; bazıları yapay zekâ sohbeti de sunar. KolayTercih listenin kendisini kurar: hangi tercihi kaçıncı sıraya, neden koyduğunu açıklar, riskini geçmiş taban verisinden hesaplar ve sorularını cevaplar. Yapay zekâ yalnızca sıralamanla açılan gerçek programlardan seçer; bu havuzun dışına çıkamaz.",
|
||||
},
|
||||
{
|
||||
question: "Yerleşme garantisi veriyor musunuz?",
|
||||
@@ -144,22 +144,37 @@ const comparison = [
|
||||
danisman: true,
|
||||
tercihai: true,
|
||||
},
|
||||
// Rakip hücreleri genelleme yapmaz: bazı ücretsiz robotlar artık yapay zekâ
|
||||
// sohbeti/dengeli liste de sunuyor, bazı danışmanlar 7/24 yazışıyor. Emin
|
||||
// olmadığımız yerde ✗ yerine metin ("Bazılarında" vb.) kullanılır.
|
||||
{
|
||||
feature: `Son ${VERI_YIL_SAYISI} yılın trend analizi`,
|
||||
robot: false,
|
||||
feature: `Son ${VERI_YIL_SAYISI} yılın taban trendi, her tercihte`,
|
||||
robot: "Bazılarında",
|
||||
danisman: true,
|
||||
tercihai: true,
|
||||
},
|
||||
{
|
||||
feature: "Gerekçeli, dengeli 24'lük liste",
|
||||
robot: false,
|
||||
robot: "Bazılarında",
|
||||
danisman: true,
|
||||
tercihai: true,
|
||||
},
|
||||
{
|
||||
feature: "Her öneri resmî YÖK Atlas kaydına dayanır (uydurma bölüm yok)",
|
||||
robot: "Sohbet botlarında belirsiz",
|
||||
danisman: true,
|
||||
tercihai: true,
|
||||
},
|
||||
{
|
||||
feature: "Nasıl çalıştığı herkese açık yazılı",
|
||||
robot: "Genelde yok",
|
||||
danisman: "Danışmana göre",
|
||||
tercihai: true,
|
||||
},
|
||||
{
|
||||
feature: "7/24 soru-cevap",
|
||||
robot: false,
|
||||
danisman: false,
|
||||
robot: "Bazılarında",
|
||||
danisman: "Danışmana göre",
|
||||
tercihai: true,
|
||||
},
|
||||
{
|
||||
@@ -420,7 +435,7 @@ export default function Home() {
|
||||
<TableRow className="bg-slate-50">
|
||||
<TableHead className="w-[40%]">Özellik</TableHead>
|
||||
<TableHead className="text-center">
|
||||
Ücretsiz robotlar
|
||||
Ücretsiz robotlar ve sohbet botları
|
||||
</TableHead>
|
||||
<TableHead className="text-center">
|
||||
İnsan danışman
|
||||
|
||||
@@ -4,10 +4,7 @@ import type { Metadata } from "next";
|
||||
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import {
|
||||
PaketSatinAl,
|
||||
PaketSatinAlSkeleton,
|
||||
} from "@/features/odeme/components/paket-satinal";
|
||||
import { PaketSatinAl } from "@/features/odeme/components/paket-satinal";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
@@ -57,9 +54,9 @@ export default function PaketPage({
|
||||
</p>
|
||||
</Parallax>
|
||||
|
||||
<Suspense fallback={<PaketSatinAlSkeleton />}>
|
||||
<PaketSatinAl />
|
||||
</Suspense>
|
||||
{/* Kart gövdesi statik kabukta; yalnız CTA alanları içeride kendi
|
||||
Suspense sınırlarıyla istek anında gelir (bkz. paket-satinal.tsx). */}
|
||||
<PaketSatinAl />
|
||||
|
||||
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
|
||||
KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas
|
||||
|
||||
@@ -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 }) {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* 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. */}
|
||||
<div className="mt-6">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="universite-programlari" className="mt-8">
|
||||
<div className="mb-4 flex items-end justify-between gap-4">
|
||||
<h2
|
||||
@@ -286,7 +295,8 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
baslik={`Sıralaman ${bolum.ad} tabanlarının neresinde?`}
|
||||
konum="alt"
|
||||
baslik={ctaBaslik}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 = (
|
||||
<div className="mt-6">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -276,6 +287,8 @@ export function UniversiteIcerik({
|
||||
</em>
|
||||
</p>
|
||||
|
||||
{ustSiraSatiri}
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||||
<UniversiteKonumHaritasi universite={uni.ad} il={uni.il} />
|
||||
<dl className="contents">
|
||||
@@ -319,22 +332,25 @@ export function UniversiteIcerik({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||
{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{" "}
|
||||
<Link
|
||||
href={`/universite/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
ilk sayfada
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||
{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{" "}
|
||||
<Link
|
||||
href={`/universite/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
ilk sayfada
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
{ustSiraSatiri}
|
||||
</>
|
||||
)}
|
||||
|
||||
<PagePixelDivider seed={57} className="mt-8" />
|
||||
@@ -363,7 +379,8 @@ export function UniversiteIcerik({
|
||||
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
baslik={`Sıralaman ${uni.ad} tabanlarının neresinde?`}
|
||||
konum="alt"
|
||||
baslik={ctaBaslik}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
|
||||
import { URUNLER, DENEME_KREDISI, RAPOR_KREDI } from "@/lib/credits";
|
||||
import { ACIK_SATIR } from "@/lib/rapor-maske";
|
||||
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
@@ -39,11 +41,17 @@ const OZELLIKLER = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Paket + top-up kartları: kullanıcının paket/kredi durumuna göre CTA'lar
|
||||
// değişir, okuma burada yaşar.
|
||||
export async function PaketSatinAl() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
// Paket + top-up kartları. Kartların gövdesi (başlık, fiyat, özellikler,
|
||||
// iyzico bandı) STATİK kabukta kalır; kullanıcıya bağlı olan yalnızca iki küçük
|
||||
// CTA alanıdır (PaketCta, TopupCta) ve her biri kendi Suspense sınırındadır.
|
||||
//
|
||||
// Neden bu kadar küçük parçalar: React Flight ~3,2 KB'ı aşan bir dinamik
|
||||
// parçanın devamını ayrı satırlara böler; PPR resume bunlara yeni segment
|
||||
// kimliği verirken kabukta geç tamamlanan sınırın (kök loading.tsx) kimliğiyle
|
||||
// çakışıyor (Next 16.2 / React 19.2) ve tarayıcıda HierarchyRequestError ile
|
||||
// sayfa boş kalıyordu. Dinamik parçayı bu sınırın altında tut: kartın gövdesini
|
||||
// yeniden bu async bileşenlerin içine TAŞIMA.
|
||||
export function PaketSatinAl() {
|
||||
return (
|
||||
<Parallax
|
||||
strength={6}
|
||||
@@ -71,44 +79,9 @@ export async function PaketSatinAl() {
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
{u ? (
|
||||
u.hasPaket ? (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">
|
||||
<CheckCircle2 className="size-4" aria-hidden />
|
||||
Paketin aktif —{" "}
|
||||
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
|
||||
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
|
||||
de yoksa boş durum sihirbaza yönlendirir. */}
|
||||
<Link href="/listem?uret=1" className="underline">
|
||||
listeni oluştur
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<SatinAlForm
|
||||
urun="paket"
|
||||
label={`${tl(URUNLER.paket.amountKurus)} TL — Paketi al`}
|
||||
vurgulu
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
asChild
|
||||
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link
|
||||
href={`/giris?callback=${encodeURIComponent("/paket")}`}
|
||||
>
|
||||
Giriş yap — sonra {tl(URUNLER.paket.amountKurus)} TL'ye
|
||||
al
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-center text-xs text-slate-500">
|
||||
Girişte {DENEME_KREDISI} deneme kredisi de tanımlanır.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<Skeleton className="h-12 w-full rounded-full" />}>
|
||||
<PaketCta />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Image
|
||||
src="/odeme/iyzico-logo-band.svg"
|
||||
@@ -139,47 +112,95 @@ export async function PaketSatinAl() {
|
||||
{tl(URUNLER.topup.amountKurus)} TL.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
{/* Top-up hasPaket vermez: paketsiz biri alırsa parasını verip
|
||||
yine maskeli liste görür. Satın alma yalnızca paketliye
|
||||
açık; paketsize durum dürüstçe açıklanır. */}
|
||||
{u?.hasPaket ? (
|
||||
<SatinAlForm
|
||||
urun="topup"
|
||||
label={`+${URUNLER.topup.credits} kredi — ${tl(URUNLER.topup.amountKurus)} TL`}
|
||||
/>
|
||||
) : u ? (
|
||||
<p className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs leading-5 text-slate-600">
|
||||
Ek kredi, kilitli analizleri açmaz — o yüzden önce paket
|
||||
gerekir. Paket zaten {URUNLER.paket.credits} kredi içerir;
|
||||
bitince buradan yükleyebilirsin.
|
||||
</p>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer"
|
||||
>
|
||||
<Link
|
||||
href={`/giris?callback=${encodeURIComponent("/paket")}`}
|
||||
>
|
||||
Önce giriş yap
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Suspense fallback={<Skeleton className="h-11 w-full rounded-full" />}>
|
||||
<TopupCta />
|
||||
</Suspense>
|
||||
</div>
|
||||
</section>
|
||||
</Parallax>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaketSatinAlSkeleton() {
|
||||
/** Ana paket kartının kullanıcıya bağlı CTA'sı — bilerek küçük (bkz. üstteki not). */
|
||||
async function PaketCta() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
if (!u) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
asChild
|
||||
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href={`/giris?callback=${encodeURIComponent("/paket")}`}>
|
||||
Giriş yap — ilk {ACIK_SATIR} tercihini ücretsiz gör
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-center text-xs text-slate-500">
|
||||
Girişte {DENEME_KREDISI} deneme kredisi tanımlanır; liste{" "}
|
||||
{RAPOR_KREDI} kredi, ilk {ACIK_SATIR} satır açık. Paketi sonra
|
||||
alırsın.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (u.hasPaket) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">
|
||||
<CheckCircle2 className="size-4" aria-hidden />
|
||||
Paketin aktif —{" "}
|
||||
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
|
||||
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
|
||||
de yoksa boş durum sihirbaza yönlendirir. */}
|
||||
<Link href="/listem?uret=1" className="underline">
|
||||
listeni oluştur
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]"
|
||||
>
|
||||
<Skeleton className="h-80 w-full rounded-2xl" />
|
||||
<Skeleton className="h-80 w-full rounded-2xl sm:w-64" />
|
||||
</div>
|
||||
<SatinAlForm
|
||||
urun="paket"
|
||||
label={`${tl(URUNLER.paket.amountKurus)} TL — Paketi al`}
|
||||
vurgulu
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Top-up kartının kullanıcıya bağlı CTA'sı — bilerek küçük (bkz. üstteki not). */
|
||||
async function TopupCta() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
// Top-up hasPaket vermez: paketsiz biri alırsa parasını verip yine maskeli
|
||||
// liste görür. Satın alma yalnızca paketliye açık; paketsize durum dürüstçe
|
||||
// açıklanır.
|
||||
if (u?.hasPaket) {
|
||||
return (
|
||||
<SatinAlForm
|
||||
urun="topup"
|
||||
label={`+${URUNLER.topup.credits} kredi — ${tl(URUNLER.topup.amountKurus)} TL`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (u) {
|
||||
return (
|
||||
<p className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs leading-5 text-slate-600">
|
||||
Ek kredi, kilitli analizleri açmaz — o yüzden önce paket gerekir.
|
||||
Paket zaten {URUNLER.paket.credits} kredi içerir; bitince buradan
|
||||
yükleyebilirsin.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild variant="outline" className="h-11 w-full cursor-pointer">
|
||||
<Link href={`/giris?callback=${encodeURIComponent("/paket")}`}>
|
||||
Önce giriş yap
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,17 @@
|
||||
// (bkz. tercih-farki.ts). Tekrar üretim riski yok: rapor varken bu bileşen
|
||||
// hiç render edilmez (bkz. listem-icerik.tsx), yani ?uret=1 ile dönmek ya da
|
||||
// sayfayı yenilemek ikinci bir harcama başlatmaz.
|
||||
//
|
||||
// Başarıdan sonra listeye geçiş (21 Eyl 2026 "hazırlanıyor'da kalıyor" hatası):
|
||||
// eskiden `history.replaceState` + `router.refresh()` art arda çağrılıyordu.
|
||||
// Server action'ın `await`i, Next'in aksiyon kuyruğu o action'ı henüz
|
||||
// "bekleyen" sayarken devam ediyor; replaceState'in ürettiği RESTORE öne
|
||||
// geçip action'ı "discarded" işaretliyor ama kuyruğun `last` işaretçisini
|
||||
// güncellemiyor, ardından gelen REFRESH o atılmış düğümün arkasına bağlanıp
|
||||
// HİÇ çalışmıyordu (bkz. next/dist/client/components/app-router-instance.js,
|
||||
// dispatchAction). Sonuç: URL /listem oluyor, ekran iskelette kalıyordu.
|
||||
// Kural: action cevabının hemen ardından yönlendiriciye TEK aksiyon gönder
|
||||
// (router.replace); ikinci aksiyon (refresh) ancak o gezinme bitince.
|
||||
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
@@ -17,7 +28,7 @@ import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Lock, RefreshCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { listeOlustur } from "@/features/rapor/rapor-actions";
|
||||
import { listeOlustur, type ListeSonuc } from "@/features/rapor/rapor-actions";
|
||||
import { olay, siraKovasi } from "@/lib/analitik";
|
||||
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||
@@ -25,8 +36,20 @@ import { Message } from "@/components/ui/message";
|
||||
import { DanismanAvatar, DanismanBaslik } from "./sohbet-client";
|
||||
import { useUretimMesaji } from "./uretim-mesaji";
|
||||
|
||||
// Başarıdan sonra yönlendiricinin listeye geçmesi için tanınan süre; dolduğunda
|
||||
// bileşen hâlâ ekrandaysa tam sayfa yüklemeyle geçilir (emniyet supabı).
|
||||
const GECIS_SINIRI_MS = 6000;
|
||||
// Tek Yapay Zeka çağrısının sunucu sınırı 60 sn (bkz. lib/ai/cagri.ts), tipik
|
||||
// üretim ~20 sn. Bu eşikten sonra kullanıcıya "hâlâ hazırlanıyor" denir; istek
|
||||
// kesilmez, cevap gelirse akış normal devam eder.
|
||||
const UZUN_SURE_MS = 90_000;
|
||||
|
||||
type Durum =
|
||||
| { ad: "uretiliyor" }
|
||||
// İstek hâlâ yolda ama beklenenden uzun sürdü: iskelet + bilgi notu
|
||||
| { ad: "uzun-surdu" }
|
||||
// Liste kuruldu; yönlendiricinin /listem'e geçmesi bekleniyor
|
||||
| { ad: "geciliyor" }
|
||||
| { ad: "kredi-yetersiz" }
|
||||
| { ad: "hata"; mesaj: string }
|
||||
| { ad: "profil-yok" };
|
||||
@@ -57,45 +80,85 @@ export function ListeUretici({
|
||||
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
||||
setDurum({ ad: "uretiliyor" });
|
||||
|
||||
// try yalnızca isteği sarar: cevap geldikten SONRAKİ bir istisna "hata"
|
||||
// ekranına düşerse "Tekrar dene" taze requestId ile ikinci kez kredi
|
||||
// harcatırdı. Cevap kaybolursa requestId korunur → retry idempotent.
|
||||
let sonuc: ListeSonuc;
|
||||
try {
|
||||
const sonuc = await listeOlustur({
|
||||
sonuc = await listeOlustur({
|
||||
sira: profil.sira,
|
||||
tur: profil.tur,
|
||||
secimler: profil.secimler,
|
||||
requestId: requestIdRef.current,
|
||||
});
|
||||
if (sonuc.ok) {
|
||||
requestIdRef.current = null;
|
||||
olay("liste_olusturuldu", {
|
||||
tur: profil.tur,
|
||||
sira_kovasi: siraKovasi(profil.sira),
|
||||
});
|
||||
// ?uret=1 URL'de kalmasın: geri dönüşte anlamsız bayrak taşınmasın.
|
||||
// refresh sunucudaki raporu (ve header'daki kredi pill'ini) ekrana
|
||||
// getirir.
|
||||
window.history.replaceState(null, "", "/listem");
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
requestIdRef.current = null;
|
||||
if (sonuc.code === "KREDI" || sonuc.code === "PAKET") {
|
||||
olay("kredi_bitti");
|
||||
setDurum({ ad: "kredi-yetersiz" });
|
||||
} else if (sonuc.code === "AUTH") {
|
||||
router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`);
|
||||
} else {
|
||||
olay("liste_uretim_hata", { kod: sonuc.code });
|
||||
setDurum({ ad: "hata", mesaj: sonuc.error });
|
||||
}
|
||||
} catch {
|
||||
olay("liste_uretim_hata", { kod: "ISTISNA" });
|
||||
setDurum({
|
||||
ad: "hata",
|
||||
mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
requestIdRef.current = null;
|
||||
if (sonuc.ok) {
|
||||
olay("liste_olusturuldu", {
|
||||
tur: profil.tur,
|
||||
sira_kovasi: siraKovasi(profil.sira),
|
||||
});
|
||||
setDurum({ ad: "geciliyor" });
|
||||
// Next'in kendi gezinmesi: URL (?uret=1 düşer) ve sayfa ağacı birlikte
|
||||
// güncellenir. Burada ikinci bir yönlendirici aksiyonu (refresh) YOK —
|
||||
// neden: dosya başındaki not. Gezinme başlatılamazsa tam yükleme.
|
||||
try {
|
||||
router.replace("/listem");
|
||||
} catch {
|
||||
window.location.replace("/listem");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sonuc.code === "KREDI" || sonuc.code === "PAKET") {
|
||||
olay("kredi_bitti");
|
||||
setDurum({ ad: "kredi-yetersiz" });
|
||||
} else if (sonuc.code === "AUTH") {
|
||||
router.push(`/giris?callback=${encodeURIComponent("/listem?uret=1")}`);
|
||||
} else {
|
||||
olay("liste_uretim_hata", { kod: sonuc.code });
|
||||
setDurum({ ad: "hata", mesaj: sonuc.error });
|
||||
}
|
||||
}
|
||||
|
||||
// Emniyet supabı: liste kuruldu ama yönlendirici ekranı listeye çevirmediyse
|
||||
// (bileşen hâlâ ekranda) tam sayfa yüklemeyle geç. replace: geri tuşu
|
||||
// ?uret=1'e dönüp üretimi yeniden başlatmasın. Ayrı olay adı OlayAdi
|
||||
// birliğini (lib/analitik.ts) genişletmeyi gerektirdiğinden supap,
|
||||
// liste_uretim_hata'nın `kod: "GECIS_ZORLANDI"` değeriyle ayırt edilir.
|
||||
useEffect(() => {
|
||||
if (durum.ad !== "geciliyor") return;
|
||||
const zamanlayici = setTimeout(() => {
|
||||
olay("liste_uretim_hata", { kod: "GECIS_ZORLANDI" });
|
||||
window.location.replace("/listem");
|
||||
}, GECIS_SINIRI_MS);
|
||||
return () => {
|
||||
clearTimeout(zamanlayici);
|
||||
// "geciliyor" son durumdur; bu temizlik yalnızca bileşen ekrandan
|
||||
// kalkarken, yani gezinme işlendikten sonra koşar — kuyruk boşken
|
||||
// gönderilen refresh güvenle çalışır ve layout'taki kredi pill'ini
|
||||
// (gezinme layout verisini yenilemez) günceller.
|
||||
router.refresh();
|
||||
};
|
||||
}, [durum.ad, router]);
|
||||
|
||||
// İstek çok uzarsa kullanıcıyı bilgilendir. İstek iptal edilmez: cevap
|
||||
// sonradan gelirse yukarıdaki akış durumu zaten ezer.
|
||||
useEffect(() => {
|
||||
if (durum.ad !== "uretiliyor") return;
|
||||
const zamanlayici = setTimeout(() => {
|
||||
setDurum((d) => (d.ad === "uretiliyor" ? { ad: "uzun-surdu" } : d));
|
||||
}, UZUN_SURE_MS);
|
||||
return () => clearTimeout(zamanlayici);
|
||||
}, [durum.ad]);
|
||||
|
||||
useEffect(() => {
|
||||
if (basladi.current) return;
|
||||
basladi.current = true;
|
||||
@@ -170,10 +233,10 @@ export function ListeUretici({
|
||||
);
|
||||
}
|
||||
|
||||
return <UretimGovdesi />;
|
||||
return <UretimGovdesi uzunSurdu={durum.ad === "uzun-surdu"} />;
|
||||
}
|
||||
|
||||
function UretimGovdesi() {
|
||||
function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) {
|
||||
const mesaj = useUretimMesaji();
|
||||
|
||||
return (
|
||||
@@ -182,9 +245,32 @@ function UretimGovdesi() {
|
||||
<h1 className="font-heading text-xl font-bold sm:text-2xl">
|
||||
Listen hazırlanıyor
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400">
|
||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||
</p>
|
||||
{uzunSurdu ? (
|
||||
<>
|
||||
<p className="max-w-md text-sm text-slate-600">
|
||||
Bu sefer beklenenden uzun sürdü, listen hâlâ hazırlanıyor. Bu
|
||||
sayfada bekleyebilir ya da hazır olup olmadığına bakabilirsin;
|
||||
bakmak kredi harcamaz.
|
||||
</p>
|
||||
{/* ?uret=1'siz /listem üretimi asla başlatmaz: rapor hazırsa
|
||||
liste, değilse boş durum gelir — kredi ikinci kez yanmaz.
|
||||
Tam yükleme: yönlendirici kuyruğu bekleyen action'ın arkasında
|
||||
tıkalı olduğundan router.refresh burada işe yaramaz. replace:
|
||||
geri tuşu ?uret=1'e dönüp üretimi yeniden başlatmasın. */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.replace("/listem")}
|
||||
className="mt-2 cursor-pointer"
|
||||
>
|
||||
Listemi kontrol et
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400">
|
||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,
|
||||
|
||||
@@ -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<HTMLElement>("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<HTMLElement>(
|
||||
"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 = (
|
||||
<form
|
||||
onSubmit={siraGonder}
|
||||
className={
|
||||
kompakt
|
||||
? "flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center"
|
||||
: "flex flex-col gap-3"
|
||||
}
|
||||
>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className={cn("flex flex-wrap gap-1.5", kompakt && "lg:shrink-0")}
|
||||
>
|
||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
||||
<button
|
||||
key={t.deger}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={tur === t.deger}
|
||||
onClick={() => setTur(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 transition-colors duration-200"
|
||||
: "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 hover:bg-slate-50"
|
||||
}
|
||||
>
|
||||
{t.etiket}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label htmlFor={alanId} className="sr-only">
|
||||
YKS başarı sıralaması
|
||||
</label>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-3 sm:flex-row",
|
||||
kompakt && "lg:min-w-0 lg:flex-1",
|
||||
)}
|
||||
>
|
||||
<div className="relative sm:flex-1">
|
||||
<Trophy
|
||||
className="pointer-events-none absolute left-4 top-1/2 size-4 -translate-y-1/2 text-slate-400"
|
||||
aria-hidden
|
||||
/>
|
||||
<Input
|
||||
id={alanId}
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
placeholder="YKS başarı sıralaman (ör. 85.000)"
|
||||
value={siralama}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={yukleniyor}
|
||||
className="h-12 cursor-pointer bg-orange-500 px-6 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
{yukleniyor ? "Hazırlanıyor…" : "Programları gör"}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
{hata ? (
|
||||
<p
|
||||
id={hataId}
|
||||
role="alert"
|
||||
className={cn(
|
||||
"text-sm font-medium text-red-600",
|
||||
kompakt && "lg:basis-full",
|
||||
)}
|
||||
>
|
||||
{hata}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
|
||||
// Ç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 = (
|
||||
<Drawer open={acik} onOpenChange={acikDegisti} handleOnly>
|
||||
<DrawerContent
|
||||
className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
|
||||
>
|
||||
{/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */}
|
||||
<DrawerTitle className="sr-only">
|
||||
Sıralamana yakın programlar hangileri?
|
||||
</DrawerTitle>
|
||||
<DrawerDescription className="sr-only">
|
||||
YKS başarı sıralamanı gir; sıralamana uygun programları bu sayfadan
|
||||
ayrılmadan gör.
|
||||
</DrawerDescription>
|
||||
<DrawerClose className="absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]">
|
||||
<X className="size-4" aria-hidden />
|
||||
<span className="sr-only">Kapat</span>
|
||||
</DrawerClose>
|
||||
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6"
|
||||
>
|
||||
{!kompakt && adim === "sira" ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<SectionEyebrow>Saniyede gör</SectionEyebrow>
|
||||
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900">
|
||||
Sıralamana yakın programlar hangileri?
|
||||
</h2>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-600">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{siraFormu}
|
||||
|
||||
<ul
|
||||
aria-label="Neler alacaksın"
|
||||
className="flex flex-wrap gap-x-4 gap-y-2 border-t border-slate-100 pt-4"
|
||||
>
|
||||
{KANITLAR.map(({ ikon: Ikon, etiket }) => (
|
||||
<li
|
||||
key={etiket}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-600"
|
||||
>
|
||||
<Ikon className="size-3.5 text-primary" aria-hidden />
|
||||
{etiket}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : facetler && sira != null ? (
|
||||
<SihirbazAdimlarLazy
|
||||
key={`${sira}-${tur}`}
|
||||
sira={sira}
|
||||
tur={tur}
|
||||
facetler={facetler}
|
||||
sonButonEtiketi={
|
||||
girisli ? "Yapay Zeka listemi kur (3 kredi)" : "Ücretsiz tablonu gör"
|
||||
}
|
||||
onTamamla={secimleriTamamla}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
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 (
|
||||
<section
|
||||
aria-labelledby={baslikId}
|
||||
className="rounded-2xl border border-slate-200 bg-white p-4 sm:p-5"
|
||||
>
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
<strong
|
||||
id={baslikId}
|
||||
className="font-heading text-base font-bold text-slate-900"
|
||||
>
|
||||
{baslik ?? "Sıralaman bu tabanların neresinde?"}
|
||||
</strong>{" "}
|
||||
Ücretsiz, üyelik gerekmez.
|
||||
</p>
|
||||
<div className="mt-3 grid">
|
||||
<div
|
||||
ref={profil ? undefined : formRef}
|
||||
inert={profil ? true : undefined}
|
||||
className={cn("col-start-1 row-start-1", profil && "invisible")}
|
||||
>
|
||||
{siraFormu}
|
||||
</div>
|
||||
{profil ? (
|
||||
<div className="col-start-1 row-start-1 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-sm leading-6 text-slate-600">
|
||||
Sıralaman kayıtlı ({profil.sira.toLocaleString("tr-TR")}. sıra).
|
||||
</p>
|
||||
<Link
|
||||
href={sonucHref()}
|
||||
className="inline-flex h-11 items-center justify-center gap-2 rounded-full bg-orange-500 px-6 text-sm font-semibold text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
Sıralamana uygun programları gör
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{cekmece}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (profil) {
|
||||
return (
|
||||
<section className="rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8">
|
||||
@@ -198,138 +433,7 @@ export function CtaSiraForm({
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
|
||||
<Drawer open={acik} onOpenChange={acikDegisti} handleOnly>
|
||||
<DrawerContent
|
||||
className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
|
||||
>
|
||||
{/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */}
|
||||
<DrawerTitle className="sr-only">
|
||||
Sıralamana yakın programlar hangileri?
|
||||
</DrawerTitle>
|
||||
<DrawerDescription className="sr-only">
|
||||
YKS başarı sıralamanı gir; sıralamana uygun programları bu sayfadan
|
||||
ayrılmadan gör.
|
||||
</DrawerDescription>
|
||||
<DrawerClose className="absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]">
|
||||
<X className="size-4" aria-hidden />
|
||||
<span className="sr-only">Kapat</span>
|
||||
</DrawerClose>
|
||||
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6"
|
||||
>
|
||||
{adim === "sira" ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<SectionEyebrow>Saniyede gör</SectionEyebrow>
|
||||
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900">
|
||||
Sıralamana yakın programlar hangileri?
|
||||
</h2>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-600">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={siraGonder} className="flex flex-col gap-3">
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className="flex flex-wrap gap-1.5"
|
||||
>
|
||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
||||
<button
|
||||
key={t.deger}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={tur === t.deger}
|
||||
onClick={() => setTur(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 transition-colors duration-200"
|
||||
: "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 hover:bg-slate-50"
|
||||
}
|
||||
>
|
||||
{t.etiket}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label htmlFor="cta-siralama" className="sr-only">
|
||||
YKS başarı sıralaması
|
||||
</label>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<div className="relative sm:flex-1">
|
||||
<Trophy
|
||||
className="pointer-events-none absolute left-4 top-1/2 size-4 -translate-y-1/2 text-slate-400"
|
||||
aria-hidden
|
||||
/>
|
||||
<Input
|
||||
id="cta-siralama"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
placeholder="YKS başarı sıralaman (ör. 85.000)"
|
||||
value={siralama}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={yukleniyor}
|
||||
className="h-12 cursor-pointer bg-orange-500 px-6 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
{yukleniyor ? "Hazırlanıyor…" : "Programları gör"}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
{hata ? (
|
||||
<p
|
||||
id="cta-siralama-hata"
|
||||
role="alert"
|
||||
className="text-sm font-medium text-red-600"
|
||||
>
|
||||
{hata}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
|
||||
<ul
|
||||
aria-label="Neler alacaksın"
|
||||
className="flex flex-wrap gap-x-4 gap-y-2 border-t border-slate-100 pt-4"
|
||||
>
|
||||
{KANITLAR.map(({ ikon: Ikon, etiket }) => (
|
||||
<li
|
||||
key={etiket}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-600"
|
||||
>
|
||||
<Ikon className="size-3.5 text-primary" aria-hidden />
|
||||
{etiket}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : facetler && sira != null ? (
|
||||
<SihirbazAdimlarLazy
|
||||
key={`${sira}-${tur}`}
|
||||
sira={sira}
|
||||
tur={tur}
|
||||
facetler={facetler}
|
||||
sonButonEtiketi={
|
||||
girisli ? "Yapay Zeka listemi kur (3 kredi)" : "Ücretsiz tablonu gör"
|
||||
}
|
||||
onTamamla={secimleriTamamla}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{cekmece}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
249
src/lib/kapanan-sluglar.ts
Normal file
249
src/lib/kapanan-sluglar.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Kapanan katalog sayfaları → yaşayan en yakın sayfa (kalıcı yönlendirme).
|
||||
*
|
||||
* Neden var: yıllık veri güncellemesinde son kılavuzda olmayan program satırları
|
||||
* `programs_arsiv`'e taşınır; bir bölüm adının ya da üniversitenin bütün
|
||||
* satırları giderse /bolum/<slug> ya da /universite/<slug> sayfası tamamen
|
||||
* kapanır. Bu URL'ler arama sonuçlarında ve dış linklerde yaşamaya devam eder;
|
||||
* "sayfa yok" göstermek yerine adı/alanı en yakın yaşayan sayfaya 308 verilir.
|
||||
* Tek tüketici `next.config.ts` içindeki `redirects()`'tir.
|
||||
*
|
||||
* Yıllık güncellemede (docs/veri/yillik-guncelleme.md, 5. adım):
|
||||
* `pnpm eski-arsivle --kuru` çıktısındaki "tamamen kapanan" slug'lar buraya
|
||||
* eklenir. Hedef seçimi sırasıyla: (1) program/kurum ad değiştirdiyse yeni adı
|
||||
* (kanıt: aynı üniversitede geçmiş yıl sıraları birebir taşınmış satır, ya da
|
||||
* resmî karar), (2) aynı alanın lisans/önlisans karşılığı, (3) emin
|
||||
* olunamıyorsa hub (`/bolumler`, `/universiteler`). Uydurma eşleme yapılmaz.
|
||||
*
|
||||
* Kurallar:
|
||||
* - `hedef` yaşayan bir sayfa olmalı (slug `programs` tablosundan türemeli).
|
||||
* - `hedef` başka bir kaydın `kaynak`'ı olamaz (zincir/döngü) —
|
||||
* `kapananYonlendirmeleri()` bunu build'de sınar ve ihlâlde durur.
|
||||
* - Kapanan bir sayfa ileride yeniden açılırsa (program kılavuza dönerse)
|
||||
* kaydı SİL; yoksa yönlendirme dosya sisteminden önce çalıştığı için yeni
|
||||
* sayfa hiç görünmez.
|
||||
*
|
||||
* Bu dosya `next.config.ts`'ten, uygulama paketinin dışında yüklenir: bağımlılıksız
|
||||
* kalmalı — `server-only`, DB ya da başka bir uygulama modülü import ETME.
|
||||
*/
|
||||
|
||||
export type KapananSlug = {
|
||||
/** Kapanan sayfanın yolu, sonda eğik çizgi yok: `/bolum/<slug>` | `/universite/<slug>` */
|
||||
kaynak: `/bolum/${string}` | `/universite/${string}`;
|
||||
/** Yaşayan hedef sayfa ya da hub */
|
||||
hedef: `/bolum/${string}` | `/universite/${string}` | "/bolumler" | "/universiteler";
|
||||
/** Hedefin neden seçildiği (kanıt türüyle) */
|
||||
gerekce: string;
|
||||
};
|
||||
|
||||
/** 2026 veri yayını (21 Eylül 2026): 28 bölüm + 2 üniversite sayfası kapandı. */
|
||||
export const KAPANAN_SLUGLAR: readonly KapananSlug[] = [
|
||||
// ---- Bölümler: ad değişikliği veriyle kanıtlı (geçmiş sıralar yeni ada taşınmış) ----
|
||||
{
|
||||
kaynak: "/bolum/radyo-ve-televizyon-programciligi",
|
||||
hedef: "/bolum/medya-ve-iletisim",
|
||||
gerekce:
|
||||
"Önlisans programı 2024'ten sonra kılavuzda yok; İzmir Ekonomi Ü. MYO'da geçmiş sıralar Medya ve İletişim'e taşınmış (ardıl ad), hedefte 21 önlisans programı var",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/iletisim-sanatlari",
|
||||
hedef: "/bolum/halkla-iliskiler-ve-reklamcilik",
|
||||
gerekce:
|
||||
"Tek kurum (İstanbul Kültür Ü. Sanat ve Tasarım Fak.); 3 satırın 2023 sıraları aynı fakültedeki Halkla İlişkiler ve Reklamcılık satırlarına taşınmış",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/cevre-temizligi-ve-denetimi",
|
||||
hedef: "/bolum/cevresel-olcum-ve-izleme-sistemleri-teknikerligi",
|
||||
gerekce:
|
||||
"Tek kurum (Kocaeli Ü. Hereke Asım Kocabıyık MYO); geçmiş sırası aynı MYO'daki yeni adlı programa taşınmış",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/atik-yonetimi",
|
||||
hedef: "/bolum/su-ve-atik-yonetimi-teknikerligi",
|
||||
gerekce:
|
||||
"Tek kurum (Karamanoğlu Mehmetbey Ü.); geçmiş sırası aynı üniversitedeki Su ve Atık Yönetimi Teknikerliği'ne taşınmış",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/emlak-ve-emlak-yonetimi",
|
||||
hedef: "/bolum/gayrimenkul-gelistirme-ve-yonetimi",
|
||||
gerekce:
|
||||
"Tek kurum (Ankara Hacı Bayram Veli Ü. Tapu Kadastro YO); geçmiş sırası aynı okuldaki Gayrimenkul Geliştirme ve Yönetimi'ne taşınmış",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/meteoroloji-muhendisligi",
|
||||
hedef: "/bolum/iklim-bilimi-ve-meteoroloji-muhendisligi",
|
||||
gerekce:
|
||||
"Tek kurum (Samsun Ü.); program aynı fakültede İklim Bilimi ve Meteoroloji Mühendisliği adıyla sürüyor",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/endustri-yonetimi-muhendisligi",
|
||||
hedef: "/bolum/endustri-muhendisligi",
|
||||
gerekce:
|
||||
"Tek kurum (Uluslararası Balkan Ü.); aynı program kimlikleri 2026 kılavuzunda Endüstri Mühendisliği adıyla",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/taki-tasarimi",
|
||||
hedef: "/bolum/kuyumculuk-ve-mucevher-tasarimi",
|
||||
gerekce:
|
||||
"Lisans; Altınbaş Ü. Uygulamalı Bilimler Fak.'nde Takı Tasarımı 2024'te bitti, aynı fakültede 2025'ten itibaren Kuyumculuk ve Mücevher Tasarımı var",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/islam-bilimleri",
|
||||
hedef: "/bolum/ilahiyat",
|
||||
gerekce:
|
||||
"Sayfanın tek satırı (Kırgızistan-Türkiye Manas Ü. İlahiyat Fak., kimlik 405210389) 2026 kılavuzunda İlahiyat adıyla",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/bati-dilleri",
|
||||
hedef: "/bolum/filoloji",
|
||||
gerekce:
|
||||
"Tek kurum (Manas Ü. Edebiyat Fak.); 2024'te kapandı, aynı fakültede 2025'ten itibaren Filoloji (İngiliz Dili ve Edebiyatı) açıldı",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/dogu-dilleri",
|
||||
hedef: "/bolum/filoloji",
|
||||
gerekce:
|
||||
"Tek kurum (Manas Ü. Edebiyat Fak.); Batı Dilleri ile aynı yeniden yapılanma, fakültenin dil programları Filoloji altında",
|
||||
},
|
||||
|
||||
// ---- Bölümler: aynı alanın lisans karşılığı / ad yakınlığı ----
|
||||
{
|
||||
kaynak: "/bolum/uygulamali-ingilizce-cevirmenlik",
|
||||
hedef: "/bolum/ingilizce-mutercim-ve-tercumanlik",
|
||||
gerekce:
|
||||
"Önlisans programı 2024'ten sonra kılavuzda yok, önlisansta çeviri programı kalmadı; aynı alanın lisans karşılığı",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/uygulamali-ispanyolca-cevirmenlik",
|
||||
hedef: "/bolum/ispanyolca-mutercim-ve-tercumanlik",
|
||||
gerekce: "Önlisans programı kapandı; aynı alanın lisans karşılığı",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/menkul-kiymetler-ve-sermaye-piyasasi",
|
||||
hedef: "/bolum/sermaye-piyasasi",
|
||||
gerekce: "Önlisans programı kapandı; aynı alanın lisans karşılığı",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/siyasal-bilimler",
|
||||
hedef: "/bolum/siyaset-bilimi",
|
||||
gerekce: "Tek kurum (Uluslararası Balkan Ü.); eş anlamlı ad",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/uluslararasi-ekonomi",
|
||||
hedef: "/bolum/ekonomi",
|
||||
gerekce: "Tek kurum (Uluslararası Balkan Ü.); adı en yakın yaşayan bölüm",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/girisimcilik",
|
||||
hedef: "/bolum/uluslararasi-girisimcilik",
|
||||
gerekce: "Tek kurum (Özyeğin Ü.); adı en yakın yaşayan bölüm",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/film-tasarimi-ve-yazarligi",
|
||||
hedef: "/bolum/film-tasarimi-ve-yonetimi",
|
||||
gerekce: "Tek kurum (Dokuz Eylül Ü. GSF); aynı alan, adı en yakın yaşayan bölüm",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/film-tasarimi-ve-yonetmenligi",
|
||||
hedef: "/bolum/film-tasarimi-ve-yonetimi",
|
||||
gerekce: "Tek kurum (Dokuz Eylül Ü. GSF); aynı alan, adı en yakın yaşayan bölüm",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/muhendislik-programlari",
|
||||
hedef: "/bolum/muhendislik-ve-doga-bilimleri-programlari",
|
||||
gerekce:
|
||||
"TED Ü. bölümsüz mühendislik girişi kapandı; aynı türden (fakülteye giriş, bölüm sonradan) yaşayan tek sayfa",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/iktisadi-ve-idari-bilimler-programlari",
|
||||
hedef: "/bolum/yonetim-bilimleri-programlari",
|
||||
gerekce:
|
||||
"TED Ü. bölümsüz İİBF girişi kapandı; aynı türden (fakülteye giriş, bölüm sonradan) en yakın yaşayan sayfa",
|
||||
},
|
||||
|
||||
// ---- Bölümler: alan yakınlığı — ardıl ad kanıtı YOK, Bilal onayı bekliyor ----
|
||||
{
|
||||
kaynak: "/bolum/spor-yoneticiligi",
|
||||
hedef: "/bolum/rekreasyon",
|
||||
gerekce:
|
||||
"113 satırın tamamı 2026 kılavuzundan çıktı, ardıl ad yok; spor bilimleri fakültelerinde merkezi yerleştirmeyle öğrenci alan kardeş bölüm (Rekreasyon Yönetimi turizm fakültelerinde, o yüzden seçilmedi)",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/spor-yonetimi",
|
||||
hedef: "/bolum/rekreasyon",
|
||||
gerekce:
|
||||
"Önlisans programı 2024'ten sonra kılavuzda yok, ardıl ad yok; Spor Yöneticiliği ile aynı hedef",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/bilgi-yonetimi",
|
||||
hedef: "/bolum/bilgi-ve-belge-yonetimi",
|
||||
gerekce:
|
||||
"Önlisans programı 2024'ten sonra kılavuzda yok, ardıl ad yok; adı ve alanı en yakın yaşayan (lisans) bölüm",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/ambalaj-tasarimi",
|
||||
hedef: "/bolum/grafik-tasarimi",
|
||||
gerekce:
|
||||
"Tek kurum (Fatih Sultan Mehmet Vakıf Ü. MYO), ardıl ad yok; ambalaj tasarımı grafik tasarımın alt dalı, hedefte 119 önlisans programı var",
|
||||
},
|
||||
|
||||
// ---- Bölümler: yakın sayfa yok → hub ----
|
||||
{
|
||||
kaynak: "/bolum/nufus-ve-vatandaslik",
|
||||
hedef: "/bolumler",
|
||||
gerekce: "Önlisans programı kapandı; ardıl ad ya da alanı yakın yaşayan bölüm yok",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/aile-ve-tuketici-bilimleri",
|
||||
hedef: "/bolumler",
|
||||
gerekce: "Tek kurum (Hacettepe Ü.); alanı yakın yaşayan bölüm yok",
|
||||
},
|
||||
{
|
||||
kaynak: "/bolum/ev-idaresi",
|
||||
hedef: "/bolumler",
|
||||
gerekce: "Tek kurum (Anadolu Ü. AÖF); alanı yakın yaşayan bölüm yok",
|
||||
},
|
||||
|
||||
// ---- Üniversiteler: kurum ad değiştirdi ----
|
||||
{
|
||||
kaynak: "/universite/izmir-kavram-meslek-yuksekokulu",
|
||||
hedef: "/universite/izmir-konak-meslek-yuksekokulu",
|
||||
gerekce:
|
||||
"Ad değişikliği: 10929 sayılı Cumhurbaşkanı Kararı, Resmî Gazete 31.01.2026 / 33154; 2026 kılavuzunda yeni adla 72 program",
|
||||
},
|
||||
{
|
||||
kaynak: "/universite/kibris-ilim-universitesi",
|
||||
hedef: "/universite/kibris-aydin-universitesi",
|
||||
gerekce:
|
||||
"Ad değişikliği (Mart 2025); veride Kıbrıs Aydın Ü. satırları Kıbrıs İlim Ü.'nün aynı programlarını ve 2024 sıralarını birebir taşıyor",
|
||||
},
|
||||
];
|
||||
|
||||
export type KaliciYonlendirme = {
|
||||
source: string;
|
||||
destination: string;
|
||||
permanent: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* `next.config.ts` → `redirects()` için kurallar. `:yol*` sıfır ya da daha çok
|
||||
* parça eşler: sayfanın kendisi, `/sayfa/N` sayfalama URL'leri ve diğer alt
|
||||
* yollar hedefin KÖK sayfasına gider (hedefin sayfa sayısı farklıdır, N taşınmaz).
|
||||
*/
|
||||
export function kapananYonlendirmeleri(): KaliciYonlendirme[] {
|
||||
const kaynaklar = new Set<string>();
|
||||
for (const { kaynak } of KAPANAN_SLUGLAR) {
|
||||
if (kaynaklar.has(kaynak)) {
|
||||
throw new Error(`kapanan-sluglar: "${kaynak}" iki kez tanımlı`);
|
||||
}
|
||||
kaynaklar.add(kaynak);
|
||||
}
|
||||
return KAPANAN_SLUGLAR.map(({ kaynak, hedef }) => {
|
||||
if (kaynaklar.has(hedef)) {
|
||||
throw new Error(
|
||||
`kapanan-sluglar: "${kaynak}" → "${hedef}" zincir oluşturuyor (hedef de kapanan listesinde)`,
|
||||
);
|
||||
}
|
||||
return { source: `${kaynak}/:yol*`, destination: hedef, permanent: true };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user