perf: TercihHaritasi dinamik + lazy sarmalayıcılara kapı/preload (Faz 3)
- tercih-haritasi-lazy: turkey-map-react il geometrisi (~197 KB) dynamic ssr:false + viewBox oranlı iskelet (CLS yok); /sonuc 1286->1099 KB, /listem 1503->1315 KB eager JS - manuel-harita: liste boşken harita hiç render edilmez (boş haritaya 197 KB iniyordu) — aynı oranlı placeholder alanı korur - program tablosu section'ına hover/focus preload (niyet anında ısıtma) - liste-cekmecesi/kaydet-banner/tercih-profili-kapisi lazy sarmalayıcıları kapı kazandı: koşul sağlanana dek chunk inmez; açan butonlara (listem-butonu, + butonu, navbar sıralama butonları) hover/focus preload - liste-cekmecesi: kullanılmayan DrawerDescription importu temizlendi Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
sonucaGitIste,
|
sonucaGitIste,
|
||||||
useTercihProfili,
|
useTercihProfili,
|
||||||
} from "@/features/liste/hooks/use-tercih-profili";
|
} from "@/features/liste/hooks/use-tercih-profili";
|
||||||
|
import { preloadTercihProfiliKapisi } from "@/features/liste/components/tercih-profili-kapisi-lazy";
|
||||||
import { PUAN_TURU_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { PUAN_TURU_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { olay } from "@/lib/analitik";
|
import { olay } from "@/lib/analitik";
|
||||||
import {
|
import {
|
||||||
@@ -256,6 +257,8 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={profilDuzenlemeyiAc}
|
onClick={profilDuzenlemeyiAc}
|
||||||
|
onPointerEnter={preloadTercihProfiliKapisi}
|
||||||
|
onFocus={preloadTercihProfiliKapisi}
|
||||||
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
|
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
|
||||||
className="group/profil flex cursor-pointer items-center gap-2 rounded-full py-1.5 pl-4 pr-1.5 transition-colors duration-150 hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none"
|
className="group/profil flex cursor-pointer items-center gap-2 rounded-full py-1.5 pl-4 pr-1.5 transition-colors duration-150 hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none"
|
||||||
>
|
>
|
||||||
@@ -282,6 +285,8 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={siralamaGirVeyaSonucaGit}
|
onClick={siralamaGirVeyaSonucaGit}
|
||||||
|
onPointerEnter={preloadTercihProfiliKapisi}
|
||||||
|
onFocus={preloadTercihProfiliKapisi}
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-full px-4 py-1.5 text-slate-700 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
className="flex cursor-pointer items-center gap-2 rounded-full px-4 py-1.5 text-slate-700 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900"
|
||||||
>
|
>
|
||||||
<Trophy className="size-4 text-orange-500" aria-hidden />
|
<Trophy className="size-4 text-orange-500" aria-hidden />
|
||||||
@@ -317,6 +322,8 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={profilDuzenlemeyiAc}
|
onClick={profilDuzenlemeyiAc}
|
||||||
|
onPointerEnter={preloadTercihProfiliKapisi}
|
||||||
|
onFocus={preloadTercihProfiliKapisi}
|
||||||
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
|
aria-label={`Sıralamanı ve tercihlerini değiştir (${profil.sira.toLocaleString("tr-TR")} · ${PUAN_TURU_ETIKET[profil.tur] ?? profil.tur})`}
|
||||||
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white py-1 pl-2.5 pr-1.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-2 sm:text-sm"
|
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white py-1 pl-2.5 pr-1.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-2 sm:text-sm"
|
||||||
>
|
>
|
||||||
@@ -342,6 +349,8 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={siralamaGirVeyaSonucaGit}
|
onClick={siralamaGirVeyaSonucaGit}
|
||||||
|
onPointerEnter={preloadTercihProfiliKapisi}
|
||||||
|
onFocus={preloadTercihProfiliKapisi}
|
||||||
aria-label="Başarı sıralamanı gir"
|
aria-label="Başarı sıralamanı gir"
|
||||||
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white py-1 pl-2.5 pr-2.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-3 sm:text-sm"
|
className="flex h-9 min-w-0 flex-1 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white py-1 pl-2.5 pr-2.5 text-xs font-semibold text-slate-900 transition-[background-color,transform] duration-150 hover:bg-slate-100 active:scale-[0.96] sm:h-12 sm:gap-2 sm:pl-3.5 sm:pr-3 sm:text-sm"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
|
|
||||||
// Banner yalnızca girişsiz + 3 seçimli kullanıcıda görünür; baseline
|
// Banner yalnızca girişsiz + 3 seçimli kullanıcıda görünür; baseline
|
||||||
// bundle'a girmesin diye ssr'siz dynamic import (liste-cekmecesi-lazy deseni).
|
// bundle'a girmesin diye ssr'siz dynamic import (liste-cekmecesi-lazy deseni).
|
||||||
|
// Kapı: liste eşiğin altındayken chunk hiç inmez. Giriş/pathname koşulları
|
||||||
|
// bileşenin kendisinde kalır — burada yalnızca en ayırt edici koşul var.
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useManuelListe } from "../liste-store";
|
||||||
|
|
||||||
export const KaydetBannerLazy = dynamic(
|
// kaydet-banner.tsx'teki ESIK ile aynı değer; oradan import etmek statik
|
||||||
|
// bağımlılıkla chunk'ı geri çekerdi.
|
||||||
|
const ESIK = 3;
|
||||||
|
|
||||||
|
const KaydetBannerIc = dynamic(
|
||||||
() => import("./kaydet-banner").then((m) => m.KaydetBanner),
|
() => import("./kaydet-banner").then((m) => m.KaydetBanner),
|
||||||
{ ssr: false },
|
{ ssr: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function KaydetBannerLazy() {
|
||||||
|
const liste = useManuelListe();
|
||||||
|
return liste.length >= ESIK ? <KaydetBannerIc /> : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,27 @@
|
|||||||
|
|
||||||
// Çekmece kapalıyken hiçbir şey çizmez; drag/Reorder kodunun baseline
|
// Çekmece kapalıyken hiçbir şey çizmez; drag/Reorder kodunun baseline
|
||||||
// bundle'a girmemesi için ssr'siz dynamic import (layout server component
|
// bundle'a girmemesi için ssr'siz dynamic import (layout server component
|
||||||
// olduğundan bu sarmalayıcı gerekli).
|
// olduğundan bu sarmalayıcı gerekli). Kapı: liste boş ve çekmece kapalıyken
|
||||||
|
// chunk hiç inmez — hydration'la birlikte koşulsuz inmesin (bundle-conditional).
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useCekmeceAcik, useManuelListe } from "../liste-store";
|
||||||
|
|
||||||
export const ListeCekmecesiLazy = dynamic(
|
const ListeCekmecesiIc = dynamic(
|
||||||
() => import("./liste-cekmecesi").then((m) => m.ListeCekmecesi),
|
() => import("./liste-cekmecesi").then((m) => m.ListeCekmecesi),
|
||||||
{ ssr: false },
|
{ ssr: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Navbar butonu hover/focus'unda chunk'ı önden ısıt (bundle-preload). */
|
||||||
|
export function preloadListeCekmecesi() {
|
||||||
|
void import("./liste-cekmecesi");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListeCekmecesiLazy() {
|
||||||
|
const acik = useCekmeceAcik();
|
||||||
|
const liste = useManuelListe();
|
||||||
|
// "+" uçuş animasyon katmanı da çekmece modülünde yaşadığı için liste
|
||||||
|
// doluysa (çekmece kapalı olsa da) mount kalır; yalnızca boş+kapalı durumda
|
||||||
|
// atlanır — ilk ziyaretin en yaygın hâli.
|
||||||
|
return acik || liste.length > 0 ? <ListeCekmecesiIc /> : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
Drawer,
|
Drawer,
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerDescription,
|
|
||||||
DrawerFooter,
|
DrawerFooter,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useCekmeceAcik,
|
useCekmeceAcik,
|
||||||
useManuelListe,
|
useManuelListe,
|
||||||
} from "../liste-store";
|
} from "../liste-store";
|
||||||
|
import { preloadListeCekmecesi } from "./liste-cekmecesi-lazy";
|
||||||
|
|
||||||
export function ListemButonu() {
|
export function ListemButonu() {
|
||||||
const liste = useManuelListe();
|
const liste = useManuelListe();
|
||||||
@@ -26,6 +27,8 @@ export function ListemButonu() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cekmeceDegistir}
|
onClick={cekmeceDegistir}
|
||||||
|
onPointerEnter={preloadListeCekmecesi}
|
||||||
|
onFocus={preloadListeCekmecesi}
|
||||||
aria-expanded={acik}
|
aria-expanded={acik}
|
||||||
aria-label={`Kendi listem — ${liste.length}/${MANUEL_LISTE_MAX} program`}
|
aria-label={`Kendi listem — ${liste.length}/${MANUEL_LISTE_MAX} program`}
|
||||||
title="Tablodan seçtiğin programlar"
|
title="Tablodan seçtiğin programlar"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
// abonedir; ekleme/çıkarma/sürükleme anında haritaya yansır.
|
// abonedir; ekleme/çıkarma/sürükleme anında haritaya yansır.
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { TercihHaritasi, type HaritaPin } from "@/features/liste/components/tercih-haritasi";
|
import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi-lazy";
|
||||||
|
import type { HaritaPin } from "@/features/liste/components/tercih-haritasi";
|
||||||
import { ilKonum, uniAdiNormalize, uniKonum } from "@/lib/harita";
|
import { ilKonum, uniAdiNormalize, uniKonum } from "@/lib/harita";
|
||||||
import type { RiskSeviyesi } from "@/lib/risk";
|
import type { RiskSeviyesi } from "@/lib/risk";
|
||||||
import { useManuelListe } from "../liste-store";
|
import { useManuelListe } from "../liste-store";
|
||||||
@@ -53,9 +54,24 @@ export function ManuelHarita() {
|
|||||||
return { pinler: [...gruplar.values()], haritaDisi };
|
return { pinler: [...gruplar.values()], haritaDisi };
|
||||||
}, [liste]);
|
}, [liste]);
|
||||||
|
|
||||||
|
// Liste boşken 197 KB'lık il geometrisi hiç inmesin: aynı oranlı
|
||||||
|
// placeholder alanı rezerve eder, ilk eklemede lazy harita yerine oturur.
|
||||||
|
if (liste.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TercihHaritasi
|
<div className="flex aspect-[1050/447] w-full items-center justify-center rounded-xl bg-slate-100/60">
|
||||||
|
<p className="max-w-xs px-4 text-center text-xs text-slate-500">
|
||||||
|
Tablodan <span aria-hidden>+</span> ile ekledikçe üniversiteler
|
||||||
|
haritada belirir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TercihHaritasiLazy
|
||||||
pinler={pinler}
|
pinler={pinler}
|
||||||
seciliIl={seciliIl}
|
seciliIl={seciliIl}
|
||||||
onSeciliIlDegisti={setSeciliIl}
|
onSeciliIlDegisti={setSeciliIl}
|
||||||
@@ -65,12 +81,6 @@ export function ManuelHarita() {
|
|||||||
{haritaDisi} tercih harita dışında (KKTC vb.)
|
{haritaDisi} tercih harita dışında (KKTC vb.)
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{liste.length === 0 ? (
|
|
||||||
<p className="mt-2 text-center text-xs text-slate-500">
|
|
||||||
Tablodan <span aria-hidden>+</span> ile ekledikçe üniversiteler
|
|
||||||
haritada belirir.
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type ManuelKaynak,
|
type ManuelKaynak,
|
||||||
} from "../liste-store";
|
} from "../liste-store";
|
||||||
import { programEkleIste } from "../hooks/use-tercih-profili";
|
import { programEkleIste } from "../hooks/use-tercih-profili";
|
||||||
|
import { preloadTercihProfiliKapisi } from "./tercih-profili-kapisi-lazy";
|
||||||
|
|
||||||
type ProgramOzet = Pick<
|
type ProgramOzet = Pick<
|
||||||
Program,
|
Program,
|
||||||
@@ -59,6 +60,9 @@ export function ProgramEkleButonu({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={listede || doldu}
|
disabled={listede || doldu}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
// Profilsiz kullanıcıda + kapıyı açar — chunk'ı niyet anında ısıt
|
||||||
|
onPointerEnter={preloadTercihProfiliKapisi}
|
||||||
|
onFocus={preloadTercihProfiliKapisi}
|
||||||
title={
|
title={
|
||||||
listede
|
listede
|
||||||
? "Zaten seçtiklerinde"
|
? "Zaten seçtiklerinde"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
|
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
|
||||||
|
import { preloadTercihHaritasi } from "@/features/liste/components/tercih-haritasi-lazy";
|
||||||
import { ProgramNetleriButonu } from "@/features/katalog/components/program-netleri";
|
import { ProgramNetleriButonu } from "@/features/katalog/components/program-netleri";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
|
|
||||||
@@ -146,7 +147,14 @@ export function ProgramTablosu({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mt-16" aria-label="Sıralamanla açılan programlar">
|
<section
|
||||||
|
className="mt-16"
|
||||||
|
aria-label="Sıralamanla açılan programlar"
|
||||||
|
// Tabloya niyet gösteren kullanıcı büyük ihtimalle + ile harita
|
||||||
|
// çizdirecek — chunk'ı görünmeden ısıt (bundle-preload)
|
||||||
|
onPointerEnter={preloadTercihHaritasi}
|
||||||
|
onFocusCapture={preloadTercihHaritasi}
|
||||||
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={aktifDilim}
|
value={aktifDilim}
|
||||||
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
onValueChange={(v) => setAktifDilim(v as DilimKey)}
|
||||||
|
|||||||
27
src/features/liste/components/tercih-haritasi-lazy.tsx
Normal file
27
src/features/liste/components/tercih-haritasi-lazy.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// turkey-map-react'ın il geometrisi (~197 KB) yalnızca harita gerçekten
|
||||||
|
// çizileceği zaman insin: dynamic + ssr'siz. İskelet, haritanın gerçek
|
||||||
|
// oranını (viewBox 1050 × 447, bkz. tercih-haritasi TAM_GORUNUM) rezerve
|
||||||
|
// eder — chunk gelince yerleşim zıplamaz.
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
export function HaritaIskeleti() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="aspect-[1050/447] w-full animate-pulse rounded-xl bg-slate-100"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TercihHaritasiLazy = dynamic(
|
||||||
|
() => import("./tercih-haritasi").then((m) => m.TercihHaritasi),
|
||||||
|
{ ssr: false, loading: () => <HaritaIskeleti /> },
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Hover/focus gibi niyet sinyallerinde chunk'ı önden ısıt. */
|
||||||
|
export function preloadTercihHaritasi() {
|
||||||
|
void import("./tercih-haritasi");
|
||||||
|
}
|
||||||
@@ -1,8 +1,28 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
// Sıra+sihirbaz kapısı ilk açılış isteğine kadar hiç inmez (bundle-conditional).
|
||||||
|
// Bir kez açıldıktan sonra mount kalır ki Dialog'un kapanış animasyonu ve
|
||||||
|
// oturum içi state'i bozulmasın.
|
||||||
|
|
||||||
export const TercihProfiliKapisiLazy = dynamic(
|
import { useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useProfilKapisi } from "../hooks/use-tercih-profili";
|
||||||
|
|
||||||
|
const TercihProfiliKapisiIc = dynamic(
|
||||||
() => import("./tercih-profili-kapisi").then((m) => m.TercihProfiliKapisi),
|
() => import("./tercih-profili-kapisi").then((m) => m.TercihProfiliKapisi),
|
||||||
{ ssr: false },
|
{ ssr: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Kapıyı açan butonların hover/focus'unda chunk'ı önden ısıt. */
|
||||||
|
export function preloadTercihProfiliKapisi() {
|
||||||
|
void import("./tercih-profili-kapisi");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TercihProfiliKapisiLazy() {
|
||||||
|
const { acik } = useProfilKapisi();
|
||||||
|
const [gerekli, setGerekli] = useState(false);
|
||||||
|
// Render sırasında koşullu setState: ilk açılışta mandallanır (React'in
|
||||||
|
// "adjust state during render" deseni — effect'e gerek yok).
|
||||||
|
if (acik && !gerekli) setGerekli(true);
|
||||||
|
return gerekli ? <TercihProfiliKapisiIc /> : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RaporListesi } from "./rapor-listesi";
|
import { RaporListesi } from "./rapor-listesi";
|
||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import { TercihHaritasi } from "@/features/liste/components/tercih-haritasi";
|
import { TercihHaritasiLazy } from "@/features/liste/components/tercih-haritasi-lazy";
|
||||||
import { pinleriTuret } from "@/lib/harita-pinler";
|
import { pinleriTuret } from "@/lib/harita-pinler";
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||||
import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client";
|
import { SohbetClient, type DanismanOzeti, type Mesaj } from "./sohbet-client";
|
||||||
@@ -92,7 +92,7 @@ export function ListemGovde({
|
|||||||
className="scroll-mt-24"
|
className="scroll-mt-24"
|
||||||
>
|
>
|
||||||
<div className="mx-auto w-full max-w-xl">
|
<div className="mx-auto w-full max-w-xl">
|
||||||
<TercihHaritasi
|
<TercihHaritasiLazy
|
||||||
pinler={pinVerisi.pinler}
|
pinler={pinVerisi.pinler}
|
||||||
seciliIl={seciliIl}
|
seciliIl={seciliIl}
|
||||||
onSeciliIlDegisti={setSeciliIl}
|
onSeciliIlDegisti={setSeciliIl}
|
||||||
|
|||||||
Reference in New Issue
Block a user