fix: update callback handling and user profile management in list components
All checks were successful
Deploy / deploy (push) Successful in 7m23s
All checks were successful
Deploy / deploy (push) Successful in 7m23s
Enhanced the handling of user profiles and callback URLs in various components. Updated the `GirisKarti` to support multiple callback scenarios, ensuring seamless user experience. Improved the `ListeUretici` and `ListemIcerik` components to manage profile data more effectively, including handling cases where the profile is stored in cookies. Additionally, refined the logic for redirecting users based on their profile status, ensuring they are directed to the appropriate pages without losing context. Updated the database file to reflect these changes.
This commit is contained in:
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
@@ -22,8 +22,10 @@ export async function GirisKarti({
|
|||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (session) redirect(callbackURL);
|
if (session) redirect(callbackURL);
|
||||||
|
|
||||||
// Sihirbaz funnel'ı: seçimler yapıldı, listeye giriş kaldı
|
// Sihirbaz funnel'ı: seçimler yapıldı, listeye giriş kaldı. /listem?uret=1
|
||||||
const funnel = callbackURL.startsWith("/sonuc");
|
// callback'i de aynı vaadi taşır (girişten dönüşte üretim orada başlar).
|
||||||
|
const funnel =
|
||||||
|
callbackURL.startsWith("/sonuc") || callbackURL.startsWith("/listem?uret=1");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -273,8 +273,15 @@ export function profilTamamlandi(
|
|||||||
// Düzenleme /sonuc'a götürür: tablo profil çerezinden SSR'lanır
|
// 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
|
// (sira/tur/il/tip) ve tercihProfiliYaz çerezi az önce güncelledi. URL
|
||||||
// değişmediği için sunucu tablosunun tazelenmesi kapı bileşenindeki
|
// değişmediği için sunucu tablosunun tazelenmesi kapı bileşenindeki
|
||||||
// router.refresh ile garanti edilir.
|
// router.refresh ile garanti edilir. Tek istisna /listem: kullanıcı
|
||||||
if (duzenle) return sonucHref();
|
// listesinin başındayken tercihlerini düzenliyorsa listesinden koparılmaz,
|
||||||
|
// aynı sayfaya döner ve "tercihlerin değişti" uyarısı orada karşılar.
|
||||||
|
if (duzenle) {
|
||||||
|
return typeof window !== "undefined" &&
|
||||||
|
window.location.pathname === "/listem"
|
||||||
|
? "/listem"
|
||||||
|
: sonucHref();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
// /listem üretim gövdesi: sihirbazın kaydettiği bekleyen seçimlerle Yapay
|
// /listem üretim gövdesi: sihirbazın kaydettiği bekleyen seçimlerle Yapay
|
||||||
// Zeka listesi burada kurulur (sonuç sayfası yalnızca buraya yönlendirir).
|
// Zeka listesi burada kurulur (sonuç sayfası yalnızca buraya yönlendirir).
|
||||||
// Başarıda profil tüketilir ve sayfa yenilenir; kredi-yetersiz ve hata
|
// Kredi-yetersiz ve hata durumları da bu bileşende karşılanır.
|
||||||
// durumları da bu bileşende karşılanır.
|
//
|
||||||
|
// Profil üretimden sonra SİLİNMEZ: kullanıcının tercih profili sitenin geri
|
||||||
|
// kalanının da kaynağı (/sonuc tablo filtresi, navbar sıralama rozeti, "+"
|
||||||
|
// kapısı) ve /listem'deki "tercihlerin değişti" karşılaştırmasının temeli
|
||||||
|
// (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.
|
||||||
|
|
||||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -14,14 +20,8 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { listeOlustur } from "@/features/rapor/rapor-actions";
|
import { listeOlustur } from "@/features/rapor/rapor-actions";
|
||||||
import { olay, siraKovasi } from "@/lib/analitik";
|
import { olay, siraKovasi } from "@/lib/analitik";
|
||||||
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { tercihProfiliOku, tercihProfiliSil } from "@/features/sihirbaz/sihirbaz-profil";
|
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
|
import { useUretimMesaji } from "./uretim-mesaji";
|
||||||
const URETIM_MESAJLARI = [
|
|
||||||
"Sıralamana uygun programlar taranıyor…",
|
|
||||||
"Gerçek YÖK Atlas verisiyle dengeli liste kuruluyor…",
|
|
||||||
"Her tercih için risk ve trend analizi yazılıyor…",
|
|
||||||
"Son rötuşlar yapılıyor, neredeyse hazır…",
|
|
||||||
];
|
|
||||||
|
|
||||||
type Durum =
|
type Durum =
|
||||||
| { ad: "uretiliyor" }
|
| { ad: "uretiliyor" }
|
||||||
@@ -31,9 +31,14 @@ type Durum =
|
|||||||
|
|
||||||
export function ListeUretici({
|
export function ListeUretici({
|
||||||
hasPaket,
|
hasPaket,
|
||||||
|
sunucuProfil = null,
|
||||||
bosDurum,
|
bosDurum,
|
||||||
}: {
|
}: {
|
||||||
hasPaket: boolean;
|
hasPaket: boolean;
|
||||||
|
/** Profil çerezinden SSR'da çözülen yedek profil: localStorage silinmiş ya
|
||||||
|
* da erişilemez olsa bile (gizli sekme, temizlenmiş depo) üretim başlasın.
|
||||||
|
* localStorage yine de birincil kaynak — çerez onun aynası. */
|
||||||
|
sunucuProfil?: TercihProfili | null;
|
||||||
/** Storage'da bekleyen seçim yoksa gösterilecek boş-liste durumu */
|
/** Storage'da bekleyen seçim yoksa gösterilecek boş-liste durumu */
|
||||||
bosDurum: ReactNode;
|
bosDurum: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
@@ -63,10 +68,9 @@ export function ListeUretici({
|
|||||||
tur: profil.tur,
|
tur: profil.tur,
|
||||||
sira_kovasi: siraKovasi(profil.sira),
|
sira_kovasi: siraKovasi(profil.sira),
|
||||||
});
|
});
|
||||||
tercihProfiliSil();
|
// ?uret=1 URL'de kalmasın: geri dönüşte anlamsız bayrak taşınmasın.
|
||||||
// ?uret=1 URL'de kalmasın: yenileme/geri dönüş üretimi tekrar
|
// refresh sunucudaki raporu (ve header'daki kredi pill'ini) ekrana
|
||||||
// tetiklemesin. refresh sunucudaki raporu (ve header'daki kredi
|
// getirir.
|
||||||
// pill'ini) ekrana getirir.
|
|
||||||
window.history.replaceState(null, "", "/listem");
|
window.history.replaceState(null, "", "/listem");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
return;
|
return;
|
||||||
@@ -91,8 +95,9 @@ export function ListeUretici({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (basladi.current) return;
|
if (basladi.current) return;
|
||||||
basladi.current = true;
|
basladi.current = true;
|
||||||
// Bekleyen seçimler yalnızca client storage'da; mount sonrası okunur.
|
// Bekleyen seçimler client storage'da; mount sonrası okunur. Depo boşsa
|
||||||
const kayitli = tercihProfiliOku();
|
// (temizlenmiş/erişilemez) sunucunun çerezden çözdüğü profile düşülür.
|
||||||
|
const kayitli = tercihProfiliOku() ?? sunucuProfil;
|
||||||
if (!kayitli) {
|
if (!kayitli) {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setDurum({ ad: "profil-yok" });
|
setDurum({ ad: "profil-yok" });
|
||||||
@@ -165,14 +170,7 @@ export function ListeUretici({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UretimGovdesi() {
|
function UretimGovdesi() {
|
||||||
const [mesajIdx, setMesajIdx] = useState(0);
|
const mesaj = useUretimMesaji();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setMesajIdx((i) => Math.min(i + 1, URETIM_MESAJLARI.length - 1));
|
|
||||||
}, 7000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -181,9 +179,7 @@ function UretimGovdesi() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-1.5 text-center">
|
<div className="flex flex-col items-center gap-1.5 text-center">
|
||||||
<h1 className="font-heading text-lg font-bold">Listen hazırlanıyor</h1>
|
<h1 className="font-heading text-lg font-bold">Listen hazırlanıyor</h1>
|
||||||
<p className="max-w-sm text-sm text-slate-500">
|
<p className="max-w-sm text-sm text-slate-500">{mesaj}</p>
|
||||||
{URETIM_MESAJLARI[mesajIdx]}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import { ArrowRight } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
||||||
import { sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /listem boş hâlinin metni + CTA'sı: sıralamasını zaten girmiş adaya
|
* /listem boş hâlinin metni + CTA'sı: seçimleri zaten hazır olan adayı
|
||||||
* "sıralamanı gir" demek yerine sihirbazı açık hâlde sonuç sayfasına
|
* /sonuc üzerinden dolaştırmadan doğrudan üretime (?uret=1) alır; profil
|
||||||
* götürür; profil yoksa ana sayfadaki sıralama formuna yönlendirir.
|
* yoksa ana sayfadaki sıralama formuna yönlendirir.
|
||||||
*/
|
*/
|
||||||
export function ListemBosCta() {
|
export function ListemBosCta() {
|
||||||
const profil = useTercihProfili();
|
const profil = useTercihProfili();
|
||||||
@@ -28,7 +27,7 @@ export function ListemBosCta() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="mt-6 cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
className="mt-6 cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
<Link href={profil ? sonucHref({ sihirbaz: "1" }) : "/"}>
|
<Link href={profil ? "/listem?uret=1" : "/"}>
|
||||||
{profil ? "Yapay Zeka listeni kur" : "Sıralamanı gir, Yapay Zeka listeni kur"}
|
{profil ? "Yapay Zeka listeni kur" : "Sıralamanı gir, Yapay Zeka listeni kur"}
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Lock,
|
Lock,
|
||||||
PencilLine,
|
PencilLine,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
SlidersHorizontal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RaporListesi } from "./rapor-listesi";
|
import { RaporListesi } from "./rapor-listesi";
|
||||||
@@ -24,6 +25,7 @@ import { pinleriTuret } from "@/lib/harita-pinler";
|
|||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||||
import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client";
|
import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client";
|
||||||
import { RevizyonKutusu } from "./revizyon-kutusu";
|
import { RevizyonKutusu } from "./revizyon-kutusu";
|
||||||
|
import { profilDuzenlemeyiAc } from "@/features/liste/hooks/use-tercih-profili";
|
||||||
|
|
||||||
export function ListemGovde({
|
export function ListemGovde({
|
||||||
rapor,
|
rapor,
|
||||||
@@ -122,8 +124,22 @@ export function ListemGovde({
|
|||||||
? "Yapay Zeka tercih listen"
|
? "Yapay Zeka tercih listen"
|
||||||
: "Veriyi ücretsiz incele. Kararı Yapay Zeka ile netleştir."}
|
: "Veriyi ücretsiz incele. Kararı Yapay Zeka ile netleştir."}
|
||||||
</h1>
|
</h1>
|
||||||
|
<div className="flex shrink-0 flex-wrap gap-2">
|
||||||
|
{/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte
|
||||||
|
TercihProfiliKapisi) profili günceller, /listem'e döner ve
|
||||||
|
değişiklik varsa "tercihlerin değişti" uyarısı karşılar. */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={profilDuzenlemeyiAc}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="size-4" aria-hidden />
|
||||||
|
Tercihlerimi düzenle
|
||||||
|
</Button>
|
||||||
{hasPaket ? (
|
{hasPaket ? (
|
||||||
<div className="flex shrink-0 gap-2">
|
<>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -146,9 +162,10 @@ export function ListemGovde({
|
|||||||
PDF
|
PDF
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<RaporListesi
|
<RaporListesi
|
||||||
rapor={rapor}
|
rapor={rapor}
|
||||||
adaySira={adaySira}
|
adaySira={adaySira}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { MessageCircleQuestion, Sparkles } from "lucide-react";
|
import { MessageCircleQuestion, Sparkles } from "lucide-react";
|
||||||
import { verifySession, getCurrentUser } from "@/lib/session";
|
import { verifySession, getCurrentUser } from "@/lib/session";
|
||||||
|
import {
|
||||||
|
PROFIL_CEREZ_ADI,
|
||||||
|
cerezdenTercihProfili,
|
||||||
|
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
||||||
import { URUNLER, MAX_REVIZYON } from "@/lib/credits";
|
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
|
||||||
import type {
|
import type {
|
||||||
MaskeliRapor,
|
MaskeliRapor,
|
||||||
RaporParams,
|
RaporParams,
|
||||||
@@ -16,6 +21,7 @@ import type { Mesaj } from "./sohbet-client";
|
|||||||
import { ListemGovde } from "./listem-govde";
|
import { ListemGovde } from "./listem-govde";
|
||||||
import { ListemBosCta } from "./listem-bos-cta";
|
import { ListemBosCta } from "./listem-bos-cta";
|
||||||
import { ListeUretici } from "./liste-uretici";
|
import { ListeUretici } from "./liste-uretici";
|
||||||
|
import { TercihDegistiUyarisi } from "./tercih-degisti-uyarisi";
|
||||||
|
|
||||||
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
@@ -28,9 +34,14 @@ export async function ListemIcerik({
|
|||||||
uret: boolean;
|
uret: boolean;
|
||||||
acildi: boolean;
|
acildi: boolean;
|
||||||
}) {
|
}) {
|
||||||
// Girişsiz gelene callback'i ?uret=1 ile ver: girişten dönüşte bekleyen
|
// Tercihleri girili olan (profil çerezi duran) ziyaretçi girişten dönünce
|
||||||
// seçimlerle üretim kaldığı yerden başlasın.
|
// "Henüz listen yok" boşluğuyla karşılaşmasın: callback ?uret=1 taşır,
|
||||||
const geriYol = uret ? "/listem?uret=1" : "/listem";
|
// üretim dönüşte kendiliğinden başlar. ?uret=1 ile gelen zaten aynı yola
|
||||||
|
// döner. Profil çerezi localStorage'ın aynası (bkz. tercihProfiliYaz).
|
||||||
|
const cerezProfili = cerezdenTercihProfili(
|
||||||
|
(await cookies()).get(PROFIL_CEREZ_ADI)?.value,
|
||||||
|
);
|
||||||
|
const geriYol = uret || cerezProfili ? "/listem?uret=1" : "/listem";
|
||||||
const session = await verifySession(geriYol);
|
const session = await verifySession(geriYol);
|
||||||
const [user, satir, sohbetGecmisi] = await Promise.all([
|
const [user, satir, sohbetGecmisi] = await Promise.all([
|
||||||
getCurrentUser(),
|
getCurrentUser(),
|
||||||
@@ -60,6 +71,7 @@ export async function ListemIcerik({
|
|||||||
{uret ? (
|
{uret ? (
|
||||||
<ListeUretici
|
<ListeUretici
|
||||||
hasPaket={Boolean(user.hasPaket)}
|
hasPaket={Boolean(user.hasPaket)}
|
||||||
|
sunucuProfil={cerezProfili}
|
||||||
bosDurum={bosDurum}
|
bosDurum={bosDurum}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -100,6 +112,16 @@ export async function ListemIcerik({
|
|||||||
PDF artık açık.
|
PDF artık açık.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{/* Tercihler liste kurulduktan sonra değiştiyse (sıra, alan, il, tip,
|
||||||
|
öncelik) uyarı burada karşılar ve listeyi yeni tercihlerle yeniden
|
||||||
|
kurmayı önerir — üretimle aynı kredi kapısından geçerek. Fark
|
||||||
|
hesabı client'ta yapılır: güncel profil localStorage'ta yaşar. */}
|
||||||
|
<TercihDegistiUyarisi
|
||||||
|
params={raporParams}
|
||||||
|
kredi={user.creditBalance}
|
||||||
|
raporKredi={RAPOR_KREDI}
|
||||||
|
hasPaket={Boolean(user.hasPaket)}
|
||||||
|
/>
|
||||||
{/* Başlık ve revizyon/PDF aksiyonları listenin kendi başlık satırında
|
{/* Başlık ve revizyon/PDF aksiyonları listenin kendi başlık satırında
|
||||||
(bkz. listem-govde.tsx) — sayfa üstünde ayrı bir header yok. */}
|
(bkz. listem-govde.tsx) — sayfa üstünde ayrı bir header yok. */}
|
||||||
<ListemGovde
|
<ListemGovde
|
||||||
|
|||||||
302
src/features/rapor/components/tercih-degisti-uyarisi.tsx
Normal file
302
src/features/rapor/components/tercih-degisti-uyarisi.tsx
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// /listem'deki "tercihlerin değişti" uyarısı: kayıtlı liste hangi tercihlerle
|
||||||
|
// kurulduysa (rapor params) kullanıcının güncel profili ondan ayrıldığında
|
||||||
|
// belirir ve listeyi yeni tercihlere göre yeniden kurmayı önerir.
|
||||||
|
//
|
||||||
|
// Yeniden kurulum normal üretimle AYNI kapıdan geçer (listeOlustur): 3 kredi,
|
||||||
|
// aynı requestId idempotency'si, aynı kredi-yetersiz/iade davranışı. Ayrı bir
|
||||||
|
// "güncelleme" ürünü ya da indirimli yol yok — kullanıcı yeni bir liste alıyor,
|
||||||
|
// bedeli de yeni liste bedeli.
|
||||||
|
//
|
||||||
|
// Uyarı kapatılabilir: kapatma kararı o anki profil imzasına yazılır, tercih
|
||||||
|
// yeniden değişirse uyarı geri gelir (bkz. profilImzasi).
|
||||||
|
|
||||||
|
import { useMemo, useRef, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { ArrowRight, Check, RefreshCcw, SlidersHorizontal, X } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { olay, siraKovasi } from "@/lib/analitik";
|
||||||
|
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
||||||
|
import { listeOlustur } from "@/features/rapor/rapor-actions";
|
||||||
|
import type { RaporParams } from "@/features/rapor/types/rapor";
|
||||||
|
import {
|
||||||
|
farkCumlesi,
|
||||||
|
farkSatirlari,
|
||||||
|
profilImzasi,
|
||||||
|
tercihFarki,
|
||||||
|
} from "@/features/rapor/tercih-farki";
|
||||||
|
import { useUretimMesaji } from "./uretim-mesaji";
|
||||||
|
|
||||||
|
const GIZLE_ANAHTARI = "kolaytercih.listem-fark-gizle";
|
||||||
|
|
||||||
|
function gizlenenImzayiOku(): string | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(GIZLE_ANAHTARI);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Durum =
|
||||||
|
| { ad: "bekliyor" }
|
||||||
|
| { ad: "uretiliyor" }
|
||||||
|
| { ad: "tamamlandi" }
|
||||||
|
| { ad: "kredi-yetersiz" }
|
||||||
|
| { ad: "hata"; mesaj: string };
|
||||||
|
|
||||||
|
export function TercihDegistiUyarisi({
|
||||||
|
params,
|
||||||
|
kredi,
|
||||||
|
raporKredi,
|
||||||
|
hasPaket,
|
||||||
|
}: {
|
||||||
|
/** Kayıtlı listenin üretildiği tercihler */
|
||||||
|
params: RaporParams;
|
||||||
|
kredi: number;
|
||||||
|
raporKredi: number;
|
||||||
|
hasPaket: boolean;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const profil = useTercihProfili();
|
||||||
|
const [durum, setDurum] = useState<Durum>({ ad: "bekliyor" });
|
||||||
|
// Profil client'ta çözüldüğü için bu bileşen SSR'da zaten hiçbir şey
|
||||||
|
// render etmiyor; gizleme kaydı ilk render'da güvenle okunabilir.
|
||||||
|
const [gizlenenImza, setGizlenenImza] = useState<string | null>(
|
||||||
|
gizlenenImzayiOku,
|
||||||
|
);
|
||||||
|
// Aynı deneme için sabit requestId: cevabı kaybolan istekte retry çifte
|
||||||
|
// harcamaz (bkz. liste-uretici.tsx ve spendCredits idempotency'si).
|
||||||
|
const requestIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const farklar = useMemo(
|
||||||
|
() => (profil ? tercihFarki(params, profil) : []),
|
||||||
|
[params, profil],
|
||||||
|
);
|
||||||
|
const satirlar = useMemo(
|
||||||
|
() => (profil ? farkSatirlari(params, profil, farklar) : []),
|
||||||
|
[params, profil, farklar],
|
||||||
|
);
|
||||||
|
|
||||||
|
async function guncelle() {
|
||||||
|
if (!profil) return;
|
||||||
|
if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID();
|
||||||
|
setDurum({ ad: "uretiliyor" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sonuc = await listeOlustur({
|
||||||
|
sira: profil.sira,
|
||||||
|
tur: profil.tur,
|
||||||
|
secimler: profil.secimler,
|
||||||
|
requestId: requestIdRef.current,
|
||||||
|
});
|
||||||
|
requestIdRef.current = null;
|
||||||
|
if (sonuc.ok) {
|
||||||
|
olay("liste_guncellendi", {
|
||||||
|
tur: profil.tur,
|
||||||
|
sira_kovasi: siraKovasi(profil.sira),
|
||||||
|
degisen_alan: farklar.length,
|
||||||
|
});
|
||||||
|
toast.success("Listen yeni tercihlerine göre yeniden kuruldu.");
|
||||||
|
// Sunucu params'ı tazelenince fark kalmaz ve uyarı kendiliğinden
|
||||||
|
// kaybolur; refresh dönene kadar "güncellendi" hâli görünür.
|
||||||
|
setDurum({ ad: "tamamlandi" });
|
||||||
|
router.refresh();
|
||||||
|
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")}`);
|
||||||
|
} else {
|
||||||
|
setDurum({ ad: "hata", mesaj: sonuc.error });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setDurum({
|
||||||
|
ad: "hata",
|
||||||
|
mesaj: "Beklenmeyen bir hata oluştu, tekrar dener misin?",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gizle() {
|
||||||
|
if (!profil) return;
|
||||||
|
const imza = profilImzasi(profil);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(GIZLE_ANAHTARI, imza);
|
||||||
|
} catch {}
|
||||||
|
setGizlenenImza(imza);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profil || farklar.length === 0) return null;
|
||||||
|
if (durum.ad === "tamamlandi") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
className="mb-6 flex items-center gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800"
|
||||||
|
>
|
||||||
|
<Check className="size-4 shrink-0" aria-hidden />
|
||||||
|
Listen yeni tercihlerine göre güncellendi.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Kapatılmış değişiklik: kullanıcı aynı tercihlerle bir daha rahatsız
|
||||||
|
// edilmez. Üretim/hata hâlleri kapatmadan bağımsız görünür kalır.
|
||||||
|
if (durum.ad === "bekliyor" && gizlenenImza === profilImzasi(profil)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const krediYeter = kredi >= raporKredi;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-live="polite"
|
||||||
|
className="mb-6 rounded-2xl border border-amber-200 bg-amber-50 p-4 sm:p-5"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="flex size-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-amber-700">
|
||||||
|
<SlidersHorizontal className="size-4" aria-hidden />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h2 className="font-heading font-bold text-amber-900">
|
||||||
|
Tercihlerin değişti
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-amber-900/80">
|
||||||
|
Aşağıdaki liste eski tercihlerinle kuruldu; {farkCumlesi(farklar)}{" "}
|
||||||
|
o zamandan beri değişti. Yeni tercihlerine göre yeniden kurabilirsin
|
||||||
|
— yeni liste bunun yerine geçer ve {raporKredi} kredi düşer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dl className="mt-3 flex flex-col gap-1.5 text-sm">
|
||||||
|
{satirlar.map((satir) => (
|
||||||
|
<div
|
||||||
|
key={satir.alan}
|
||||||
|
className="flex flex-col gap-0.5 sm:flex-row sm:items-baseline sm:gap-2"
|
||||||
|
>
|
||||||
|
<dt className="shrink-0 text-xs font-medium uppercase tracking-wide text-amber-700">
|
||||||
|
{satir.etiket}
|
||||||
|
</dt>
|
||||||
|
<dd className="min-w-0 text-amber-900">
|
||||||
|
<span className="text-amber-900/60">{satir.eski}</span>
|
||||||
|
<span className="px-1.5 text-amber-700" aria-label="yerine">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">{satir.yeni}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{durum.ad === "uretiliyor" ? (
|
||||||
|
<UretimSatiri />
|
||||||
|
) : durum.ad === "kredi-yetersiz" ? (
|
||||||
|
<KrediYetersiz hasPaket={hasPaket} raporKredi={raporKredi} />
|
||||||
|
) : (
|
||||||
|
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||||
|
{durum.ad === "hata" ? (
|
||||||
|
<>
|
||||||
|
<p className="w-full text-sm text-red-700">{durum.mesaj}</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => void guncelle()}
|
||||||
|
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
<RefreshCcw className="size-4" aria-hidden />
|
||||||
|
Tekrar dene
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : krediYeter ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => void guncelle()}
|
||||||
|
className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
Listemi güncelle — {raporKredi} kredi
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
<Link href="/paket">
|
||||||
|
{hasPaket ? "Kredi yükle ve güncelle" : "Paketi aktive et"}
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={gizle}
|
||||||
|
className="cursor-pointer text-sm font-medium text-amber-800 underline underline-offset-2 transition-colors hover:text-amber-900"
|
||||||
|
>
|
||||||
|
Şimdilik kalsın
|
||||||
|
</button>
|
||||||
|
{!krediYeter && durum.ad !== "hata" ? (
|
||||||
|
<p className="w-full text-xs text-amber-900/70">
|
||||||
|
Güncelleme {raporKredi} kredi; bakiyen {kredi} kredi.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{durum.ad === "bekliyor" ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={gizle}
|
||||||
|
className="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-amber-200 text-amber-800 transition-colors duration-200 active:scale-[0.96] hover:bg-amber-100"
|
||||||
|
>
|
||||||
|
<X className="size-4" aria-hidden />
|
||||||
|
<span className="sr-only">Uyarıyı kapat</span>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UretimSatiri() {
|
||||||
|
const mesaj = useUretimMesaji();
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex flex-col gap-1">
|
||||||
|
<p className="flex items-center gap-2 text-sm font-medium text-amber-900">
|
||||||
|
<RefreshCcw className="size-4 animate-spin" aria-hidden />
|
||||||
|
Listen yeni tercihlerine göre kuruluyor…
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-amber-900/70">{mesaj}</p>
|
||||||
|
<p className="text-xs text-amber-900/60">
|
||||||
|
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KrediYetersiz({
|
||||||
|
hasPaket,
|
||||||
|
raporKredi,
|
||||||
|
}: {
|
||||||
|
hasPaket: boolean;
|
||||||
|
raporKredi: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||||
|
<p className="w-full text-sm text-amber-900">
|
||||||
|
{hasPaket
|
||||||
|
? `Güncelleme ${raporKredi} kredi ve bakiyen yetmedi. Kredi yüklediğinde listen ve sohbet geçmişin duruyor, kaldığın yerden devam edersin.`
|
||||||
|
: `Güncelleme ${raporKredi} kredi ve bakiyen yetmedi. Tercih Dönemi Paketi'yle kredilerin, tam liste, PDF ve revizyon hakları birlikte gelir.`}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
|
>
|
||||||
|
<Link href="/paket">
|
||||||
|
{hasPaket ? "Kredi yükle" : "Paketi aktive et"}
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/features/rapor/components/uretim-mesaji.ts
Normal file
31
src/features/rapor/components/uretim-mesaji.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Liste üretiminin bekleme metinleri tek kaynakta: ilk kurulum (ListeUretici)
|
||||||
|
// ve tercih değişikliği sonrası yeniden kurulum (TercihDegistiUyarisi) aynı
|
||||||
|
// dili ve ritmi paylaşsın diye.
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const URETIM_MESAJLARI = [
|
||||||
|
"Sıralamana uygun programlar taranıyor…",
|
||||||
|
"Gerçek YÖK Atlas verisiyle dengeli liste kuruluyor…",
|
||||||
|
"Her tercih için risk ve trend analizi yazılıyor…",
|
||||||
|
"Son rötuşlar yapılıyor, neredeyse hazır…",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ARALIK_MS = 7000;
|
||||||
|
|
||||||
|
/** Sırayla ilerleyen, sonuncuda duran bekleme metni. Yalnızca üretim
|
||||||
|
* sürerken monte edilir — sayaç mount ile başlar. */
|
||||||
|
export function useUretimMesaji(): string {
|
||||||
|
const [idx, setIdx] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const zamanlayici = setInterval(() => {
|
||||||
|
setIdx((i) => Math.min(i + 1, URETIM_MESAJLARI.length - 1));
|
||||||
|
}, ARALIK_MS);
|
||||||
|
return () => clearInterval(zamanlayici);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return URETIM_MESAJLARI[idx];
|
||||||
|
}
|
||||||
134
src/features/rapor/tercih-farki.ts
Normal file
134
src/features/rapor/tercih-farki.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// Kayıtlı listenin üretildiği tercihler (rapor params) ile kullanıcının şu
|
||||||
|
// anki tercih profilini karşılaştırır. /listem'deki "tercihlerin değişti"
|
||||||
|
// uyarısı ve güncelleme CTA'sı bu farktan doğar.
|
||||||
|
//
|
||||||
|
// İzomorfik: yalnızca tip ve etiket sabitlerine bağlı — hem client bileşeni
|
||||||
|
// hem sunucu tarafı tüketebilir.
|
||||||
|
//
|
||||||
|
// Eski akışla üretilmiş raporlarda sihirbaz alanları (kategoriler/iller/
|
||||||
|
// universiteTipi/oncelikler) hiç yok. Olmayan alan "değişmiş" sayılmaz:
|
||||||
|
// kıyaslanacak bir değer yokken kullanıcıyı 3 kredilik güncellemeye
|
||||||
|
// çağırmak dürüst olmaz. Sıralama ve puan türü her raporda dolu olduğundan
|
||||||
|
// onlar her zaman kıyaslanır.
|
||||||
|
|
||||||
|
import {
|
||||||
|
PUAN_TURU_ETIKET,
|
||||||
|
UNIVERSITE_TIPI_ETIKET,
|
||||||
|
type TercihProfili,
|
||||||
|
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
|
import type { RaporParams } from "./types/rapor";
|
||||||
|
|
||||||
|
export type FarkAlani =
|
||||||
|
| "sira"
|
||||||
|
| "tur"
|
||||||
|
| "kategoriler"
|
||||||
|
| "iller"
|
||||||
|
| "universiteTipi"
|
||||||
|
| "oncelikler";
|
||||||
|
|
||||||
|
export const FARK_ETIKET: Record<FarkAlani, string> = {
|
||||||
|
sira: "başarı sıralaman",
|
||||||
|
tur: "puan türün",
|
||||||
|
kategoriler: "ilgi alanların",
|
||||||
|
iller: "şehir tercihlerin",
|
||||||
|
universiteTipi: "devlet/vakıf tercihin",
|
||||||
|
oncelikler: "önceliklerin",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Sıra/tür/seçim üçlüsünün kararlı imzası — uyarının "bu değişikliği
|
||||||
|
* gizle" kaydı bu imzaya bağlanır (tercih yine değişirse uyarı geri gelir). */
|
||||||
|
export function profilImzasi(profil: TercihProfili): string {
|
||||||
|
const s = profil.secimler;
|
||||||
|
return JSON.stringify([
|
||||||
|
Math.round(profil.sira),
|
||||||
|
profil.tur,
|
||||||
|
[...s.kategoriler].sort(),
|
||||||
|
[...s.iller].sort(),
|
||||||
|
s.universiteTipi,
|
||||||
|
[...s.oncelikler].sort(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sıra bağımsız küme kıyası: sihirbaz seçimleri tekrarsız olarak doğrulanır
|
||||||
|
// (bkz. sihirbazDogrula), aynı seçimlerin farklı sırayla kaydedilmesi
|
||||||
|
// "değişiklik" değildir.
|
||||||
|
function kumeFarkli(
|
||||||
|
eski: readonly string[] | undefined,
|
||||||
|
yeni: readonly string[],
|
||||||
|
): boolean {
|
||||||
|
if (!eski) return false;
|
||||||
|
if (eski.length !== yeni.length) return true;
|
||||||
|
const kume = new Set(eski);
|
||||||
|
return yeni.some((x) => !kume.has(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
function liste(degerler: readonly string[] | undefined): string {
|
||||||
|
return degerler && degerler.length > 0 ? degerler.join(", ") : "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tercihFarki(
|
||||||
|
params: RaporParams,
|
||||||
|
profil: TercihProfili,
|
||||||
|
): FarkAlani[] {
|
||||||
|
const farklar: FarkAlani[] = [];
|
||||||
|
const s = profil.secimler;
|
||||||
|
|
||||||
|
if (Math.round(params.sira) !== Math.round(profil.sira)) farklar.push("sira");
|
||||||
|
if (params.tur !== profil.tur) farklar.push("tur");
|
||||||
|
if (kumeFarkli(params.kategoriler, s.kategoriler)) farklar.push("kategoriler");
|
||||||
|
if (kumeFarkli(params.iller, s.iller)) farklar.push("iller");
|
||||||
|
if (
|
||||||
|
params.universiteTipi !== undefined &&
|
||||||
|
params.universiteTipi !== s.universiteTipi
|
||||||
|
) {
|
||||||
|
farklar.push("universiteTipi");
|
||||||
|
}
|
||||||
|
if (kumeFarkli(params.oncelikler, s.oncelikler)) farklar.push("oncelikler");
|
||||||
|
|
||||||
|
return farklar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "başarı sıralaman ve ilgi alanların" — uyarı cümlesinin ortası. */
|
||||||
|
export function farkCumlesi(farklar: FarkAlani[]): string {
|
||||||
|
const etiketler = farklar.map((f) => FARK_ETIKET[f]);
|
||||||
|
if (etiketler.length <= 1) return etiketler[0] ?? "";
|
||||||
|
return `${etiketler.slice(0, -1).join(", ")} ve ${etiketler[etiketler.length - 1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FarkSatiri = { alan: FarkAlani; etiket: string; eski: string; yeni: string };
|
||||||
|
|
||||||
|
/** Değişen her alanın eski → yeni değeri; uyarıda tek tek gösterilir. */
|
||||||
|
export function farkSatirlari(
|
||||||
|
params: RaporParams,
|
||||||
|
profil: TercihProfili,
|
||||||
|
farklar: FarkAlani[],
|
||||||
|
): FarkSatiri[] {
|
||||||
|
const s = profil.secimler;
|
||||||
|
const sayi = (n: number) => Math.round(n).toLocaleString("tr-TR");
|
||||||
|
|
||||||
|
return farklar.map((alan) => {
|
||||||
|
const ortak = { alan, etiket: FARK_ETIKET[alan] };
|
||||||
|
switch (alan) {
|
||||||
|
case "sira":
|
||||||
|
return { ...ortak, eski: `${sayi(params.sira)}.`, yeni: `${sayi(profil.sira)}.` };
|
||||||
|
case "tur":
|
||||||
|
return {
|
||||||
|
...ortak,
|
||||||
|
eski: PUAN_TURU_ETIKET[params.tur] ?? params.tur,
|
||||||
|
yeni: PUAN_TURU_ETIKET[profil.tur] ?? profil.tur,
|
||||||
|
};
|
||||||
|
case "kategoriler":
|
||||||
|
return { ...ortak, eski: liste(params.kategoriler), yeni: liste(s.kategoriler) };
|
||||||
|
case "iller":
|
||||||
|
return { ...ortak, eski: liste(params.iller), yeni: liste(s.iller) };
|
||||||
|
case "universiteTipi":
|
||||||
|
return {
|
||||||
|
...ortak,
|
||||||
|
eski: UNIVERSITE_TIPI_ETIKET[params.universiteTipi ?? "farketmez"],
|
||||||
|
yeni: UNIVERSITE_TIPI_ETIKET[s.universiteTipi],
|
||||||
|
};
|
||||||
|
case "oncelikler":
|
||||||
|
return { ...ortak, eski: liste(params.oncelikler), yeni: liste(s.oncelikler) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ export type OlayAdi =
|
|||||||
| "sihirbaz_tamamlandi"
|
| "sihirbaz_tamamlandi"
|
||||||
| "giris_denendi"
|
| "giris_denendi"
|
||||||
| "liste_olusturuldu"
|
| "liste_olusturuldu"
|
||||||
|
| "liste_guncellendi"
|
||||||
| "liste_revize"
|
| "liste_revize"
|
||||||
| "program_eklendi"
|
| "program_eklendi"
|
||||||
| "netler_goruntulendi"
|
| "netler_goruntulendi"
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ export type YurtdisiPin = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Yurtdışı tercihini şehir pinine ekler; şehir çözülemezse false (harita
|
* Yurtdışı tercihini şehir pinine ekler; şehir çözülemezse false (harita
|
||||||
* dışı). Kilitli satırlarda universite null verilir — pin şehir bazlı
|
* dışı). Kilitli satırlarda da üniversite adı verilebilir: ad yalnızca şehri
|
||||||
* olduğundan üniversite kimliği zaten görünmez.
|
* çözmek için okunur (KKTC satırlarının çoğunda şehir adın parantez ekinde),
|
||||||
|
* üretilen pin yalnızca ülke/şehir/sayı taşır — üniversite kimliği görünmez.
|
||||||
*/
|
*/
|
||||||
export function yurtdisiPinKaydet(
|
export function yurtdisiPinKaydet(
|
||||||
gruplar: Map<string, YurtdisiPin>,
|
gruplar: Map<string, YurtdisiPin>,
|
||||||
@@ -92,7 +93,8 @@ export function pinleriTuret(
|
|||||||
if (kilitli) {
|
if (kilitli) {
|
||||||
const konum = ilKonum(p.il);
|
const konum = ilKonum(p.il);
|
||||||
if (!konum) {
|
if (!konum) {
|
||||||
if (!yurtdisiPinKaydet(yurtdisi, null, p.il, t.sira, risk))
|
// Şehir çözümü için ad okunur, pine yazılmaz (bkz. yurtdisiPinKaydet)
|
||||||
|
if (!yurtdisiPinKaydet(yurtdisi, p.universite, p.il, t.sira, risk))
|
||||||
haritaDisi += 1;
|
haritaDisi += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,23 @@ const UNI_DUZELTME: Record<string, SehirAnahtari | null> = {
|
|||||||
/** il değeri Türkiye dışını mı gösteriyor? ("KIBRIS" genel değeri dahil) */
|
/** il değeri Türkiye dışını mı gösteriyor? ("KIBRIS" genel değeri dahil) */
|
||||||
const YURTDISI_IL = new Set([...Object.keys(SEHIRLER), "KIBRIS"]);
|
const YURTDISI_IL = new Set([...Object.keys(SEHIRLER), "KIBRIS"]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ad sonundaki "(KKTC-LEFKOŞA)" ekinden şehir anahtarı. YÖK Atlas'ta 100'den
|
||||||
|
* fazla KKTC satırının il alanı şehir değil genel "KIBRIS"tır (Doğu Akdeniz,
|
||||||
|
* Yakın Doğu, Lefke Avrupa, Uluslararası Final, Kıbrıs Sağlık ve Toplum
|
||||||
|
* Bilimleri); şehir yalnızca üniversite adının parantezinde durur. Bu ek
|
||||||
|
* okunmazsa o programlar hiçbir haritaya düşmez, "haritada gösterilemiyor"
|
||||||
|
* sayısına yazılır.
|
||||||
|
*/
|
||||||
|
function ektenSehir(universite: string): SehirAnahtari | null {
|
||||||
|
const ek = universite.match(/\(([^)]*)\)\s*$/)?.[1];
|
||||||
|
if (!ek) return null;
|
||||||
|
// "KKTC-GAZİMAĞUSA" → "GAZİMAĞUSA"; ülke öneki olmayan "(ANKARA)" gibi
|
||||||
|
// ekler SEHIRLER'de karşılık bulmadığından kendiliğinden elenir.
|
||||||
|
const sehir = ek.split("-").pop()!.trim().toLocaleUpperCase("tr-TR");
|
||||||
|
return sehir in SEHIRLER ? (sehir as SehirAnahtari) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Üniversite adı + il'den şehir anahtarı; şehri bilinmeyen KKTC'de ülke. */
|
/** Üniversite adı + il'den şehir anahtarı; şehri bilinmeyen KKTC'de ülke. */
|
||||||
type Cozum = { anahtar: SehirAnahtari } | { ulke: UlkeKodu } | null;
|
type Cozum = { anahtar: SehirAnahtari } | { ulke: UlkeKodu } | null;
|
||||||
|
|
||||||
@@ -96,7 +113,10 @@ function coz(universite: string, il: string | null): Cozum {
|
|||||||
}
|
}
|
||||||
if (!il || !YURTDISI_IL.has(il)) return null;
|
if (!il || !YURTDISI_IL.has(il)) return null;
|
||||||
if (il in SEHIRLER) return { anahtar: il as SehirAnahtari };
|
if (il in SEHIRLER) return { anahtar: il as SehirAnahtari };
|
||||||
return { ulke: "kktc" }; // il "KIBRIS": ülke bilinir, şehir bilinmez
|
// il "KIBRIS": ülke bilinir, şehir adın parantez ekinden okunur;
|
||||||
|
// o da yoksa ülke seviyesinde kalır (şehir pini çizilemez)
|
||||||
|
const ekSehir = ektenSehir(universite);
|
||||||
|
return ekSehir ? { anahtar: ekSehir } : { ulke: "kktc" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UlkeKonum = {
|
export type UlkeKonum = {
|
||||||
|
|||||||
Reference in New Issue
Block a user