Compare commits
2 Commits
24f53ddbce
...
ce6871d623
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce6871d623 | ||
|
|
3a8c049092 |
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
209
scripts/ulke-harita-uret.ts
Normal file
209
scripts/ulke-harita-uret.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
// Yurtdışı üniversite sayfaları için ülke kontürlerini üretir.
|
||||||
|
//
|
||||||
|
// Kaynak: Natural Earth 10m admin_0_countries (kamu malı). GeoJSON bir kez
|
||||||
|
// indirilip tmp'ye önbelleklenir; çıktı src/lib/harita-ulkeler-verisi.ts
|
||||||
|
// dosyasına yazılır ve repo'ya commit'lenir (build ağa çıkmaz).
|
||||||
|
//
|
||||||
|
// Projeksiyon: ülke başına eşdikdörtgen (plate carrée) + cos(ortaEnlem)
|
||||||
|
// düzeltmesi; Türkiye haritasının kırpılmış oranıyla aynı 1050×447 kutuya
|
||||||
|
// sığdırılır. Nokta seyreltme ~1px eşikli olduğundan çıktı küçük kalır.
|
||||||
|
//
|
||||||
|
// Çalıştırma: pnpm tsx scripts/ulke-harita-uret.ts
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const GENISLIK = 1050;
|
||||||
|
const YUKSEKLIK = 447;
|
||||||
|
const KENAR_PAYI = 26;
|
||||||
|
const SEYRELTME_PX = 1.1;
|
||||||
|
const MIN_HALKA_PX = 3; // bbox'u bundan küçük halkalar (adacıklar) atlanır
|
||||||
|
|
||||||
|
const KAYNAK_URL =
|
||||||
|
"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_10m_admin_0_countries.geojson";
|
||||||
|
|
||||||
|
type UlkeTanim = {
|
||||||
|
kod: string;
|
||||||
|
ad: string;
|
||||||
|
/** Vurgulanan (mavi) NE ADMIN adları */
|
||||||
|
vurgulu: string[];
|
||||||
|
/** Soluk çizilen bağlam parçaları (ör. Kıbrıs'ın güneyi) */
|
||||||
|
baglam?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ULKELER: UlkeTanim[] = [
|
||||||
|
{
|
||||||
|
kod: "kktc",
|
||||||
|
ad: "KKTC",
|
||||||
|
vurgulu: ["Northern Cyprus"],
|
||||||
|
baglam: [
|
||||||
|
"Cyprus",
|
||||||
|
"Cyprus No Mans Area",
|
||||||
|
"Dhekelia Sovereign Base Area",
|
||||||
|
"Akrotiri Sovereign Base Area",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ kod: "azerbaycan", ad: "Azerbaycan", vurgulu: ["Azerbaijan"] },
|
||||||
|
{ kod: "kirgizistan", ad: "Kırgızistan", vurgulu: ["Kyrgyzstan"] },
|
||||||
|
{ kod: "kazakistan", ad: "Kazakistan", vurgulu: ["Kazakhstan"] },
|
||||||
|
{
|
||||||
|
kod: "bosna-hersek",
|
||||||
|
ad: "Bosna-Hersek",
|
||||||
|
vurgulu: ["Bosnia and Herzegovina"],
|
||||||
|
},
|
||||||
|
{ kod: "makedonya", ad: "Kuzey Makedonya", vurgulu: ["North Macedonia"] },
|
||||||
|
{ kod: "arnavutluk", ad: "Arnavutluk", vurgulu: ["Albania"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
type Halka = [number, number][]; // [lng, lat]
|
||||||
|
|
||||||
|
async function geojsonGetir(): Promise<{
|
||||||
|
features: {
|
||||||
|
properties: { ADMIN: string };
|
||||||
|
geometry: { type: string; coordinates: unknown };
|
||||||
|
}[];
|
||||||
|
}> {
|
||||||
|
const cachePath = path.join(os.tmpdir(), "ne_10m_admin_0_countries.geojson");
|
||||||
|
if (!fs.existsSync(cachePath)) {
|
||||||
|
console.log("indiriliyor:", KAYNAK_URL);
|
||||||
|
const res = await fetch(KAYNAK_URL);
|
||||||
|
if (!res.ok) throw new Error(`indirme başarısız: ${res.status}`);
|
||||||
|
fs.writeFileSync(cachePath, Buffer.from(await res.arrayBuffer()));
|
||||||
|
}
|
||||||
|
return JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function halkalar(geometry: { type: string; coordinates: unknown }): Halka[] {
|
||||||
|
if (geometry.type === "Polygon") return geometry.coordinates as Halka[];
|
||||||
|
if (geometry.type === "MultiPolygon")
|
||||||
|
return (geometry.coordinates as Halka[][]).flat();
|
||||||
|
throw new Error(`beklenmeyen geometri: ${geometry.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathUret(
|
||||||
|
ringler: Halka[],
|
||||||
|
proje: (lng: number, lat: number) => { x: number; y: number },
|
||||||
|
): string[] {
|
||||||
|
const parcalar: string[] = [];
|
||||||
|
for (const ring of ringler) {
|
||||||
|
const pts = ring.map(([lng, lat]) => proje(lng, lat));
|
||||||
|
const xs = pts.map((p) => p.x);
|
||||||
|
const ys = pts.map((p) => p.y);
|
||||||
|
if (
|
||||||
|
Math.max(...xs) - Math.min(...xs) < MIN_HALKA_PX &&
|
||||||
|
Math.max(...ys) - Math.min(...ys) < MIN_HALKA_PX
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let d = "";
|
||||||
|
let sonX = NaN;
|
||||||
|
let sonY = NaN;
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
const { x, y } = pts[i];
|
||||||
|
if (i > 0 && Math.hypot(x - sonX, y - sonY) < SEYRELTME_PX) continue;
|
||||||
|
d += `${d ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`;
|
||||||
|
sonX = x;
|
||||||
|
sonY = y;
|
||||||
|
}
|
||||||
|
d += "Z";
|
||||||
|
if (d.length > 10) parcalar.push(d);
|
||||||
|
}
|
||||||
|
return parcalar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const geojson = await geojsonGetir();
|
||||||
|
const byAdmin = new Map(
|
||||||
|
geojson.features.map((f) => [f.properties.ADMIN, f] as const),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cikti: string[] = [];
|
||||||
|
const kodlar = ULKELER.map((u) => `"${u.kod}"`).join(" | ");
|
||||||
|
|
||||||
|
cikti.push(
|
||||||
|
"// Bu dosya scripts/ulke-harita-uret.ts tarafından üretildi — elle düzenleme.",
|
||||||
|
"// Kaynak: Natural Earth 10m admin_0_countries (kamu malı).",
|
||||||
|
"",
|
||||||
|
`export type UlkeKodu = ${kodlar};`,
|
||||||
|
"",
|
||||||
|
"export type UlkeHarita = {",
|
||||||
|
" ad: string;",
|
||||||
|
" /** lat/lng → viewBox koordinatı: x = ax*lng + bx, y = ay*lat + by */",
|
||||||
|
" proje: { ax: number; bx: number; ay: number; by: number };",
|
||||||
|
" /** Vurgulanan ülke kontürleri */",
|
||||||
|
" ulke: string[];",
|
||||||
|
" /** Soluk çizilen bağlam kontürleri (ör. Kıbrıs adasının güneyi) */",
|
||||||
|
" baglam: string[];",
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
`export const ULKE_HARITA_GENISLIK = ${GENISLIK};`,
|
||||||
|
`export const ULKE_HARITA_YUKSEKLIK = ${YUKSEKLIK};`,
|
||||||
|
"",
|
||||||
|
"export const ULKE_HARITALARI: Record<UlkeKodu, UlkeHarita> = {",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const ulke of ULKELER) {
|
||||||
|
const adlar = [...ulke.vurgulu, ...(ulke.baglam ?? [])];
|
||||||
|
const geometriler = adlar.map((ad) => {
|
||||||
|
const f = byAdmin.get(ad);
|
||||||
|
if (!f) throw new Error(`Natural Earth'te bulunamadı: ${ad}`);
|
||||||
|
return { ad, ringler: halkalar(f.geometry) };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sığdırma: tüm parçaların (bağlam dahil) bbox'una göre tek ölçek
|
||||||
|
const tumNoktalar = geometriler.flatMap((g) => g.ringler.flat());
|
||||||
|
const latler = tumNoktalar.map(([, lat]) => lat);
|
||||||
|
const ortaLat = (Math.min(...latler) + Math.max(...latler)) / 2;
|
||||||
|
const kx = Math.cos((ortaLat * Math.PI) / 180);
|
||||||
|
const pxler = tumNoktalar.map(([lng]) => lng * kx);
|
||||||
|
const pyler = tumNoktalar.map(([, lat]) => -lat);
|
||||||
|
const minX = Math.min(...pxler);
|
||||||
|
const maxX = Math.max(...pxler);
|
||||||
|
const minY = Math.min(...pyler);
|
||||||
|
const maxY = Math.max(...pyler);
|
||||||
|
const s = Math.min(
|
||||||
|
(GENISLIK - 2 * KENAR_PAYI) / (maxX - minX),
|
||||||
|
(YUKSEKLIK - 2 * KENAR_PAYI) / (maxY - minY),
|
||||||
|
);
|
||||||
|
const ox = (GENISLIK - s * (maxX - minX)) / 2;
|
||||||
|
const oy = (YUKSEKLIK - s * (maxY - minY)) / 2;
|
||||||
|
const proje = (lng: number, lat: number) => ({
|
||||||
|
x: s * (lng * kx - minX) + ox,
|
||||||
|
y: s * (-lat - minY) + oy,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vurguluPaths = geometriler
|
||||||
|
.filter((g) => ulke.vurgulu.includes(g.ad))
|
||||||
|
.flatMap((g) => pathUret(g.ringler, proje));
|
||||||
|
const baglamPaths = geometriler
|
||||||
|
.filter((g) => !ulke.vurgulu.includes(g.ad))
|
||||||
|
.flatMap((g) => pathUret(g.ringler, proje));
|
||||||
|
|
||||||
|
const yaz = (n: number) => Number(n.toFixed(4));
|
||||||
|
cikti.push(
|
||||||
|
` "${ulke.kod}": {`,
|
||||||
|
` ad: ${JSON.stringify(ulke.ad)},`,
|
||||||
|
` proje: { ax: ${yaz(s * kx)}, bx: ${yaz(ox - s * minX)}, ay: ${yaz(-s)}, by: ${yaz(oy - s * minY)} },`,
|
||||||
|
` ulke: ${JSON.stringify(vurguluPaths)},`,
|
||||||
|
` baglam: ${JSON.stringify(baglamPaths)},`,
|
||||||
|
" },",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`${ulke.kod}: ${vurguluPaths.length} vurgulu + ${baglamPaths.length} bağlam path, ` +
|
||||||
|
`~${Math.round((vurguluPaths.join("").length + baglamPaths.join("").length) / 1024)}KB`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cikti.push("};", "");
|
||||||
|
const hedef = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"src",
|
||||||
|
"lib",
|
||||||
|
"harita-ulkeler-verisi.ts",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(hedef, cikti.join("\n"));
|
||||||
|
console.log("yazıldı:", hedef);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -36,7 +36,8 @@ export function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
|
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// /sonuc tablosunun filtreleri sayfalamada da korunur (bkz. sonucHref)
|
// /sonuc tablosunun filtreleri (SSR'da profil çerezinden çözülüp tabloya
|
||||||
|
// prop geçen il/tip) sayfalama XHR'ında query ile korunur
|
||||||
const iller = [
|
const iller = [
|
||||||
...new Set(searchParams.getAll("il").filter((i) => i.trim().length > 0)),
|
...new Set(searchParams.getAll("il").filter((i) => i.trim().length > 0)),
|
||||||
].slice(0, 5);
|
].slice(0, 5);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
||||||
|
import {
|
||||||
|
cerezdenTercihProfili,
|
||||||
|
PROFIL_CEREZ_ADI,
|
||||||
|
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import {
|
import {
|
||||||
SonucProgramTablosu,
|
SonucProgramTablosu,
|
||||||
SonucProgramTablosuSkeleton,
|
SonucProgramTablosuSkeleton,
|
||||||
@@ -14,9 +19,9 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Tercih Listeni Kur",
|
title: "Tercih Listeni Kur",
|
||||||
// Param uzayı sonsuz + içerik sıralamaya göre kişisel: indexlenmez ama
|
// İçerik profil çerezine göre kişisel (eski linklerde param uzayı sonsuz):
|
||||||
// link değeri follow ile geri akar. robots.txt'te ENGELLEME — noindex'in
|
// indexlenmez ama link değeri follow ile geri akar. robots.txt'te
|
||||||
// görülebilmesi için sayfa taranabilir kalmalı.
|
// ENGELLEME — noindex'in görülebilmesi için sayfa taranabilir kalmalı.
|
||||||
robots: { index: false, follow: true },
|
robots: { index: false, follow: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,28 +52,37 @@ export default function SonucPage({
|
|||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{searchParams.then((params) => {
|
{Promise.all([searchParams, cookies()]).then(([params, cerezler]) => {
|
||||||
const sira = Number.parseInt(params.sira ?? "", 10);
|
// Profil (sıra/tür/il/tip) normalde PROFIL_CEREZ_ADI çerezinden gelir;
|
||||||
|
// URL param taşımaz (bkz. sonucHref). Query'de sira varsa eski
|
||||||
|
// paylaşılan/yer imli link demektir — açık URL niyeti çerezi ezer,
|
||||||
|
// eski linkler çalışmayı sürdürür.
|
||||||
|
const eskiLinkSira = Number.parseInt(params.sira ?? "", 10);
|
||||||
|
const eskiLink = Number.isFinite(eskiLinkSira);
|
||||||
|
const profil = eskiLink
|
||||||
|
? null
|
||||||
|
: cerezdenTercihProfili(cerezler.get(PROFIL_CEREZ_ADI)?.value);
|
||||||
|
|
||||||
|
const sira = eskiLink ? eskiLinkSira : (profil?.sira ?? Number.NaN);
|
||||||
|
const turHam = eskiLink ? params.tur : profil?.tur;
|
||||||
const turKey: PuanTuruKey =
|
const turKey: PuanTuruKey =
|
||||||
params.tur && params.tur in PUAN_TURLERI
|
turHam && turHam in PUAN_TURLERI ? (turHam as PuanTuruKey) : "say";
|
||||||
? (params.tur as PuanTuruKey)
|
// Tablo filtreleri: il en fazla 5 tekrarsız, tip devlet|vakif
|
||||||
: "say";
|
// allowlist'li (çerez tarafı tercihProfiliDogrula'dan zaten geçmiş
|
||||||
// Sihirbaz seçimlerinden gelen tablo filtreleri (bkz. sonucHref):
|
// olsa da iki kaynak aynı süzgeçten akar).
|
||||||
// il tekrarlı param, tip devlet|vakif allowlist'li
|
const hamIller = eskiLink
|
||||||
const iller = [
|
? Array.isArray(params.il)
|
||||||
...new Set(
|
|
||||||
(Array.isArray(params.il)
|
|
||||||
? params.il
|
? params.il
|
||||||
: params.il
|
: params.il
|
||||||
? [params.il]
|
? [params.il]
|
||||||
: []
|
: []
|
||||||
).filter((i) => i.trim().length > 0),
|
: (profil?.secimler.iller ?? []);
|
||||||
),
|
const iller = [
|
||||||
|
...new Set(hamIller.filter((i) => i.trim().length > 0)),
|
||||||
].slice(0, 5);
|
].slice(0, 5);
|
||||||
|
const tipHam = eskiLink ? params.tip : profil?.secimler.universiteTipi;
|
||||||
const uniturGrubu =
|
const uniturGrubu =
|
||||||
params.tip === "devlet" || params.tip === "vakif"
|
tipHam === "devlet" || tipHam === "vakif" ? tipHam : undefined;
|
||||||
? params.tip
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +104,7 @@ export default function SonucPage({
|
|||||||
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
|
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
|
||||||
sayfanın ana gövdesi; puan türü navbar'daki sıralama
|
sayfanın ana gövdesi; puan türü navbar'daki sıralama
|
||||||
rozetinden değiştirilir. Sihirbaz seçimleri (il, devlet/
|
rozetinden değiştirilir. Sihirbaz seçimleri (il, devlet/
|
||||||
vakıf) URL paramlarıyla tabloya uygulanır. */}
|
vakıf) profil çerezinden tabloya uygulanır. */}
|
||||||
<SonucProgramTablosu
|
<SonucProgramTablosu
|
||||||
sira={sira}
|
sira={sira}
|
||||||
tur={turKey}
|
tur={turKey}
|
||||||
|
|||||||
@@ -82,14 +82,16 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) {
|
|||||||
|
|
||||||
function sihirbazaGit() {
|
function sihirbazaGit() {
|
||||||
onSec();
|
onSec();
|
||||||
// Orkestratör (SihirbazBolumu) yalnızca /sonuc?sira= geçerliyken monte;
|
// Orkestratör (SihirbazBolumu) yalnızca /sonuc tabloyu render ederken
|
||||||
// sırasız /sonuc'ta (SiraGerekli görünümü) olay boşa gider — kapıya düş.
|
// monte: profil çerezi (client'ta aynası localStorage profili) ya da eski
|
||||||
if (pathname === "/sonuc" && searchParams.get("sira")) {
|
// linklerdeki ?sira= geçerliyken. Profilsiz /sonuc'ta (SiraGerekli
|
||||||
|
// görünümü) olay boşa gider — kapıya düş.
|
||||||
|
if (pathname === "/sonuc" && (profil || searchParams.get("sira"))) {
|
||||||
window.dispatchEvent(new Event(SIHIRBAZ_AC_EVENT));
|
window.dispatchEvent(new Event(SIHIRBAZ_AC_EVENT));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (profil) {
|
if (profil) {
|
||||||
router.push(sonucHref(profil, { sihirbaz: "1" }));
|
router.push(sonucHref({ sihirbaz: "1" }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Yapay Zeka niyetli CTA: kapı tamamlanınca /sonuc'ta üretim akışı devam
|
// Yapay Zeka niyetli CTA: kapı tamamlanınca /sonuc'ta üretim akışı devam
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { SiteFooter } from "@/components/site-footer";
|
|||||||
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
||||||
|
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
||||||
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
|
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||||
@@ -94,6 +95,7 @@ export function UniversiteIcerik({
|
|||||||
const buYol = ilkSayfa
|
const buYol = ilkSayfa
|
||||||
? `/universite/${slug}`
|
? `/universite/${slug}`
|
||||||
: `/universite/${slug}/sayfa/${sayfa}`;
|
: `/universite/${slug}/sayfa/${sayfa}`;
|
||||||
|
const yurtdisi = uniUlkeKonum(uni.ad, uni.il);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -116,12 +118,16 @@ export function UniversiteIcerik({
|
|||||||
...(uniLogoYolu(slug)
|
...(uniLogoYolu(slug)
|
||||||
? { logo: `${SITE_URL}${uniLogoYolu(slug)}` }
|
? { logo: `${SITE_URL}${uniLogoYolu(slug)}` }
|
||||||
: {}),
|
: {}),
|
||||||
...(uni.il
|
...(uni.il || yurtdisi
|
||||||
? {
|
? {
|
||||||
address: {
|
address: {
|
||||||
"@type": "PostalAddress",
|
"@type": "PostalAddress",
|
||||||
addressLocality: trBaslikDuzeni(uni.il),
|
...(yurtdisi?.sehir
|
||||||
addressCountry: "TR",
|
? { addressLocality: yurtdisi.sehir }
|
||||||
|
: uni.il
|
||||||
|
? { addressLocality: trBaslikDuzeni(uni.il) }
|
||||||
|
: {}),
|
||||||
|
addressCountry: yurtdisi ? yurtdisi.iso : "TR",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import {
|
|||||||
HARITA_YUKSEKLIK,
|
HARITA_YUKSEKLIK,
|
||||||
normalizeIlAdi,
|
normalizeIlAdi,
|
||||||
} from "@/lib/harita";
|
} from "@/lib/harita";
|
||||||
|
import {
|
||||||
|
ULKE_HARITA_GENISLIK,
|
||||||
|
ULKE_HARITA_YUKSEKLIK,
|
||||||
|
uniUlkeKonum,
|
||||||
|
type UlkeKonum,
|
||||||
|
} from "@/lib/harita-ulkeler";
|
||||||
|
|
||||||
|
const SECILI_DOLGU = "oklch(0.809 0.105 251.8)";
|
||||||
|
const ZEMIN_DOLGU = "oklch(0.929 0.013 255.5)";
|
||||||
|
|
||||||
export function UniversiteKonumHaritasi({
|
export function UniversiteKonumHaritasi({
|
||||||
universite,
|
universite,
|
||||||
@@ -13,18 +22,23 @@ export function UniversiteKonumHaritasi({
|
|||||||
universite: string;
|
universite: string;
|
||||||
il: string | null;
|
il: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const yurtdisi = uniUlkeKonum(universite, il);
|
||||||
|
if (yurtdisi) {
|
||||||
|
return (
|
||||||
|
<KonumBolumu universite={universite}>
|
||||||
|
<UlkeHaritasi universite={universite} konum={yurtdisi} />
|
||||||
|
<p className="pointer-events-none absolute bottom-0 left-0 text-[11px] font-medium text-slate-500">
|
||||||
|
{yurtdisi.ulkeAd}
|
||||||
|
{yurtdisi.sehir ? ` · ${yurtdisi.sehir}` : ""}
|
||||||
|
</p>
|
||||||
|
</KonumBolumu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!il) return null;
|
if (!il) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<KonumBolumu universite={universite}>
|
||||||
aria-labelledby="universite-konumu"
|
|
||||||
className="relative col-span-2 min-h-44 lg:col-span-1 lg:col-start-3 lg:row-start-1 lg:row-span-2 lg:min-h-0"
|
|
||||||
>
|
|
||||||
<h2 id="universite-konumu" className="sr-only">
|
|
||||||
{universite} kampüs konumu
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="relative h-full min-h-44 w-full">
|
|
||||||
<svg
|
<svg
|
||||||
viewBox={`0 ${HARITA_UST_KIRPMA} ${HARITA_GENISLIK} ${HARITA_YUKSEKLIK - HARITA_UST_KIRPMA}`}
|
viewBox={`0 ${HARITA_UST_KIRPMA} ${HARITA_GENISLIK} ${HARITA_YUKSEKLIK - HARITA_UST_KIRPMA}`}
|
||||||
className="absolute inset-0 h-full w-full"
|
className="absolute inset-0 h-full w-full"
|
||||||
@@ -37,11 +51,7 @@ export function UniversiteKonumHaritasi({
|
|||||||
<path
|
<path
|
||||||
key={city.id}
|
key={city.id}
|
||||||
d={city.path}
|
d={city.path}
|
||||||
fill={
|
fill={secili ? SECILI_DOLGU : ZEMIN_DOLGU}
|
||||||
secili
|
|
||||||
? "oklch(0.809 0.105 251.8)"
|
|
||||||
: "oklch(0.929 0.013 255.5)"
|
|
||||||
}
|
|
||||||
stroke="white"
|
stroke="white"
|
||||||
strokeWidth={secili ? 1.8 : 1}
|
strokeWidth={secili ? 1.8 : 1}
|
||||||
>
|
>
|
||||||
@@ -50,7 +60,53 @@ export function UniversiteKonumHaritasi({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</KonumBolumu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KonumBolumu({
|
||||||
|
universite,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
universite: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-labelledby="universite-konumu"
|
||||||
|
className="relative col-span-2 min-h-44 lg:col-span-1 lg:col-start-3 lg:row-start-1 lg:row-span-2 lg:min-h-0"
|
||||||
|
>
|
||||||
|
<h2 id="universite-konumu" className="sr-only">
|
||||||
|
{universite} kampüs konumu
|
||||||
|
</h2>
|
||||||
|
<div className="relative h-full min-h-44 w-full">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UlkeHaritasi({
|
||||||
|
universite,
|
||||||
|
konum,
|
||||||
|
}: {
|
||||||
|
universite: string;
|
||||||
|
konum: UlkeKonum;
|
||||||
|
}) {
|
||||||
|
const { harita, ulkeAd, sehir } = konum;
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${ULKE_HARITA_GENISLIK} ${ULKE_HARITA_YUKSEKLIK}`}
|
||||||
|
className="absolute inset-0 h-full w-full"
|
||||||
|
role="img"
|
||||||
|
aria-label={`${universite} konumunun ${ulkeAd} haritasındaki görünümü${sehir ? ` (${sehir})` : ""}`}
|
||||||
|
>
|
||||||
|
{harita.baglam.map((d, i) => (
|
||||||
|
<path key={`b${i}`} d={d} fill={ZEMIN_DOLGU} stroke="white" strokeWidth={1} />
|
||||||
|
))}
|
||||||
|
{harita.ulke.map((d, i) => (
|
||||||
|
<path key={`u${i}`} d={d} fill={SECILI_DOLGU} stroke="white" strokeWidth={1.8}>
|
||||||
|
<title>{ulkeAd}</title>
|
||||||
|
</path>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ export function ProgramTablosu({
|
|||||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
||||||
varsayilanDilim(sonuclar),
|
varsayilanDilim(sonuclar),
|
||||||
);
|
);
|
||||||
// Sihirbaz seçimlerinden URL'e yazılan filtreler (bkz. sonucHref) —
|
// Sihirbaz seçimlerinden profil çerezine yazılıp SSR'da prop olarak gelen
|
||||||
// sayfalama isteklerinde ve veri anahtarında kullanılır.
|
// filtreler — sayfalama isteklerinde ve veri anahtarında kullanılır.
|
||||||
const iller = useMemo(() => filtreIller ?? [], [filtreIller]);
|
const iller = useMemo(() => filtreIller ?? [], [filtreIller]);
|
||||||
|
|
||||||
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür/filtre
|
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür/filtre
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function SecimlerimPaneli() {
|
|||||||
asChild
|
asChild
|
||||||
className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
<Link href={sonucHref(profil)}>Sonuçlara git</Link>
|
<Link href={sonucHref()}>Sonuçlara git</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ function baskinRisk(pinler: HaritaPin[]): RiskSeviyesi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Aynı noktaya düşen pinleri küçük bir halka üzerinde ayrıştırır. */
|
/** Aynı noktaya düşen pinleri küçük bir halka üzerinde ayrıştırır. */
|
||||||
function cakismaAc(pinler: HaritaPin[]): HaritaPin[] {
|
function cakismaAc(pinler: HaritaPin[], halka: number): HaritaPin[] {
|
||||||
const gruplar = new Map<string, HaritaPin[]>();
|
const gruplar = new Map<string, HaritaPin[]>();
|
||||||
for (const p of pinler) {
|
for (const p of pinler) {
|
||||||
const key = `${Math.round(p.x)}:${Math.round(p.y)}`;
|
const key = `${Math.round(p.x)}:${Math.round(p.y)}`;
|
||||||
@@ -109,7 +109,11 @@ function cakismaAc(pinler: HaritaPin[]): HaritaPin[] {
|
|||||||
}
|
}
|
||||||
grup.forEach((p, i) => {
|
grup.forEach((p, i) => {
|
||||||
const aci = (2 * Math.PI * i) / grup.length;
|
const aci = (2 * Math.PI * i) / grup.length;
|
||||||
sonuc.push({ ...p, x: p.x + Math.cos(aci) * 5, y: p.y + Math.sin(aci) * 5 });
|
sonuc.push({
|
||||||
|
...p,
|
||||||
|
x: p.x + Math.cos(aci) * halka,
|
||||||
|
y: p.y + Math.sin(aci) * halka,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return sonuc;
|
return sonuc;
|
||||||
@@ -243,7 +247,12 @@ export function TercihHaritasi({
|
|||||||
() => (seciliIl ? (ilPinleri.get(seciliIl) ?? []) : []),
|
() => (seciliIl ? (ilPinleri.get(seciliIl) ?? []) : []),
|
||||||
[ilPinleri, seciliIl],
|
[ilPinleri, seciliIl],
|
||||||
);
|
);
|
||||||
const acikPinler = useMemo(() => cakismaAc(seciliPinler), [seciliPinler]);
|
// Halka yarıçapı zoom oranıyla ölçeklenir ki pinler ekranda hep aynı
|
||||||
|
// mesafede ayrışsın (pin boyutları da sf ile ekran-sabit).
|
||||||
|
const acikPinler = useMemo(
|
||||||
|
() => cakismaAc(seciliPinler, 20 * sf),
|
||||||
|
[seciliPinler, sf],
|
||||||
|
);
|
||||||
|
|
||||||
// Ülke seviyesi toplu pinler: il başına adet/merkez/renk tek geçişte
|
// Ülke seviyesi toplu pinler: il başına adet/merkez/renk tek geçişte
|
||||||
// (üç ayrı reduce + baskinRisk dördüncü geçişti) ve memo'da.
|
// (üç ayrı reduce + baskinRisk dördüncü geçişti) ve memo'da.
|
||||||
@@ -370,7 +379,7 @@ export function TercihHaritasi({
|
|||||||
// Kilitli pinlerde logo da gösterilmez — üniversite kimliği
|
// Kilitli pinlerde logo da gösterilmez — üniversite kimliği
|
||||||
// client'a inmediği gibi görselden de sızmamalı.
|
// client'a inmediği gibi görselden de sızmamalı.
|
||||||
const logo = p.kilitli ? null : uniLogoYoluFromAd(p.universite);
|
const logo = p.kilitli ? null : uniLogoYoluFromAd(p.universite);
|
||||||
const r = (p.kilitli ? 7 : logo ? 11 : 8) * sf;
|
const r = (p.kilitli ? 12 : logo ? 20 : 14) * sf;
|
||||||
const etiket = p.kilitli ? null : kisaUniAdi(p.universite);
|
const etiket = p.kilitli ? null : kisaUniAdi(p.universite);
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
@@ -396,31 +405,31 @@ export function TercihHaritasi({
|
|||||||
r={r}
|
r={r}
|
||||||
fill="white"
|
fill="white"
|
||||||
stroke={RISK_RENK[p.risk]}
|
stroke={RISK_RENK[p.risk]}
|
||||||
strokeWidth={2 * sf}
|
strokeWidth={2.5 * sf}
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
href={logo}
|
href={logo}
|
||||||
x={p.x - 8 * sf}
|
x={p.x - 15 * sf}
|
||||||
y={p.y - 8 * sf}
|
y={p.y - 15 * sf}
|
||||||
width={16 * sf}
|
width={30 * sf}
|
||||||
height={16 * sf}
|
height={30 * sf}
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
className="pointer-events-none"
|
className="pointer-events-none"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx={p.x + r * 0.78}
|
cx={p.x + r * 0.78}
|
||||||
cy={p.y - r * 0.78}
|
cy={p.y - r * 0.78}
|
||||||
r={4.5 * sf}
|
r={7.5 * sf}
|
||||||
fill={RISK_RENK[p.risk]}
|
fill={RISK_RENK[p.risk]}
|
||||||
stroke="white"
|
stroke="white"
|
||||||
strokeWidth={1.2 * sf}
|
strokeWidth={1.5 * sf}
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x={p.x + r * 0.78}
|
x={p.x + r * 0.78}
|
||||||
y={p.y - r * 0.78}
|
y={p.y - r * 0.78}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fontSize={6 * sf}
|
fontSize={9.5 * sf}
|
||||||
fontWeight={700}
|
fontWeight={700}
|
||||||
fill="white"
|
fill="white"
|
||||||
className="pointer-events-none select-none"
|
className="pointer-events-none select-none"
|
||||||
@@ -437,14 +446,14 @@ export function TercihHaritasi({
|
|||||||
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
|
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
|
||||||
stroke="white"
|
stroke="white"
|
||||||
strokeWidth={2 * sf}
|
strokeWidth={2 * sf}
|
||||||
strokeDasharray={p.kilitli ? `${3 * sf} ${2.5 * sf}` : undefined}
|
strokeDasharray={p.kilitli ? `${4 * sf} ${3 * sf}` : undefined}
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x={p.x}
|
x={p.x}
|
||||||
y={p.y}
|
y={p.y}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dominantBaseline="central"
|
dominantBaseline="central"
|
||||||
fontSize={9 * sf}
|
fontSize={13 * sf}
|
||||||
fontWeight={700}
|
fontWeight={700}
|
||||||
fill="white"
|
fill="white"
|
||||||
className="pointer-events-none select-none"
|
className="pointer-events-none select-none"
|
||||||
@@ -456,13 +465,13 @@ export function TercihHaritasi({
|
|||||||
{etiket ? (
|
{etiket ? (
|
||||||
<text
|
<text
|
||||||
x={p.x}
|
x={p.x}
|
||||||
y={p.y - r - 4 * sf}
|
y={p.y - r - 6 * sf}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontSize={10.5 * sf}
|
fontSize={16 * sf}
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
fill="oklch(0.279 0.041 260.0)" // slate-800
|
fill="oklch(0.279 0.041 260.0)" // slate-800
|
||||||
stroke="white"
|
stroke="white"
|
||||||
strokeWidth={3 * sf}
|
strokeWidth={4 * sf}
|
||||||
paintOrder="stroke"
|
paintOrder="stroke"
|
||||||
className="pointer-events-none select-none"
|
className="pointer-events-none select-none"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -199,11 +199,14 @@ function KapıIcerik({
|
|||||||
if (!duzenle && !bekleyenVar && hedef !== "sihirbaz") {
|
if (!duzenle && !bekleyenVar && hedef !== "sihirbaz") {
|
||||||
toast.success("Tercihlerin kaydedildi.");
|
toast.success("Tercihlerin kaydedildi.");
|
||||||
}
|
}
|
||||||
// Düzenlemede replace: aynı /sonuc sayfasında sira/tur değişince geçmiş
|
// Düzenlemede replace (geçmiş şişmesin), ilk kurulumda push. Profil
|
||||||
// şişmesin; ilk kurulumda push ile hedefe gitsin.
|
// artık çerezden SSR'landığı ve URL çoğu zaman değişmediği için sunucu
|
||||||
|
// tablosunun yeni profille tazelenmesi her iki yolda da refresh ile
|
||||||
|
// garanti edilir (zaten /sonuc'tayken aynı URL'e push no-op kalabilir).
|
||||||
if (yonlendir) {
|
if (yonlendir) {
|
||||||
if (duzenle) router.replace(yonlendir);
|
if (duzenle) router.replace(yonlendir);
|
||||||
else router.push(yonlendir);
|
else router.push(yonlendir);
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export function sonucaGitIste(secenekler?: { dogrudanSira?: boolean }): {
|
|||||||
if (!durum.profilYuklendi) yukle();
|
if (!durum.profilYuklendi) yukle();
|
||||||
const profil = durum.profil ?? tercihProfiliOku();
|
const profil = durum.profil ?? tercihProfiliOku();
|
||||||
if (profil) {
|
if (profil) {
|
||||||
return { href: sonucHref(profil), kapiAcildi: false };
|
return { href: sonucHref(), kapiAcildi: false };
|
||||||
}
|
}
|
||||||
profilToplamaAc("sonuc", secenekler?.dogrudanSira ?? false);
|
profilToplamaAc("sonuc", secenekler?.dogrudanSira ?? false);
|
||||||
return { href: null, kapiAcildi: true };
|
return { href: null, kapiAcildi: true };
|
||||||
@@ -245,7 +245,6 @@ export function profilTamamlandi(
|
|||||||
profil: TercihProfili,
|
profil: TercihProfili,
|
||||||
girisli = false,
|
girisli = false,
|
||||||
): string | null {
|
): string | null {
|
||||||
const onceki = durum.profil;
|
|
||||||
const duzenle = durum.duzenle;
|
const duzenle = durum.duzenle;
|
||||||
riskleriYenile(profil);
|
riskleriYenile(profil);
|
||||||
const bekleyen = durum.bekleyen;
|
const bekleyen = durum.bekleyen;
|
||||||
@@ -268,20 +267,14 @@ export function profilTamamlandi(
|
|||||||
// Yapay Zeka niyetli giriş: kapıda sihirbazı zaten doldurdu, /sonuc'ta
|
// Yapay Zeka niyetli giriş: kapıda sihirbazı zaten doldurdu, /sonuc'ta
|
||||||
// akış kesilmesin — girişliyse üretim otomatik başlar (?sihirbaz=1),
|
// akış kesilmesin — girişliyse üretim otomatik başlar (?sihirbaz=1),
|
||||||
// girişsizse ücretsiz tablo + giriş CTA paneli (?hazir=1) karşılar.
|
// girişsizse ücretsiz tablo + giriş CTA paneli (?hazir=1) karşılar.
|
||||||
return sonucHref(profil, girisli ? { sihirbaz: "1" } : { hazir: "1" });
|
return sonucHref(girisli ? { sihirbaz: "1" } : { hazir: "1" });
|
||||||
}
|
|
||||||
if (hedef === "sonuc") return sonucHref(profil);
|
|
||||||
// Düzenlemede sıra/tür değişince /sonuc?sira= URL'si yenilenmezse sunucudaki
|
|
||||||
// hayal/dengeli/garanti tablosu eski sıralamada kalır (profil localStorage'da
|
|
||||||
// güncellenir ama sayfa searchParams'tan hesaplanır).
|
|
||||||
if (
|
|
||||||
duzenle &&
|
|
||||||
typeof window !== "undefined" &&
|
|
||||||
window.location.pathname.startsWith("/sonuc") &&
|
|
||||||
(onceki?.sira !== profil.sira || onceki?.tur !== profil.tur)
|
|
||||||
) {
|
|
||||||
return sonucHref(profil);
|
|
||||||
}
|
}
|
||||||
|
if (hedef === "sonuc") return sonucHref();
|
||||||
|
// Düzenleme /sonuc'a götürür: tablo profil çerezinden SSR'lanır
|
||||||
|
// (sira/tur/il/tip) ve tercihProfiliYaz çerezi az önce güncelledi. URL
|
||||||
|
// değişmediği için sunucu tablosunun tazelenmesi kapı bileşenindeki
|
||||||
|
// router.refresh ile garanti edilir.
|
||||||
|
if (duzenle) return sonucHref();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function RaporYokCta() {
|
|||||||
asChild
|
asChild
|
||||||
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
<Link href={profil ? sonucHref(profil, { sihirbaz: "1" }) : "/"}>
|
<Link href={profil ? sonucHref({ sihirbaz: "1" }) : "/"}>
|
||||||
24'lük listeni oluştur
|
24'lük listeni oluştur
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function KapanisCta() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="mt-8 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-8 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
<Link href={sonucHref(profil)}>
|
<Link href={sonucHref()}>
|
||||||
Üniversitelere göz at
|
Üniversitelere göz at
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export function DemoKapanisCta() {
|
|||||||
: "Kuralları öğrendin — şimdi kendi sıralamanla dene."}
|
: "Kuralları öğrendin — şimdi kendi sıralamanla dene."}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href={profil ? sonucHref(profil) : "/"}
|
href={profil ? sonucHref() : "/"}
|
||||||
className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
{profil ? "Sıralamana uygun programları gör" : "Sıralamanı gir"}
|
{profil ? "Sıralamana uygun programları gör" : "Sıralamanı gir"}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function ListeUretici({
|
|||||||
{profil ? (
|
{profil ? (
|
||||||
<p className="mt-4 text-xs text-slate-500">
|
<p className="mt-4 text-xs text-slate-500">
|
||||||
Seçimlerin kayıtlı —{" "}
|
Seçimlerin kayıtlı —{" "}
|
||||||
<Link href={sonucHref(profil)} className="underline">
|
<Link href={sonucHref()} className="underline">
|
||||||
sonuç sayfasına dönebilirsin
|
sonuç sayfasına dönebilirsin
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -28,7 +28,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(profil, { sihirbaz: "1" }) : "/"}>
|
<Link href={profil ? sonucHref({ sihirbaz: "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>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function TadimlikSatiri({
|
|||||||
const seri = raporSiraSerisi(p.siraGecmisi);
|
const seri = raporSiraSerisi(p.siraGecmisi);
|
||||||
const taban = p.efektifSira ?? programEtkinSira(seri);
|
const taban = p.efektifSira ?? programEtkinSira(seri);
|
||||||
const devlet = p.unitur === "DEVLET";
|
const devlet = p.unitur === "DEVLET";
|
||||||
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref({ sira, tur }))}`;
|
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref())}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section ref={gorunumRef} className="mt-16 min-w-0">
|
<section ref={gorunumRef} className="mt-16 min-w-0">
|
||||||
|
|||||||
@@ -123,9 +123,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
|||||||
const yeniProfil = { sira, tur, secimler };
|
const yeniProfil = { sira, tur, secimler };
|
||||||
tercihProfiliYaz(yeniProfil);
|
tercihProfiliYaz(yeniProfil);
|
||||||
setAcik(false);
|
setAcik(false);
|
||||||
router.push(
|
router.push(sonucHref(girisli ? { sihirbaz: "1" } : { hazir: "1" }));
|
||||||
sonucHref(yeniProfil, girisli ? { sihirbaz: "1" } : { hazir: "1" }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function acikDegisti(v: boolean) {
|
function acikDegisti(v: boolean) {
|
||||||
@@ -150,7 +148,7 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
|||||||
programların hazır.
|
programların hazır.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href={sonucHref(profil)}
|
href={sonucHref()}
|
||||||
className="mt-4 inline-flex h-12 items-center justify-center gap-2 rounded-full bg-orange-500 px-7 text-sm font-semibold text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-4 inline-flex h-12 items-center justify-center gap-2 rounded-full bg-orange-500 px-7 text-sm font-semibold text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
Sıralamana uygun programları gör
|
Sıralamana uygun programları gör
|
||||||
|
|||||||
@@ -100,9 +100,7 @@ export function HeroForm() {
|
|||||||
const profil = { sira, tur, secimler };
|
const profil = { sira, tur, secimler };
|
||||||
tercihProfiliYaz(profil);
|
tercihProfiliYaz(profil);
|
||||||
setModalAcik(false);
|
setModalAcik(false);
|
||||||
router.push(
|
router.push(sonucHref(girisli ? { sihirbaz: "1" } : { hazir: "1" }));
|
||||||
sonucHref(profil, girisli ? { sihirbaz: "1" } : { hazir: "1" }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kayıtlı profil varsa sihirbazı atla, doğrudan sonuç sayfasına götür.
|
// Kayıtlı profil varsa sihirbazı atla, doğrudan sonuç sayfasına götür.
|
||||||
@@ -116,7 +114,7 @@ export function HeroForm() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto"
|
className="w-full cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Link href={sonucHref(kayitliProfil)}>
|
<Link href={sonucHref()}>
|
||||||
Üniversitelere göz at
|
Üniversitelere göz at
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ export function SihirbazAdimlar({
|
|||||||
// eşleşme için iki taraf da normalizeIlAdi'dan geçirilir.
|
// eşleşme için iki taraf da normalizeIlAdi'dan geçirilir.
|
||||||
const [ilArama, setIlArama] = useState("");
|
const [ilArama, setIlArama] = useState("");
|
||||||
const ilAramaNorm = normalizeIlAdi(ilArama.trim());
|
const ilAramaNorm = normalizeIlAdi(ilArama.trim());
|
||||||
|
// 81 çip ilk bakışta boğucu; varsayılan görünüm ilk 10 il + "+N il daha"
|
||||||
|
// rozeti. Arama açıkken kırpma yok (arama zaten görünümü daraltıyor).
|
||||||
|
const [tumIllerAcik, setTumIllerAcik] = useState(false);
|
||||||
const gorunenTurkiyeIlleri = useMemo(
|
const gorunenTurkiyeIlleri = useMemo(
|
||||||
() =>
|
() =>
|
||||||
ilAramaNorm
|
ilAramaNorm
|
||||||
@@ -119,6 +122,14 @@ export function SihirbazAdimlar({
|
|||||||
: turkiyeIlleri,
|
: turkiyeIlleri,
|
||||||
[turkiyeIlleri, ilAramaNorm],
|
[turkiyeIlleri, ilAramaNorm],
|
||||||
);
|
);
|
||||||
|
const IL_ON_IZLEME = 10;
|
||||||
|
const ilListesiKirpik =
|
||||||
|
!ilAramaNorm &&
|
||||||
|
!tumIllerAcik &&
|
||||||
|
gorunenTurkiyeIlleri.length > IL_ON_IZLEME;
|
||||||
|
const listelenenTurkiyeIlleri = ilListesiKirpik
|
||||||
|
? gorunenTurkiyeIlleri.slice(0, IL_ON_IZLEME)
|
||||||
|
: gorunenTurkiyeIlleri;
|
||||||
const gorunenDisIller = useMemo(
|
const gorunenDisIller = useMemo(
|
||||||
() =>
|
() =>
|
||||||
ilAramaNorm
|
ilAramaNorm
|
||||||
@@ -314,15 +325,16 @@ export function SihirbazAdimlar({
|
|||||||
|
|
||||||
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
||||||
güvenilir dokunma hedefi değil); iller seçili kategorilere göre
|
güvenilir dokunma hedefi değil); iller seçili kategorilere göre
|
||||||
süzülmüş, program sayısına göre sıralı listelenir. Penceresinde
|
süzülmüş, program sayısına göre sıralı listelenir. Varsayılan
|
||||||
programı olan TÜM iller görünür (kırpma yok); arama yalnızca
|
görünüm ilk 10 il; kalanı "+N il daha" rozetiyle açılır. Arama
|
||||||
görünümü daraltır, seçimleri etkilemez. */}
|
yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı
|
||||||
|
devre dışı bırakır. */}
|
||||||
<div
|
<div
|
||||||
className={`mt-3 flex max-h-56 flex-wrap gap-1.5 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
className={`mt-3 flex max-h-56 flex-wrap gap-1.5 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
||||||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{gorunenTurkiyeIlleri.map((i) => (
|
{listelenenTurkiyeIlleri.map((i) => (
|
||||||
<Cip
|
<Cip
|
||||||
key={i.il}
|
key={i.il}
|
||||||
kucuk
|
kucuk
|
||||||
@@ -333,6 +345,27 @@ export function SihirbazAdimlar({
|
|||||||
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
|
{ilListesiKirpik ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTumIllerAcik(true)}
|
||||||
|
className="cursor-pointer rounded-full border border-dashed border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-500 transition-colors duration-200 hover:border-slate-400 hover:bg-slate-50 hover:text-slate-700"
|
||||||
|
>
|
||||||
|
+{gorunenTurkiyeIlleri.length - IL_ON_IZLEME} il daha
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{!ilListesiKirpik &&
|
||||||
|
tumIllerAcik &&
|
||||||
|
!ilAramaNorm &&
|
||||||
|
gorunenTurkiyeIlleri.length > IL_ON_IZLEME ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTumIllerAcik(false)}
|
||||||
|
className="cursor-pointer rounded-full border border-dashed border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-500 transition-colors duration-200 hover:border-slate-400 hover:bg-slate-50 hover:text-slate-700"
|
||||||
|
>
|
||||||
|
Daha az göster
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ilAramaNorm &&
|
{ilAramaNorm &&
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// /sonuc?sira= yokken: kayıtlı profil varsa oraya taşı; yoksa kapıyı aç.
|
// Profil çerezi yokken: localStorage'da kayıtlı profil varsa çereze taşıyıp
|
||||||
// Sıra bilgisi localStorage'daki TercihProfili ile tüm uygulamada ortak.
|
// sayfayı tazele (URL değişmez); yoksa kapıyı aç. Sıra bilgisi localStorage'daki
|
||||||
|
// TercihProfili ile tüm uygulamada ortak, sunucu tablosu çerezden okur.
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -9,7 +10,7 @@ import Link from "next/link";
|
|||||||
import { ArrowLeft, Trophy } from "lucide-react";
|
import { ArrowLeft, Trophy } 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 { sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { profilCerezineYaz } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
import {
|
import {
|
||||||
sonucaGitIste,
|
sonucaGitIste,
|
||||||
useProfilKapisi,
|
useProfilKapisi,
|
||||||
@@ -20,9 +21,14 @@ export function SiraGerekli() {
|
|||||||
const { profil, profilYuklendi } = useProfilKapisi();
|
const { profil, profilYuklendi } = useProfilKapisi();
|
||||||
|
|
||||||
// "Yönleniyor" durumu ayrı state gerektirmez: profil varken aşağıdaki koşul
|
// "Yönleniyor" durumu ayrı state gerektirmez: profil varken aşağıdaki koşul
|
||||||
// zaten bekleme ekranını gösteriyor; effect yalnızca yönlendirmeyi yapar.
|
// zaten bekleme ekranını gösteriyor. Çerezi olmayan eski kullanıcı (profil
|
||||||
|
// yalnızca localStorage'da) burada çereze taşınır; refresh sunucu tablosunu
|
||||||
|
// aynı URL'de yeni çerezle render eder.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profilYuklendi && profil) router.replace(sonucHref(profil));
|
if (profilYuklendi && profil) {
|
||||||
|
profilCerezineYaz(profil);
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
}, [profil, profilYuklendi, router]);
|
}, [profil, profilYuklendi, router]);
|
||||||
|
|
||||||
if (!profilYuklendi || profil) {
|
if (!profilYuklendi || profil) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Şema, doğrulama ve sabitler sihirbaz-sabitler.ts'te yaşar.
|
// Şema, doğrulama ve sabitler sihirbaz-sabitler.ts'te yaşar.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
PROFIL_CEREZ_ADI,
|
||||||
PROFIL_DEGISTI_EVENT,
|
PROFIL_DEGISTI_EVENT,
|
||||||
SIHIRBAZ_STORAGE_KEY,
|
SIHIRBAZ_STORAGE_KEY,
|
||||||
tercihProfiliDogrula,
|
tercihProfiliDogrula,
|
||||||
@@ -42,6 +43,24 @@ export function tercihProfiliOku(): TercihProfili | null {
|
|||||||
return profilCache;
|
return profilCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profilin sunucu aynası: /sonuc SSR'ı tabloyu bu çerezden filtreler
|
||||||
|
* (bkz. PROFIL_CEREZ_ADI). localStorage'ı olup çerezi olmayan eski
|
||||||
|
* kullanıcıların taşınması için SiraGerekli de bunu çağırır.
|
||||||
|
*/
|
||||||
|
export function profilCerezineYaz(profil: TercihProfili): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
const guvenli = window.location.protocol === "https:" ? "; secure" : "";
|
||||||
|
document.cookie = `${PROFIL_CEREZ_ADI}=${encodeURIComponent(
|
||||||
|
JSON.stringify(profil),
|
||||||
|
)}; path=/; max-age=31536000; samesite=lax${guvenli}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function profilCereziniSil(): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
document.cookie = `${PROFIL_CEREZ_ADI}=; path=/; max-age=0`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Ortak profili yazar; hero ve Yapay Zeka sihirbazıyla aynı anahtarı kullanır. */
|
/** Ortak profili yazar; hero ve Yapay Zeka sihirbazıyla aynı anahtarı kullanır. */
|
||||||
export function tercihProfiliYaz(profil: TercihProfili): void {
|
export function tercihProfiliYaz(profil: TercihProfili): void {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
@@ -50,6 +69,7 @@ export function tercihProfiliYaz(profil: TercihProfili): void {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(SIHIRBAZ_STORAGE_KEY, JSON.stringify(dogru));
|
localStorage.setItem(SIHIRBAZ_STORAGE_KEY, JSON.stringify(dogru));
|
||||||
} catch {}
|
} catch {}
|
||||||
|
profilCerezineYaz(dogru);
|
||||||
profilCache = dogru;
|
profilCache = dogru;
|
||||||
profilDegistiginiYayinla();
|
profilDegistiginiYayinla();
|
||||||
}
|
}
|
||||||
@@ -60,6 +80,7 @@ export function tercihProfiliSil(): void {
|
|||||||
try {
|
try {
|
||||||
localStorage.removeItem(SIHIRBAZ_STORAGE_KEY);
|
localStorage.removeItem(SIHIRBAZ_STORAGE_KEY);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
profilCereziniSil();
|
||||||
profilCache = null;
|
profilCache = null;
|
||||||
profilDegistiginiYayinla();
|
profilDegistiginiYayinla();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ export const PUAN_TURU_ETIKET: Record<string, string> = {
|
|||||||
// dönene kadar burada bekler.
|
// dönene kadar burada bekler.
|
||||||
export const SIHIRBAZ_STORAGE_KEY = "kolaytercih.sihirbaz";
|
export const SIHIRBAZ_STORAGE_KEY = "kolaytercih.sihirbaz";
|
||||||
|
|
||||||
|
// Profilin sunucuya giden kopyası: /sonuc tablosu SSR'da bu çerezden
|
||||||
|
// filtrelenir, URL'de sira/il/tip paramı taşınmaz. localStorage client
|
||||||
|
// tarafındaki tek doğruluk kaynağı olmayı sürdürür; çerez onun aynasıdır
|
||||||
|
// (tercihProfiliYaz/Sil ikisini birlikte günceller).
|
||||||
|
export const PROFIL_CEREZ_ADI = "kolaytercih.profil";
|
||||||
|
|
||||||
// storage olayı yalnızca diğer sekmelerde tetiklenir; aynı sekmedeki
|
// storage olayı yalnızca diğer sekmelerde tetiklenir; aynı sekmedeki
|
||||||
// aboneler (ör. navbar sıralama rozeti) bu olayla haberdar edilir.
|
// aboneler (ör. navbar sıralama rozeti) bu olayla haberdar edilir.
|
||||||
export const PROFIL_DEGISTI_EVENT = "kolaytercih:profil-degisti";
|
export const PROFIL_DEGISTI_EVENT = "kolaytercih:profil-degisti";
|
||||||
@@ -126,30 +132,27 @@ export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
|
|||||||
return { sira, tur: p.tur, secimler };
|
return { sira, tur: p.tur, secimler };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** /sonuc çerezindeki ham değeri güvenle profile çözer (SSR tarafı). */
|
||||||
|
export function cerezdenTercihProfili(
|
||||||
|
ham: string | undefined | null,
|
||||||
|
): TercihProfili | null {
|
||||||
|
if (!ham) return null;
|
||||||
|
try {
|
||||||
|
return tercihProfiliDogrula(JSON.parse(decodeURIComponent(ham)));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Profildeki sıra/tur ile sonuç sayfası URL'i — tüm uygulama aynı sorguyu
|
* Sonuç sayfası URL'i. Profil verisi (sıra/tür/il/tip) URL'de taşınmaz —
|
||||||
* kullanır. Profilde sihirbaz seçimleri varsa il + üniversite tipi de URL'e
|
* /sonuc'un ham tablosu SSR'da PROFIL_CEREZ_ADI çerezinden filtrelenir.
|
||||||
* yazılır: /sonuc'un ham tablosu SSR'da bu paramlarla filtrelenir (tek
|
* `ekstra` yalnızca tek seferlik akış bayrakları içindir (sihirbaz=1, hazir=1).
|
||||||
* doğruluk kaynağı URL; localStorage profili yalnızca URL'i üretir).
|
|
||||||
*/
|
*/
|
||||||
export function sonucHref(
|
export function sonucHref(ekstra?: Record<string, string>): string {
|
||||||
profil: Pick<TercihProfili, "sira" | "tur"> & {
|
const girdiler = Object.entries(ekstra ?? {});
|
||||||
secimler?: SihirbazSecimleri;
|
if (girdiler.length === 0) return "/sonuc";
|
||||||
},
|
return `/sonuc?${new URLSearchParams(girdiler)}`;
|
||||||
ekstra?: Record<string, string>,
|
|
||||||
): string {
|
|
||||||
const q = new URLSearchParams({
|
|
||||||
sira: String(profil.sira),
|
|
||||||
tur: profil.tur,
|
|
||||||
});
|
|
||||||
if (profil.secimler) {
|
|
||||||
for (const il of profil.secimler.iller) q.append("il", il);
|
|
||||||
if (profil.secimler.universiteTipi !== "farketmez") {
|
|
||||||
q.set("tip", profil.secimler.universiteTipi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [k, v] of Object.entries(ekstra ?? {})) q.set(k, v);
|
|
||||||
return `/sonuc?${q}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prompt'a enjekte edilecek Türkçe özet. */
|
/** Prompt'a enjekte edilecek Türkçe özet. */
|
||||||
|
|||||||
62
src/lib/harita-ulkeler-verisi.ts
Normal file
62
src/lib/harita-ulkeler-verisi.ts
Normal file
File diff suppressed because one or more lines are too long
115
src/lib/harita-ulkeler.ts
Normal file
115
src/lib/harita-ulkeler.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// Türkiye dışındaki (KKTC ve yurtdışı) üniversitelerin ülke haritası çözümü.
|
||||||
|
// Kontür verisi harita-ulkeler-verisi.ts'te (scripts/ulke-harita-uret.ts üretir).
|
||||||
|
|
||||||
|
import {
|
||||||
|
ULKE_HARITALARI,
|
||||||
|
type UlkeKodu,
|
||||||
|
type UlkeHarita,
|
||||||
|
} from "./harita-ulkeler-verisi";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ULKE_HARITALARI,
|
||||||
|
ULKE_HARITA_GENISLIK,
|
||||||
|
ULKE_HARITA_YUKSEKLIK,
|
||||||
|
} from "./harita-ulkeler-verisi";
|
||||||
|
export type { UlkeKodu, UlkeHarita } from "./harita-ulkeler-verisi";
|
||||||
|
|
||||||
|
/** Schema.org addressCountry için ISO 3166-1 alpha-2 kodları. */
|
||||||
|
export const ULKE_ISO: Record<UlkeKodu, string> = {
|
||||||
|
kktc: "CY",
|
||||||
|
azerbaycan: "AZ",
|
||||||
|
kirgizistan: "KG",
|
||||||
|
kazakistan: "KZ",
|
||||||
|
"bosna-hersek": "BA",
|
||||||
|
makedonya: "MK",
|
||||||
|
arnavutluk: "AL",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Sehir = { ulke: UlkeKodu; ad: string };
|
||||||
|
|
||||||
|
/** DB'deki il değerlerinden ve düzeltme tablosundan şehir anahtarları. */
|
||||||
|
const SEHIRLER: Record<string, Sehir> = {
|
||||||
|
GİRNE: { ulke: "kktc", ad: "Girne" },
|
||||||
|
LEFKOŞA: { ulke: "kktc", ad: "Lefkoşa" },
|
||||||
|
GAZİMAĞUSA: { ulke: "kktc", ad: "Gazimağusa" },
|
||||||
|
GÜZELYURT: { ulke: "kktc", ad: "Güzelyurt" },
|
||||||
|
LEFKE: { ulke: "kktc", ad: "Lefke" },
|
||||||
|
"BAKÜ-AZERBAYCAN": { ulke: "azerbaycan", ad: "Bakü" },
|
||||||
|
"BİŞKEK-KIRGIZİSTAN": { ulke: "kirgizistan", ad: "Bişkek" },
|
||||||
|
"TÜRKİSTAN-KAZAKİSTAN": { ulke: "kazakistan", ad: "Türkistan" },
|
||||||
|
"SARAYBOSNA - BOSNA - HERSEK": { ulke: "bosna-hersek", ad: "Saraybosna" },
|
||||||
|
ÜSKÜP: { ulke: "makedonya", ad: "Üsküp" },
|
||||||
|
TİRAN: { ulke: "arnavutluk", ad: "Tiran" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kaynak verideki gürültü için üniversite adına göre düzeltme:
|
||||||
|
* - Uluslararası Balkan Üniversitesi Üsküp'tedir; YÖK Atlas satırlarının
|
||||||
|
* çoğu il'i yanlışlıkla "SARAYBOSNA - BOSNA - HERSEK" der.
|
||||||
|
* - Tiran New York Üniversitesi'nin il alanı boştur.
|
||||||
|
* - ODTÜ ve ASBÜ'nün KKTC kampüs satırları vardır ama ana kampüs Türkiye'de;
|
||||||
|
* null → Türkiye haritasında kalır.
|
||||||
|
* Anahtar: uniAdiNormalize edilmiş adın tr-TR büyük harf hali.
|
||||||
|
*/
|
||||||
|
const UNI_DUZELTME: Record<string, keyof typeof SEHIRLER | null> = {
|
||||||
|
"ULUSLARARASI BALKAN ÜNİVERSİTESİ": "ÜSKÜP",
|
||||||
|
"TİRAN NEW YORK ÜNİVERSİTESİ": "TİRAN",
|
||||||
|
"KIBRIS AYDIN ÜNİVERSİTESİ": "GİRNE", // il alanı yalnızca "KIBRIS"
|
||||||
|
"ORTA DOĞU TEKNİK ÜNİVERSİTESİ": null,
|
||||||
|
"ANKARA SOSYAL BİLİMLER ÜNİVERSİTESİ": null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** il değeri Türkiye dışını mı gösteriyor? ("KIBRIS" genel değeri dahil) */
|
||||||
|
const YURTDISI_IL = new Set([...Object.keys(SEHIRLER), "KIBRIS"]);
|
||||||
|
|
||||||
|
export type UlkeKonum = {
|
||||||
|
ulke: UlkeKodu;
|
||||||
|
ulkeAd: string;
|
||||||
|
iso: string;
|
||||||
|
harita: UlkeHarita;
|
||||||
|
/** Şehir adı; genel "KIBRIS" gibi şehri bilinmeyen durumda null */
|
||||||
|
sehir: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sehirdenKonum(anahtar: keyof typeof SEHIRLER): UlkeKonum {
|
||||||
|
const sehir = SEHIRLER[anahtar];
|
||||||
|
const harita = ULKE_HARITALARI[sehir.ulke];
|
||||||
|
return {
|
||||||
|
ulke: sehir.ulke,
|
||||||
|
ulkeAd: harita.ad,
|
||||||
|
iso: ULKE_ISO[sehir.ulke],
|
||||||
|
harita,
|
||||||
|
sehir: sehir.ad,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Üniversite Türkiye dışındaysa ülke haritası + şehir konumu döner;
|
||||||
|
* Türkiye'deyse (veya çözülemiyorsa) null — Türkiye haritası kullanılır.
|
||||||
|
*/
|
||||||
|
export function uniUlkeKonum(
|
||||||
|
universite: string,
|
||||||
|
il: string | null,
|
||||||
|
): UlkeKonum | null {
|
||||||
|
const ad = universite
|
||||||
|
.replace(/\s*\([^)]*\)\s*$/, "")
|
||||||
|
.trim()
|
||||||
|
.toLocaleUpperCase("tr-TR");
|
||||||
|
|
||||||
|
if (ad in UNI_DUZELTME) {
|
||||||
|
const anahtar = UNI_DUZELTME[ad];
|
||||||
|
return anahtar === null ? null : sehirdenKonum(anahtar);
|
||||||
|
}
|
||||||
|
if (!il || !YURTDISI_IL.has(il)) return null;
|
||||||
|
if (il in SEHIRLER) return sehirdenKonum(il as keyof typeof SEHIRLER);
|
||||||
|
|
||||||
|
// il "KIBRIS": ülke bilinir, şehir bilinmez — haritayı işaretsiz göster
|
||||||
|
const harita = ULKE_HARITALARI.kktc;
|
||||||
|
return {
|
||||||
|
ulke: "kktc",
|
||||||
|
ulkeAd: harita.ad,
|
||||||
|
iso: ULKE_ISO.kktc,
|
||||||
|
harita,
|
||||||
|
sehir: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -259,16 +259,23 @@ export function getAllUniversiteler(): UniOzet[] {
|
|||||||
const map = uniMapGetir();
|
const map = uniMapGetir();
|
||||||
const rows = getDb()
|
const rows = getDb()
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT universite, MAX(il) AS il, MAX(unitur) AS unitur,
|
`SELECT universite, MAX(unitur) AS unitur, COUNT(*) AS adet
|
||||||
COUNT(*) AS adet
|
|
||||||
FROM programs GROUP BY universite`,
|
FROM programs GROUP BY universite`,
|
||||||
)
|
)
|
||||||
.all() as {
|
.all() as {
|
||||||
universite: string;
|
universite: string;
|
||||||
il: string | null;
|
|
||||||
unitur: string | null;
|
unitur: string | null;
|
||||||
adet: number;
|
adet: number;
|
||||||
}[];
|
}[];
|
||||||
|
// il: MAX(il) alfabetik en büyüğü seçip yanıltıyordu (ör. Ankara Sosyal
|
||||||
|
// Bilimler'in 12 KKTC satırı 47 Ankara satırını eziyordu); üniversitenin
|
||||||
|
// en çok programının bulunduğu il esas alınır.
|
||||||
|
const ilRows = getDb()
|
||||||
|
.prepare(
|
||||||
|
`SELECT universite, il, COUNT(*) AS adet FROM programs
|
||||||
|
WHERE il IS NOT NULL GROUP BY universite, il`,
|
||||||
|
)
|
||||||
|
.all() as { universite: string; il: string; adet: number }[];
|
||||||
// Fakülte sayısı: aynı üniversitenin iki yazımı ("X ÜNİV." / "X ÜNİV. (İL)")
|
// Fakülte sayısı: aynı üniversitenin iki yazımı ("X ÜNİV." / "X ÜNİV. (İL)")
|
||||||
// birleştiği için GROUP BY universite üzerinden saymak çift sayardı;
|
// birleştiği için GROUP BY universite üzerinden saymak çift sayardı;
|
||||||
// (slug, fakulte) çifti üzerinden kesin sayılır.
|
// (slug, fakulte) çifti üzerinden kesin sayılır.
|
||||||
@@ -296,9 +303,30 @@ export function getAllUniversiteler(): UniOzet[] {
|
|||||||
if (!slug) continue;
|
if (!slug) continue;
|
||||||
const u = bySlug.get(slug)!;
|
const u = bySlug.get(slug)!;
|
||||||
u.programSayisi += row.adet;
|
u.programSayisi += row.adet;
|
||||||
if (!u.il && row.il) u.il = row.il;
|
|
||||||
if (!u.unitur && row.unitur) u.unitur = row.unitur;
|
if (!u.unitur && row.unitur) u.unitur = row.unitur;
|
||||||
}
|
}
|
||||||
|
const ilAdetleri = new Map<string, Map<string, number>>();
|
||||||
|
for (const row of ilRows) {
|
||||||
|
const slug = slugByHam.get(row.universite);
|
||||||
|
if (!slug) continue;
|
||||||
|
let sayac = ilAdetleri.get(slug);
|
||||||
|
if (!sayac) {
|
||||||
|
sayac = new Map();
|
||||||
|
ilAdetleri.set(slug, sayac);
|
||||||
|
}
|
||||||
|
sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet);
|
||||||
|
}
|
||||||
|
for (const [slug, sayac] of ilAdetleri) {
|
||||||
|
let enIyi: string | null = null;
|
||||||
|
let enCok = 0;
|
||||||
|
for (const [il, adet] of sayac) {
|
||||||
|
if (adet > enCok) {
|
||||||
|
enIyi = il;
|
||||||
|
enCok = adet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bySlug.get(slug)!.il = enIyi;
|
||||||
|
}
|
||||||
const fakulteSetleri = new Map<string, Set<string>>();
|
const fakulteSetleri = new Map<string, Set<string>>();
|
||||||
for (const row of fakulteRows) {
|
for (const row of fakulteRows) {
|
||||||
const slug = slugByHam.get(row.universite);
|
const slug = slugByHam.get(row.universite);
|
||||||
|
|||||||
Reference in New Issue
Block a user