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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user