perf: sunucu waterfall'ları ve blocking analitik düzeltildi (Faz 1)
Vercel React Best Practices denetimi, sunucu tarafı: - odeme: analitik olayı after() ile yanıt sonrasına alındı; ucuz ürün doğrulaması oturum turundan öne çekildi; sonuç kartında oturum+sipariş paralel okunuyor - /api/soru POST: gövde+oturum ve 3 bağımsız sorgu Promise.all'a bağlandı - rehber: prod'da globalThis cache + özetler için frontmatter fast-path (24 dosya okuma + marked.parse her render'da tekrarlanıyordu); arama kutusuna giden payload RehberAramaKaydi'ya kırpıldı (tarih serileşmiyor) - /sonuc hattı: ProgramListeSatiri (fakulte/puan2025 client'a serileşmiyor) - katalog: universite/bolum listelerinde O(n²) filtreler tek geçişe indi - sira-gerekli + katalog-arama: effect'te setState kalktı (lint yeşil) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,15 +34,26 @@ function KategoriGruplari({
|
||||
bolumler: BolumOzet[];
|
||||
seviye: "lisans" | "onlisans";
|
||||
}) {
|
||||
const kalanlar = new Set(bolumler.map((b) => b.slug));
|
||||
const gruplar = KATEGORILER.map((kategori) => {
|
||||
const uyeler = bolumler.filter((b) =>
|
||||
(b.kategoriler as readonly string[]).includes(kategori),
|
||||
);
|
||||
for (const b of uyeler) kalanlar.delete(b.slug);
|
||||
return { kategori: kategori as string, uyeler };
|
||||
}).filter((g) => g.uyeler.length > 0);
|
||||
const diger = bolumler.filter((b) => kalanlar.has(b.slug));
|
||||
// Tek geçişte kategori index'i — kategori×bölüm çapraz filter yerine.
|
||||
// Bir bölüm birden çok kategoride görünebilir; hiçbirinde yoksa "Diğer"e düşer.
|
||||
const uyeMap = new Map<string, BolumOzet[]>();
|
||||
const diger: BolumOzet[] = [];
|
||||
const gecerli = new Set<string>(KATEGORILER);
|
||||
for (const b of bolumler) {
|
||||
let eslesti = false;
|
||||
for (const kategori of b.kategoriler as readonly string[]) {
|
||||
if (!gecerli.has(kategori)) continue;
|
||||
const grup = uyeMap.get(kategori);
|
||||
if (grup) grup.push(b);
|
||||
else uyeMap.set(kategori, [b]);
|
||||
eslesti = true;
|
||||
}
|
||||
if (!eslesti) diger.push(b);
|
||||
}
|
||||
const gruplar = KATEGORILER.filter((k) => uyeMap.has(k)).map((kategori) => ({
|
||||
kategori: kategori as string,
|
||||
uyeler: uyeMap.get(kategori)!,
|
||||
}));
|
||||
if (diger.length > 0) gruplar.push({ kategori: "Diğer", uyeler: diger });
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AramaFunnelKarti } from "@/components/arama-funnel-karti";
|
||||
import { RehberKapak } from "@/features/rehber/components/rehber-kapak";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { aramaNormalize, eslesmePuani } from "@/lib/arama";
|
||||
import type { RehberOzet } from "@/lib/rehber";
|
||||
import type { RehberAramaKaydi } from "@/lib/rehber";
|
||||
import {
|
||||
profilDuzenlemeyiAc,
|
||||
sonucaGitIste,
|
||||
@@ -60,6 +67,18 @@ const BOS_SONUCLAR: AramaSonuclari = { bolumler: [], universiteler: [] };
|
||||
const REHBER_SONUC_SINIRI = 4;
|
||||
const sayi = (deger: number) => deger.toLocaleString("tr-TR");
|
||||
|
||||
// Platform değişmez — abonelik gerektirmeyen dış değer (useSyncExternalStore)
|
||||
const bosAbonelik = () => () => {};
|
||||
const macMiSunucu = () => true;
|
||||
function macMiOku(): boolean {
|
||||
const platform =
|
||||
(navigator as { userAgentData?: { platform?: string } }).userAgentData
|
||||
?.platform ??
|
||||
navigator.platform ??
|
||||
"";
|
||||
return /mac|iphone|ipad/i.test(platform);
|
||||
}
|
||||
|
||||
const KESFET_TONLARI = {
|
||||
slate: {
|
||||
pencere: "border-slate-200/80 bg-white",
|
||||
@@ -81,7 +100,7 @@ const KESFET_TONLARI = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
||||
export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
||||
const router = useRouter();
|
||||
const profil = useTercihProfili();
|
||||
const [acik, setAcik] = useState(false);
|
||||
@@ -95,7 +114,10 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
||||
const [sorgu, setSorgu] = useState("");
|
||||
const [sonuclar, setSonuclar] = useState<AramaSonuclari>(BOS_SONUCLAR);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const [macMi, setMacMi] = useState(true);
|
||||
// Kısayol etiketi (⌘/Ctrl) platforma bağlı sabit bir dış değer: effect'te
|
||||
// setState yerine useSyncExternalStore — SSR'da ⌘ (eski varsayılan),
|
||||
// hydration sonrası gerçek platform.
|
||||
const macMi = useSyncExternalStore(bosAbonelik, macMiOku, macMiSunucu);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const kaydirmaRef = useRef<HTMLDivElement>(null);
|
||||
const [kenarBlur, setKenarBlur] = useState({ ust: false, alt: false });
|
||||
@@ -139,15 +161,6 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
|
||||
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") {
|
||||
@@ -722,7 +735,7 @@ function RehberSonucKarti({
|
||||
yazi,
|
||||
onSec,
|
||||
}: {
|
||||
yazi: RehberOzet;
|
||||
yazi: RehberAramaKaydi;
|
||||
onSec: () => void;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -36,13 +36,15 @@ function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
|
||||
|
||||
export function UniversiteKatalogu() {
|
||||
const hepsi = getAllUniversiteler();
|
||||
const devlet = hepsi.filter((u) => u.unitur === "DEVLET");
|
||||
const vakif = hepsi.filter(
|
||||
(u) => u.unitur != null && ["VAKIF", "VAKIF MYO"].includes(u.unitur),
|
||||
);
|
||||
const digerUniler = hepsi.filter(
|
||||
(u) => !devlet.includes(u) && !vakif.includes(u),
|
||||
);
|
||||
// Tek geçişte üç grup — filter×3 + includes'lı O(n²) tarama yerine
|
||||
const devlet: UniOzet[] = [];
|
||||
const vakif: UniOzet[] = [];
|
||||
const digerUniler: UniOzet[] = [];
|
||||
for (const u of hepsi) {
|
||||
if (u.unitur === "DEVLET") devlet.push(u);
|
||||
else if (u.unitur === "VAKIF" || u.unitur === "VAKIF MYO") vakif.push(u);
|
||||
else digerUniler.push(u);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user