Enhance university-related components by adding logo support and improving search functionality. Introduced a new "logolar" script in package.json, updated KatalogArama to include rehber results, and integrated UniLogo component across various pages for better visual representation. Refactored UniversiteIcerik and TercihHaritasi to display logos conditionally, enhancing user experience. Additionally, made adjustments to styles and layout for improved responsiveness and clarity.
All checks were successful
Deploy / deploy (push) Successful in 16m24s
All checks were successful
Deploy / deploy (push) Successful in 16m24s
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
ProgramTabanTrendi,
|
||||
} from "@/components/program-liste-verileri";
|
||||
import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -162,39 +163,46 @@ export function BolumProgramListesi({
|
||||
return (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="max-w-0 w-full">
|
||||
{p.universiteSlug ? (
|
||||
<Link
|
||||
href={`/universite/${p.universiteSlug}`}
|
||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{uniAd}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
{uniAd}
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
||||
{p.unitur ? (
|
||||
<span
|
||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||
devlet
|
||||
? "bg-slate-100 text-slate-600"
|
||||
: "bg-violet-50 text-violet-600"
|
||||
}`}
|
||||
>
|
||||
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
|
||||
</span>
|
||||
<span className="flex min-w-0 items-center gap-2.5">
|
||||
{p.universiteSlug ? (
|
||||
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
|
||||
) : null}
|
||||
<span className="truncate">
|
||||
{p.il ? trBaslikDuzeni(p.il) : null}
|
||||
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
<span className="shrink-0 text-slate-400">
|
||||
· {p.sure} yıl
|
||||
<span className="min-w-0 flex-1">
|
||||
{p.universiteSlug ? (
|
||||
<Link
|
||||
href={`/universite/${p.universiteSlug}`}
|
||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{uniAd}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
{uniAd}
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
||||
{p.unitur ? (
|
||||
<span
|
||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||
devlet
|
||||
? "bg-slate-100 text-slate-600"
|
||||
: "bg-violet-50 text-violet-600"
|
||||
}`}
|
||||
>
|
||||
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate">
|
||||
{p.il ? trBaslikDuzeni(p.il) : null}
|
||||
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
<span className="shrink-0 text-slate-400">
|
||||
· {p.sure} yıl
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { RehberKapak } from "@/components/rehber-kapak";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { aramaNormalize, eslesmePuani } from "@/lib/arama";
|
||||
import type { RehberOzet } from "@/lib/rehber";
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpenText,
|
||||
@@ -16,6 +20,8 @@ import {
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Kbd, KbdGroup } from "@/components/ui/kbd";
|
||||
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
|
||||
|
||||
type BolumSonucu = {
|
||||
href: string;
|
||||
@@ -40,18 +46,72 @@ type AramaSonuclari = {
|
||||
};
|
||||
|
||||
const BOS_SONUCLAR: AramaSonuclari = { bolumler: [], universiteler: [] };
|
||||
const REHBER_SONUC_SINIRI = 4;
|
||||
const sayi = (deger: number) => deger.toLocaleString("tr-TR");
|
||||
|
||||
export function KatalogArama() {
|
||||
export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
||||
const [acik, setAcik] = useState(false);
|
||||
const [sorgu, setSorgu] = useState("");
|
||||
const [sonuclar, setSonuclar] = useState<AramaSonuclari>(BOS_SONUCLAR);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const [macMi, setMacMi] = useState(true);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const kaydirmaRef = useRef<HTMLDivElement>(null);
|
||||
const [kenarBlur, setKenarBlur] = useState({ ust: false, alt: false });
|
||||
const temizSorgu = sorgu.trim();
|
||||
const aramaAktif = temizSorgu.length >= 2;
|
||||
|
||||
const kenarBlurGuncelle = useCallback(() => {
|
||||
const alan = kaydirmaRef.current;
|
||||
if (!alan) return;
|
||||
const ust = alan.scrollTop > 4;
|
||||
const alt = alan.scrollTop + alan.clientHeight < alan.scrollHeight - 4;
|
||||
setKenarBlur((onceki) =>
|
||||
onceki.ust === ust && onceki.alt === alt ? onceki : { ust, alt },
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Rehber özetleri build sırasında server'dan prop olarak gelir (~10 yazı);
|
||||
// arama client'ta yapılır — prod imajında content/ dizini bulunmaz.
|
||||
const rehberSonuclari = useMemo(() => {
|
||||
if (!aramaAktif) return [];
|
||||
const normalSorgu = aramaNormalize(temizSorgu).slice(0, 80);
|
||||
return rehberler
|
||||
.filter((yazi) =>
|
||||
aramaNormalize(`${yazi.baslik} ${yazi.aciklama}`).includes(normalSorgu),
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
eslesmePuani(a.baslik, normalSorgu) -
|
||||
eslesmePuani(b.baslik, normalSorgu),
|
||||
)
|
||||
.slice(0, REHBER_SONUC_SINIRI);
|
||||
}, [aramaAktif, temizSorgu, rehberler]);
|
||||
|
||||
const sonucVar =
|
||||
sonuclar.bolumler.length > 0 || sonuclar.universiteler.length > 0;
|
||||
sonuclar.bolumler.length > 0 ||
|
||||
sonuclar.universiteler.length > 0 ||
|
||||
rehberSonuclari.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
const platform =
|
||||
(navigator as { userAgentData?: { platform?: string } }).userAgentData
|
||||
?.platform ??
|
||||
navigator.platform ??
|
||||
"";
|
||||
setMacMi(/mac|iphone|ipad/i.test(platform));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function kisayolDinle(event: KeyboardEvent) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
||||
event.preventDefault();
|
||||
modalDegisti(!acik);
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", kisayolDinle);
|
||||
return () => window.removeEventListener("keydown", kisayolDinle);
|
||||
}, [acik]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!acik || !aramaAktif) return;
|
||||
@@ -81,6 +141,19 @@ export function KatalogArama() {
|
||||
};
|
||||
}, [acik, aramaAktif, temizSorgu]);
|
||||
|
||||
// Sonuçlar, logolar ve kapak görselleri yüklendikçe scrollHeight değişir;
|
||||
// kenar blur görünürlüğünü scroll olayına ek olarak boyut değişiminde de tazele.
|
||||
useEffect(() => {
|
||||
if (!acik) return;
|
||||
const alan = kaydirmaRef.current;
|
||||
if (!alan) return;
|
||||
kenarBlurGuncelle();
|
||||
const gozlemci = new ResizeObserver(kenarBlurGuncelle);
|
||||
gozlemci.observe(alan);
|
||||
for (const cocuk of alan.children) gozlemci.observe(cocuk);
|
||||
return () => gozlemci.disconnect();
|
||||
}, [acik, aramaAktif, yukleniyor, sonucVar, kenarBlurGuncelle]);
|
||||
|
||||
function modalDegisti(yeniAcik: boolean) {
|
||||
setAcik(yeniAcik);
|
||||
if (!yeniAcik) {
|
||||
@@ -95,7 +168,8 @@ export function KatalogArama() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 sm:flex">
|
||||
{/* lg altında logo + sağ küme ile çakışır (absolute); o aralıkta gizli */}
|
||||
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-1 rounded-full border border-slate-200 bg-white p-1.5 text-base font-medium text-slate-600 lg:flex">
|
||||
<Link
|
||||
href="/#nasil-calisir"
|
||||
className="rounded-full px-5 py-2 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||||
@@ -115,14 +189,20 @@ export function KatalogArama() {
|
||||
>
|
||||
<Search className="size-4" aria-hidden />
|
||||
Ara
|
||||
<KbdGroup aria-hidden className="ml-1">
|
||||
<Kbd className="bg-white/15 text-white/85">
|
||||
{macMi ? "⌘" : "Ctrl"}
|
||||
</Kbd>
|
||||
<Kbd className="bg-white/15 text-white/85">K</Kbd>
|
||||
</KbdGroup>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAcik(true)}
|
||||
aria-label="Bölüm veya üniversite ara"
|
||||
className="ml-auto flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-700 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:hidden"
|
||||
aria-label="Bölüm, üniversite veya rehber ara"
|
||||
className="ml-auto flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-700 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:size-12 lg:hidden"
|
||||
>
|
||||
<Search className="size-4" aria-hidden />
|
||||
</button>
|
||||
@@ -134,13 +214,14 @@ export function KatalogArama() {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
className="max-h-[min(720px,calc(100dvh-2rem))] gap-0 overflow-hidden rounded-2xl bg-white p-0 duration-200 sm:max-w-2xl motion-reduce:data-open:zoom-in-100 motion-reduce:data-closed:zoom-out-100"
|
||||
className="max-h-[min(720px,calc(100dvh-2rem))] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden rounded-2xl bg-white p-0 duration-200 sm:max-w-2xl motion-reduce:data-open:zoom-in-100 motion-reduce:data-closed:zoom-out-100"
|
||||
>
|
||||
<DialogTitle className="sr-only">
|
||||
Bölüm ve üniversite ara
|
||||
Bölüm, üniversite ve rehber ara
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
YKS bölümlerini ve üniversiteleri tek yerden ara.
|
||||
YKS bölümlerini, üniversiteleri ve rehber yazılarını tek yerden
|
||||
ara.
|
||||
</DialogDescription>
|
||||
|
||||
<div className="flex items-center border-b border-slate-200 px-4 sm:px-5">
|
||||
@@ -158,8 +239,8 @@ export function KatalogArama() {
|
||||
setSonuclar(BOS_SONUCLAR);
|
||||
setYukleniyor(yeniSorgu.trim().length >= 2);
|
||||
}}
|
||||
placeholder="Bölüm veya üniversite ara..."
|
||||
aria-label="Bölüm veya üniversite ara"
|
||||
placeholder="Bölüm, üniversite veya rehber ara..."
|
||||
aria-label="Bölüm, üniversite veya rehber ara"
|
||||
autoComplete="off"
|
||||
className="h-16 min-w-0 flex-1 bg-transparent text-base text-slate-950 outline-none placeholder:text-slate-400 sm:h-18 sm:text-lg"
|
||||
/>
|
||||
@@ -173,10 +254,13 @@ export function KatalogArama() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="min-h-72 overflow-y-auto overscroll-contain p-3 sm:p-4"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="relative min-h-0">
|
||||
<div
|
||||
ref={kaydirmaRef}
|
||||
onScroll={kenarBlurGuncelle}
|
||||
className="h-full max-h-full min-h-72 overflow-y-auto overscroll-contain p-3 sm:p-4"
|
||||
aria-live="polite"
|
||||
>
|
||||
{!aramaAktif ? (
|
||||
<BaslangicIcerigi onSec={kapat} />
|
||||
) : yukleniyor ? (
|
||||
@@ -214,6 +298,13 @@ export function KatalogArama() {
|
||||
<SonucLinki
|
||||
key={universite.href}
|
||||
href={universite.href}
|
||||
gorsel={
|
||||
<UniLogo
|
||||
slug={universite.href.split("/").pop() ?? ""}
|
||||
ad={universite.ad}
|
||||
boy="sm"
|
||||
/>
|
||||
}
|
||||
baslik={universite.ad}
|
||||
meta={
|
||||
[universite.il, universite.tur]
|
||||
@@ -226,6 +317,23 @@ export function KatalogArama() {
|
||||
))}
|
||||
</SonucGrubu>
|
||||
) : null}
|
||||
|
||||
{rehberSonuclari.length > 0 ? (
|
||||
<SonucGrubu
|
||||
baslik="Rehber yazıları"
|
||||
ikon={<BookOpenText className="size-4" aria-hidden />}
|
||||
>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{rehberSonuclari.map((yazi) => (
|
||||
<RehberSonucKarti
|
||||
key={yazi.slug}
|
||||
yazi={yazi}
|
||||
onSec={kapat}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SonucGrubu>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-64 flex-col items-center justify-center px-6 text-center">
|
||||
@@ -236,15 +344,42 @@ export function KatalogArama() {
|
||||
"{temizSorgu}" için sonuç bulunamadı
|
||||
</p>
|
||||
<p className="mt-1 max-w-sm text-sm leading-6 text-slate-500">
|
||||
Bölüm veya üniversite adını farklı yazarak tekrar dene.
|
||||
Bölüm, üniversite veya rehber konusunu farklı yazarak tekrar
|
||||
dene.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-hidden
|
||||
className={`pointer-events-none absolute inset-x-0 top-0 h-14 transition-opacity duration-200 ${kenarBlur.ust ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
<ProgressiveBlur
|
||||
position="top"
|
||||
backgroundColor="#ffffff"
|
||||
height="56px"
|
||||
blurAmount="4px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className={`pointer-events-none absolute inset-x-0 bottom-0 h-14 transition-opacity duration-200 ${kenarBlur.alt ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
<ProgressiveBlur
|
||||
position="bottom"
|
||||
backgroundColor="#ffffff"
|
||||
height="56px"
|
||||
blurAmount="4px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-slate-200 bg-slate-50/80 px-4 py-3 text-xs text-slate-500 sm:px-5">
|
||||
<span>En az 2 harf yazarak ara</span>
|
||||
<span className="hidden sm:inline">Esc ile kapat</span>
|
||||
<span className="hidden items-center gap-1.5 sm:flex">
|
||||
<Kbd>Esc</Kbd> ile kapat
|
||||
</span>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -349,12 +484,14 @@ function SonucLinki({
|
||||
baslik,
|
||||
meta,
|
||||
detay,
|
||||
gorsel,
|
||||
onSec,
|
||||
}: {
|
||||
href: string;
|
||||
baslik: string;
|
||||
meta: string;
|
||||
detay: string;
|
||||
gorsel?: React.ReactNode;
|
||||
onSec: () => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -363,6 +500,7 @@ function SonucLinki({
|
||||
onClick={onSec}
|
||||
className="group flex items-center gap-3 rounded-xl px-3 py-3 transition-colors duration-150 hover:bg-slate-100"
|
||||
>
|
||||
{gorsel}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-semibold text-slate-900">
|
||||
{baslik}
|
||||
@@ -382,6 +520,36 @@ function SonucLinki({
|
||||
);
|
||||
}
|
||||
|
||||
function RehberSonucKarti({
|
||||
yazi,
|
||||
onSec,
|
||||
}: {
|
||||
yazi: RehberOzet;
|
||||
onSec: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={`/rehber/${yazi.slug}`}
|
||||
onClick={onSec}
|
||||
className="group overflow-hidden rounded-xl border border-slate-200 bg-white p-1.5 transition-[border-color,background-color,transform] duration-150 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.99]"
|
||||
>
|
||||
<RehberKapak baslik={yazi.baslik} compact className="rounded-lg" />
|
||||
<span className="block px-2 pb-1.5 pt-2.5">
|
||||
<span className="line-clamp-2 text-xs leading-5 text-slate-600">
|
||||
{yazi.aciklama}
|
||||
</span>
|
||||
<span className="mt-2 inline-flex items-center gap-1 text-xs font-semibold text-primary">
|
||||
Yazıyı oku
|
||||
<ArrowRight
|
||||
className="size-3.5 transition-transform duration-150 group-hover:translate-x-0.5"
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function AramaIskeleti() {
|
||||
return (
|
||||
<div className="space-y-2 px-2 pt-2" aria-label="Aranıyor">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import {
|
||||
GripVertical,
|
||||
ListChecks,
|
||||
@@ -332,6 +333,7 @@ function CekmeceSatiri({
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-[11px] font-bold tabular-nums">
|
||||
{sira}
|
||||
</span>
|
||||
<UniLogo ad={tercih.universite} boy="xs" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
{tercih.isim}
|
||||
|
||||
@@ -32,7 +32,8 @@ export function ListemButonu() {
|
||||
className="relative inline-flex h-9 cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-full border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 transition-[color,background-color,border-color,transform] duration-200 hover:border-slate-300 hover:bg-slate-50 active:scale-[0.97] sm:h-12 sm:gap-2 sm:px-5 sm:text-sm"
|
||||
>
|
||||
<ListChecks className="size-3.5 text-primary sm:size-4" aria-hidden />
|
||||
<span className="hidden sm:inline">Seçtiklerim</span>
|
||||
{/* lg–xl: ortadaki nav ile yer dar — metin gizli, ikon+rozet kalır */}
|
||||
<span className="hidden sm:inline lg:hidden xl:inline">Seçtiklerim</span>
|
||||
{liste.length > 0 ? (
|
||||
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
|
||||
<motion.span
|
||||
|
||||
@@ -44,14 +44,11 @@ export function ProgramEkleButonu({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const sonuc = programEkleIste({ program, kaynak, adaySira, adayTur });
|
||||
if (sonuc.eklendi) {
|
||||
toast.success("Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik.");
|
||||
} else if (sonuc.doldu) {
|
||||
if (sonuc.doldu) {
|
||||
toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`);
|
||||
} else if (sonuc.zatenVar) {
|
||||
toast.info("Bu program zaten seçtiklerinde.");
|
||||
}
|
||||
// kapiAcildi: modal açılır, toast yok
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
@@ -198,19 +199,22 @@ function SecimSatiri({
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-slate-900">
|
||||
{tercih.isim}
|
||||
</p>
|
||||
<p className="truncate text-xs text-slate-500">
|
||||
{tercih.universite}
|
||||
{tercih.il
|
||||
? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}`
|
||||
: ""}
|
||||
{tercih.efektifSira != null
|
||||
? ` · ~${tercih.efektifSira.toLocaleString("tr-TR")}.`
|
||||
: ""}
|
||||
</p>
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<UniLogo ad={tercih.universite} boy="sm" />
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-slate-900">
|
||||
{tercih.isim}
|
||||
</p>
|
||||
<p className="truncate text-xs text-slate-500">
|
||||
{tercih.universite}
|
||||
{tercih.il
|
||||
? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}`
|
||||
: ""}
|
||||
{tercih.efektifSira != null
|
||||
? ` · ~${tercih.efektifSira.toLocaleString("tr-TR")}.`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -152,11 +152,7 @@ function KapıIcerik({
|
||||
toast.success(
|
||||
"Tercihlerin güncellendi. Listendeki programları yeni seçimlerine göre yeniden değerlendirdik; hiçbir programı silmedik.",
|
||||
);
|
||||
} else if (bekleyenVar) {
|
||||
toast.success(
|
||||
"Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik.",
|
||||
);
|
||||
} else {
|
||||
} else if (!bekleyenVar) {
|
||||
toast.success("Tercihlerin kaydedildi.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import type { RaporSonuc } from "@/lib/ai/rapor";
|
||||
import {
|
||||
RISK_ETIKET,
|
||||
@@ -231,7 +232,11 @@ export function RaporListesi({
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-xs font-bold">
|
||||
{t.sira}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="flex min-w-0 items-center gap-2.5">
|
||||
{p?.universite ? (
|
||||
<UniLogo ad={p.universite} boy="sm" className="hidden sm:flex" />
|
||||
) : null}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
{p?.isim ?? t.programId}
|
||||
</span>
|
||||
@@ -244,6 +249,7 @@ export function RaporListesi({
|
||||
<span>2025: {sira2025?.toLocaleString("tr-TR") ?? "—"}</span>
|
||||
{trendG ? <span className={trendG.renk}>{trendG.etiket}</span> : null}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="hidden text-right text-xs tabular-nums text-slate-600 xl:block">
|
||||
{sira2024?.toLocaleString("tr-TR") ?? "—"}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { KatalogArama } from "@/components/katalog-arama";
|
||||
import { UserNav, UserNavFallback } from "@/components/user-nav";
|
||||
import { getAllRehberler } from "@/lib/rehber";
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
@@ -29,7 +30,7 @@ export function SiteHeader() {
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<KatalogArama />
|
||||
<KatalogArama rehberler={getAllRehberler()} />
|
||||
{/* UserNav session+DB bekler; Suspense dışına taşarsa tüm kabuk bloklanır */}
|
||||
<Suspense fallback={<UserNavFallback />}>
|
||||
<UserNav />
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Image from "next/image";
|
||||
import { ArrowLeft, Lock } from "lucide-react";
|
||||
import { cities } from "turkey-map-react/lib/data";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { uniLogoYoluFromAd } from "@/components/uni-logo";
|
||||
import {
|
||||
HARITA_GENISLIK,
|
||||
HARITA_UST_KIRPMA,
|
||||
@@ -296,7 +298,10 @@ export function TercihHaritasi({
|
||||
{/* İl seviyesi: üniversite pinleri */}
|
||||
{zoomda
|
||||
? acikPinler.map((p) => {
|
||||
const r = (p.kilitli ? 7 : 8) * sf;
|
||||
// Kilitli pinlerde logo da gösterilmez — üniversite kimliği
|
||||
// client'a inmediği gibi görselden de sızmamalı.
|
||||
const logo = p.kilitli ? null : uniLogoYoluFromAd(p.universite);
|
||||
const r = (p.kilitli ? 7 : logo ? 11 : 8) * sf;
|
||||
const etiket = p.kilitli ? null : kisaUniAdi(p.universite);
|
||||
return (
|
||||
<g
|
||||
@@ -314,27 +319,71 @@ export function TercihHaritasi({
|
||||
: `${etiket}: ${p.tercihSiralari.length} tercih`
|
||||
}
|
||||
>
|
||||
<circle
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={r}
|
||||
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
|
||||
stroke="white"
|
||||
strokeWidth={2 * sf}
|
||||
strokeDasharray={p.kilitli ? `${3 * sf} ${2.5 * sf}` : undefined}
|
||||
/>
|
||||
<text
|
||||
x={p.x}
|
||||
y={p.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={9 * sf}
|
||||
fontWeight={700}
|
||||
fill="white"
|
||||
className="pointer-events-none select-none"
|
||||
>
|
||||
{p.kilitli ? "?" : p.tercihSiralari.length}
|
||||
</text>
|
||||
{logo ? (
|
||||
<>
|
||||
<circle
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={r}
|
||||
fill="white"
|
||||
stroke={RISK_RENK[p.risk]}
|
||||
strokeWidth={2 * sf}
|
||||
/>
|
||||
<image
|
||||
href={logo}
|
||||
x={p.x - 8 * sf}
|
||||
y={p.y - 8 * sf}
|
||||
width={16 * sf}
|
||||
height={16 * sf}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<circle
|
||||
cx={p.x + r * 0.78}
|
||||
cy={p.y - r * 0.78}
|
||||
r={4.5 * sf}
|
||||
fill={RISK_RENK[p.risk]}
|
||||
stroke="white"
|
||||
strokeWidth={1.2 * sf}
|
||||
/>
|
||||
<text
|
||||
x={p.x + r * 0.78}
|
||||
y={p.y - r * 0.78}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={6 * sf}
|
||||
fontWeight={700}
|
||||
fill="white"
|
||||
className="pointer-events-none select-none"
|
||||
>
|
||||
{p.tercihSiralari.length}
|
||||
</text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<circle
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r={r}
|
||||
fill={p.kilitli ? "oklch(0.704 0.04 256.8)" : RISK_RENK[p.risk]}
|
||||
stroke="white"
|
||||
strokeWidth={2 * sf}
|
||||
strokeDasharray={p.kilitli ? `${3 * sf} ${2.5 * sf}` : undefined}
|
||||
/>
|
||||
<text
|
||||
x={p.x}
|
||||
y={p.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={9 * sf}
|
||||
fontWeight={700}
|
||||
fill="white"
|
||||
className="pointer-events-none select-none"
|
||||
>
|
||||
{p.kilitli ? "?" : p.tercihSiralari.length}
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
{etiket ? (
|
||||
<text
|
||||
x={p.x}
|
||||
@@ -401,8 +450,20 @@ export function TercihHaritasi({
|
||||
Kilitli tercih
|
||||
</span>
|
||||
) : (
|
||||
<span className="max-w-40 truncate font-medium text-slate-700">
|
||||
{kisaUniAdi(pin.universite)}
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
{uniLogoYoluFromAd(pin.universite) ? (
|
||||
<Image
|
||||
src={uniLogoYoluFromAd(pin.universite)!}
|
||||
alt=""
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden
|
||||
className="size-4 shrink-0 object-contain"
|
||||
/>
|
||||
) : null}
|
||||
<span className="max-w-40 truncate font-medium text-slate-700">
|
||||
{kisaUniAdi(pin.universite)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
26
src/components/ui/kbd.tsx
Normal file
26
src/components/ui/kbd.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd"
|
||||
className={cn(
|
||||
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
className={cn("inline-flex items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Kbd, KbdGroup }
|
||||
116
src/components/uni-logo.tsx
Normal file
116
src/components/uni-logo.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||
import { turkishSlugify } from "@/lib/slug";
|
||||
|
||||
/**
|
||||
* Üniversite logosu; dosyası olmayanlar için ad baş harflerinden monogram.
|
||||
* Logolar 300×300 PNG (public/uni-logo, kaynak YÖK Atlas) — beyaz zeminli
|
||||
* kapsayıcı hem şeffaf hem beyaz arka planlı logolarda tutarlı görünür.
|
||||
*/
|
||||
|
||||
const BOYUT: Record<"xs" | "sm" | "md" | "lg", string> = {
|
||||
xs: "size-7 rounded-md p-0.5",
|
||||
sm: "size-9 rounded-lg p-1",
|
||||
md: "size-12 rounded-xl p-1.5",
|
||||
lg: "size-16 rounded-2xl p-2 sm:size-20",
|
||||
};
|
||||
|
||||
const MONOGRAM_METIN: Record<"xs" | "sm" | "md" | "lg", string> = {
|
||||
xs: "text-[10px]",
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-lg sm:text-xl",
|
||||
};
|
||||
|
||||
// "İstanbul Teknik Üniversitesi" → "İT"; tür sözcükleri baş harfe girmez
|
||||
const TUR_SOZCUKLERI = new Set([
|
||||
"üniversitesi",
|
||||
"üniversite",
|
||||
"meslek",
|
||||
"yüksekokulu",
|
||||
"yüksek",
|
||||
"okulu",
|
||||
"ve",
|
||||
]);
|
||||
|
||||
function monogram(ad: string): string {
|
||||
const harfler = ad
|
||||
.split(/\s+/)
|
||||
.filter((w) => w && !TUR_SOZCUKLERI.has(w.toLocaleLowerCase("tr-TR")))
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0].toLocaleUpperCase("tr-TR"));
|
||||
return harfler.join("") || ad.slice(0, 1).toLocaleUpperCase("tr-TR");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ham DB adından logo yolu: "(İL)" eki atılıp slug'lanır (katalog.ts ile
|
||||
* aynı anahtar). Haritalar gibi next/image kullanamayan SVG bağlamları için.
|
||||
*/
|
||||
export function uniLogoYoluFromAd(ad: string): string | null {
|
||||
return uniLogoYolu(turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim()));
|
||||
}
|
||||
|
||||
// Slug'dan deterministik pastel ton — aynı üniversite her yerde aynı renk
|
||||
function tonu(slug: string): number {
|
||||
let h = 0;
|
||||
for (let i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) % 360;
|
||||
return h;
|
||||
}
|
||||
|
||||
export function UniLogo({
|
||||
slug,
|
||||
ad,
|
||||
boy = "sm",
|
||||
className,
|
||||
}: {
|
||||
/** Verilmezse ham addan türetilir ("(İL)" eki atılıp slug'lanır) */
|
||||
slug?: string;
|
||||
ad: string;
|
||||
boy?: "xs" | "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}) {
|
||||
const anahtar =
|
||||
slug ?? turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim());
|
||||
const yol = uniLogoYolu(anahtar);
|
||||
const px = boy === "lg" ? 80 : boy === "md" ? 48 : boy === "sm" ? 36 : 28;
|
||||
|
||||
if (!yol) {
|
||||
const h = tonu(anahtar);
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"flex shrink-0 select-none items-center justify-center font-heading font-bold ring-1 ring-inset ring-slate-900/10",
|
||||
BOYUT[boy],
|
||||
MONOGRAM_METIN[boy],
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `oklch(0.95 0.03 ${h})`,
|
||||
color: `oklch(0.45 0.09 ${h})`,
|
||||
}}
|
||||
>
|
||||
{monogram(ad)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex shrink-0 items-center justify-center bg-white ring-1 ring-inset ring-slate-900/10",
|
||||
BOYUT[boy],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={yol}
|
||||
alt={`${ad} logosu`}
|
||||
width={px}
|
||||
height={px}
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
HARITA_UST_KIRPMA,
|
||||
HARITA_YUKSEKLIK,
|
||||
normalizeIlAdi,
|
||||
uniKonum,
|
||||
} from "@/lib/harita";
|
||||
|
||||
export function UniversiteKonumHaritasi({
|
||||
@@ -14,8 +13,7 @@ export function UniversiteKonumHaritasi({
|
||||
universite: string;
|
||||
il: string | null;
|
||||
}) {
|
||||
const konum = uniKonum(universite, il);
|
||||
if (!konum) return null;
|
||||
if (!il) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -51,23 +49,6 @@ export function UniversiteKonumHaritasi({
|
||||
</path>
|
||||
);
|
||||
})}
|
||||
<g aria-hidden>
|
||||
<circle
|
||||
cx={konum.x}
|
||||
cy={konum.y}
|
||||
r={18}
|
||||
fill="oklch(0.623 0.188 259.8 / 18%)"
|
||||
/>
|
||||
<circle
|
||||
cx={konum.x}
|
||||
cy={konum.y}
|
||||
r={9}
|
||||
fill="oklch(0.623 0.188 259.8)"
|
||||
stroke="white"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<circle cx={konum.x} cy={konum.y} r={2.5} fill="white" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -45,7 +45,10 @@ export async function UserNav() {
|
||||
>
|
||||
<Coins className="size-3.5 text-amber-500 sm:size-4" aria-hidden />
|
||||
<span className="tabular-nums">{u.creditBalance}</span>
|
||||
<span className="hidden sm:inline">kredi</span>
|
||||
{/* Etiket yalnızca md–lg arası: daha darda logo kesiliyor, lg üstünde
|
||||
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
|
||||
title her genişlikte anlamı taşıyor. */}
|
||||
<span className="hidden md:inline lg:hidden">kredi</span>
|
||||
</Link>
|
||||
<SignOutButton />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user