Enhance application performance and user experience by enabling component caching in next.config.ts, optimizing database queries in various pages, and implementing lazy loading for heavy components. Updated styles for smoother transitions and improved visual clarity in multiple components, including globals.css and landing sections.
All checks were successful
Deploy / deploy (push) Successful in 2m30s
All checks were successful
Deploy / deploy (push) Successful in 2m30s
This commit is contained in:
@@ -11,7 +11,10 @@ import {
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { SihirbazAdimlar } from "@/components/sihirbaz-adimlar";
|
||||
import {
|
||||
SihirbazAdimlarLazy,
|
||||
preloadSihirbazAdimlar,
|
||||
} from "@/components/sihirbaz-adimlar-lazy";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import type { SihirbazFacetleri } from "@/lib/db";
|
||||
import { SIHIRBAZ_STORAGE_KEY, type SihirbazSecimleri } from "@/lib/sihirbaz";
|
||||
@@ -33,9 +36,8 @@ export function HeroForm() {
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
const [modalAcik, setModalAcik] = useState(false);
|
||||
const [hata, setHata] = useState<string | null>(null);
|
||||
const [girisli, setGirisli] = useState(false);
|
||||
const router = useRouter();
|
||||
const { data: oturum } = authClient.useSession();
|
||||
const girisli = Boolean(oturum?.user);
|
||||
|
||||
// Yazarken binlik ayracıyla biçimle (85000 → 85.000)
|
||||
function siralamaDegisti(ham: string) {
|
||||
@@ -55,9 +57,15 @@ export function HeroForm() {
|
||||
}
|
||||
setYukleniyor(true);
|
||||
try {
|
||||
const res = await fetch(`/api/facetler?sira=${value}&tur=${tur}`);
|
||||
// Oturum bilgisi mount'ta değil burada, facet isteğiyle PARALEL çözülür:
|
||||
// landing ziyareti başına gereksiz auth turu atılmaz (sayfa statik kalır).
|
||||
const [res, oturum] = await Promise.all([
|
||||
fetch(`/api/facetler?sira=${value}&tur=${tur}`),
|
||||
authClient.getSession().catch(() => null),
|
||||
]);
|
||||
if (!res.ok) throw new Error();
|
||||
const veri = (await res.json()) as { facetler: SihirbazFacetleri };
|
||||
setGirisli(Boolean(oturum?.data?.user));
|
||||
setSira(value);
|
||||
setFacetler(veri.facetler);
|
||||
setModalAcik(true);
|
||||
@@ -127,6 +135,7 @@ export function HeroForm() {
|
||||
placeholder="YKS başarı sıralaman (ör. 85.000)"
|
||||
value={siralama}
|
||||
onChange={(e) => siralamaDegisti(e.target.value)}
|
||||
onFocus={preloadSihirbazAdimlar}
|
||||
aria-invalid={hata ? true : undefined}
|
||||
aria-describedby={hata ? "siralama-hata" : undefined}
|
||||
// flex-1 yalnızca yatay dizilimde (sm+): mobilde form flex-col olduğu
|
||||
@@ -162,7 +171,7 @@ export function HeroForm() {
|
||||
Sıralamana uygun seçimlerini yap, tercih planına geç.
|
||||
</DialogDescription>
|
||||
{facetler ? (
|
||||
<SihirbazAdimlar
|
||||
<SihirbazAdimlarLazy
|
||||
key={`${sira}-${tur}`}
|
||||
facetler={facetler}
|
||||
sonButonEtiketi={
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Landing bölümlerinin görünüme girerken yumuşakça belirmesi.
|
||||
* Yalnızca transform/opacity animasyonu yapar; hareket azaltılmışsa
|
||||
* kaydırma yerine sadece kısa bir belirme kalır.
|
||||
* Motion yerine CSS transition + IntersectionObserver: SSR HTML'i görünür
|
||||
* gelir (JS'siz/yavaş istemcide sayfa boş kalmaz), gizleme yalnızca
|
||||
* `@media (scripting: enabled)` altında yaşar (bkz. globals.css .kt-reveal).
|
||||
*/
|
||||
export function Reveal({
|
||||
children,
|
||||
@@ -19,20 +20,38 @@ export function Reveal({
|
||||
y?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [gorunur, setGorunur] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || typeof IntersectionObserver === "undefined") {
|
||||
setGorunur(true);
|
||||
return;
|
||||
}
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((e) => e.isIntersecting)) {
|
||||
setGorunur(true);
|
||||
io.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "0px 0px -80px 0px" },
|
||||
);
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
initial={{ opacity: 0, y: reduceMotion ? 0 : y }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "0px 0px -80px 0px" }}
|
||||
transition={{
|
||||
duration: reduceMotion ? 0.25 : 0.55,
|
||||
delay,
|
||||
ease: [0.21, 0.47, 0.32, 0.98],
|
||||
}}
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("kt-reveal", gorunur && "kt-reveal-in", className)}
|
||||
style={{
|
||||
transitionDelay: delay ? `${delay}s` : undefined,
|
||||
...(y !== 28 ? { "--reveal-y": `${y}px` } : null),
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/manuel-liste/liste-cekmecesi-lazy.tsx
Normal file
12
src/components/manuel-liste/liste-cekmecesi-lazy.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
// Çekmece kapalıyken hiçbir şey çizmez; drag/Reorder kodunun baseline
|
||||
// bundle'a girmemesi için ssr'siz dynamic import (layout server component
|
||||
// olduğundan bu sarmalayıcı gerekli).
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const ListeCekmecesiLazy = dynamic(
|
||||
() => import("./liste-cekmecesi").then((m) => m.ListeCekmecesi),
|
||||
{ ssr: false },
|
||||
);
|
||||
@@ -58,13 +58,18 @@ export function ScrollFlow({
|
||||
return `skewY(${v}deg)`;
|
||||
});
|
||||
|
||||
if (reduceMotion) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
// Hareket azaltılmışsa da AYNI ağaç render edilir (server/client farklı
|
||||
// ağaç hydration uyumsuzluğu yaratıyordu); efektler koşullu kapatılır:
|
||||
// Lenis tekerlek yumuşatması kapalı, skew transform'u hiç bağlanmaz.
|
||||
return (
|
||||
<ReactLenis root options={{ lerp: 0.09, anchors: true }}>
|
||||
<motion.div className={className} style={{ transform }}>
|
||||
<ReactLenis
|
||||
root
|
||||
options={{ lerp: 0.09, anchors: true, smoothWheel: !reduceMotion }}
|
||||
>
|
||||
<motion.div
|
||||
className={className}
|
||||
style={reduceMotion ? undefined : { transform }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</ReactLenis>
|
||||
|
||||
28
src/components/sihirbaz-adimlar-lazy.tsx
Normal file
28
src/components/sihirbaz-adimlar-lazy.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
// SihirbazAdimlar, turkey-map-react'in ~235 KB'lık il geometrisini taşır.
|
||||
// Modal kapalıyken bu yükün ilk sayfa bundle'ına girmemesi için tek noktadan
|
||||
// dynamic import: hem hero-form hem sonuc/sihirbaz-modal burayı kullanır.
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const SihirbazAdimlarLazy = dynamic(
|
||||
() =>
|
||||
import("@/components/sihirbaz-adimlar").then((m) => m.SihirbazAdimlar),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="flex min-h-64 items-center justify-center text-sm text-slate-500"
|
||||
>
|
||||
Sihirbaz hazırlanıyor…
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
/** Kullanıcı forma dokunduğu anda chunk'ı arka planda indir. */
|
||||
export function preloadSihirbazAdimlar() {
|
||||
void import("@/components/sihirbaz-adimlar");
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { UserNav } from "@/components/user-nav";
|
||||
import { UserNav, UserNavFallback } from "@/components/user-nav";
|
||||
|
||||
const navLinks = [
|
||||
{ href: "/#nasil-calisir", label: "Nasıl çalışır?" },
|
||||
@@ -42,7 +43,10 @@ export function SiteHeader() {
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<UserNav />
|
||||
{/* UserNav session+DB bekler; Suspense dışına taşarsa tüm kabuk bloklanır */}
|
||||
<Suspense fallback={<UserNavFallback />}>
|
||||
<UserNav />
|
||||
</Suspense>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { codeToHtml } from "shiki"
|
||||
|
||||
export type CodeBlockProps = {
|
||||
children?: React.ReactNode
|
||||
@@ -47,6 +46,9 @@ function CodeBlockCode({
|
||||
return
|
||||
}
|
||||
|
||||
// Shiki ~380 KB: yalnızca gerçekten bir kod bloğu çizilirken indir;
|
||||
// o ana dek aşağıdaki düz <pre> fallback'i görünür.
|
||||
const { codeToHtml } = await import("shiki")
|
||||
const html = await codeToHtml(code, { lang: language, theme })
|
||||
setHighlightedHtml(html)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
* SVG katmanları çoğunlukla pointer-events-none olduğundan imleç window
|
||||
* üzerinden izlenir ve viewBox koordinatına çevrilir. Beklenen veri
|
||||
* öznitelikleri: data-px, data-cx, data-cy, data-op (taban opaklık).
|
||||
*
|
||||
* Landing'de 16 örnek yaşar: her örneğin kendi window dinleyicisi yerine
|
||||
* modül seviyesinde TEK pointermove dinleyicisi + rAF birleştirmesi kullanılır;
|
||||
* kare başına en fazla bir tarama yapılır, imleçten uzak örnekler hiç
|
||||
* piksel iterasyonuna girmez.
|
||||
*/
|
||||
|
||||
import { useEffect, type RefObject } from "react";
|
||||
@@ -26,6 +31,99 @@ type HeatTarget = {
|
||||
baseOpacity: number;
|
||||
};
|
||||
|
||||
type HeatInstance = {
|
||||
svg: SVGSVGElement;
|
||||
viewW: number;
|
||||
radius: number;
|
||||
baseColor: string;
|
||||
pixels: HeatTarget[];
|
||||
hot: Set<SVGRectElement>;
|
||||
controls: Map<SVGRectElement, AnimationPlaybackControls>;
|
||||
};
|
||||
|
||||
const instances = new Set<HeatInstance>();
|
||||
let sonOlay: PointerEvent | null = null;
|
||||
let rafId = 0;
|
||||
|
||||
function calistir(
|
||||
inst: HeatInstance,
|
||||
el: SVGRectElement,
|
||||
to: { fill: string; fillOpacity: number },
|
||||
duration: number,
|
||||
) {
|
||||
inst.controls.get(el)?.stop();
|
||||
inst.controls.set(el, animate(el, to, { duration, ease: "easeOut" }));
|
||||
}
|
||||
|
||||
function tara() {
|
||||
rafId = 0;
|
||||
const e = sonOlay;
|
||||
if (!e) return;
|
||||
for (const inst of instances) {
|
||||
const rect = inst.svg.getBoundingClientRect();
|
||||
if (rect.width === 0) continue;
|
||||
const scale = rect.width / inst.viewW;
|
||||
const pad = inst.radius * scale;
|
||||
// İmleç fırça menzilinin tamamen dışındaysa ve sıcak piksel yoksa
|
||||
// bu örnek için hiç hesap yapma
|
||||
if (
|
||||
inst.hot.size === 0 &&
|
||||
(e.clientX < rect.left - pad ||
|
||||
e.clientX > rect.right + pad ||
|
||||
e.clientY < rect.top - pad ||
|
||||
e.clientY > rect.bottom + pad)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const x = (e.clientX - rect.left) / scale;
|
||||
const y = (e.clientY - rect.top) / scale;
|
||||
for (const p of inst.pixels) {
|
||||
const d2 = (p.cx - x) ** 2 + (p.cy - y) ** 2;
|
||||
const isHot = d2 <= inst.radius * inst.radius;
|
||||
if (isHot && !inst.hot.has(p.el)) {
|
||||
inst.hot.add(p.el);
|
||||
calistir(
|
||||
inst,
|
||||
p.el,
|
||||
{ fill: HOT_COLOR, fillOpacity: HOT_OPACITY },
|
||||
HEAT_DURATION,
|
||||
);
|
||||
} else if (!isHot && inst.hot.has(p.el)) {
|
||||
inst.hot.delete(p.el);
|
||||
calistir(
|
||||
inst,
|
||||
p.el,
|
||||
{ fill: inst.baseColor, fillOpacity: p.baseOpacity },
|
||||
COOL_DURATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMove(e: PointerEvent) {
|
||||
sonOlay = e;
|
||||
if (!rafId) rafId = requestAnimationFrame(tara);
|
||||
}
|
||||
|
||||
function kaydol(inst: HeatInstance) {
|
||||
if (instances.size === 0) {
|
||||
window.addEventListener("pointermove", onMove, { passive: true });
|
||||
}
|
||||
instances.add(inst);
|
||||
}
|
||||
|
||||
function ayril(inst: HeatInstance) {
|
||||
instances.delete(inst);
|
||||
inst.controls.forEach((c) => c.stop());
|
||||
if (instances.size === 0) {
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = 0;
|
||||
sonOlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function usePixelHeat(
|
||||
svgRef: RefObject<SVGSVGElement | null>,
|
||||
{
|
||||
@@ -42,7 +140,6 @@ export function usePixelHeat(
|
||||
const viewW = svg.viewBox.baseVal.width;
|
||||
if (!viewW) return;
|
||||
|
||||
const baseColor = getComputedStyle(svg).color;
|
||||
const pixels: HeatTarget[] = Array.from(
|
||||
svg.querySelectorAll<SVGRectElement>("[data-px]"),
|
||||
).map((el) => ({
|
||||
@@ -53,61 +150,16 @@ export function usePixelHeat(
|
||||
}));
|
||||
if (pixels.length === 0) return;
|
||||
|
||||
const hot = new Set<SVGRectElement>();
|
||||
const controls = new Map<SVGRectElement, AnimationPlaybackControls>();
|
||||
|
||||
const run = (
|
||||
el: SVGRectElement,
|
||||
to: { fill: string; fillOpacity: number },
|
||||
duration: number,
|
||||
) => {
|
||||
controls.get(el)?.stop();
|
||||
controls.set(el, animate(el, to, { duration, ease: "easeOut" }));
|
||||
};
|
||||
|
||||
const onMove = (e: PointerEvent) => {
|
||||
const rect = svg.getBoundingClientRect();
|
||||
if (rect.width === 0) return;
|
||||
const scale = rect.width / viewW;
|
||||
const pad = radius * scale;
|
||||
// İmleç fırça menzilinin tamamen dışındaysa ve sıcak piksel yoksa
|
||||
// hiç hesap yapma (sayfada birden çok örnek dinlediği için önemli)
|
||||
if (
|
||||
hot.size === 0 &&
|
||||
(e.clientX < rect.left - pad ||
|
||||
e.clientX > rect.right + pad ||
|
||||
e.clientY < rect.top - pad ||
|
||||
e.clientY > rect.bottom + pad)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const x = (e.clientX - rect.left) / scale;
|
||||
const y = (e.clientY - rect.top) / scale;
|
||||
for (const p of pixels) {
|
||||
const d2 = (p.cx - x) ** 2 + (p.cy - y) ** 2;
|
||||
const isHot = d2 <= radius * radius;
|
||||
if (isHot && !hot.has(p.el)) {
|
||||
hot.add(p.el);
|
||||
run(
|
||||
p.el,
|
||||
{ fill: HOT_COLOR, fillOpacity: HOT_OPACITY },
|
||||
HEAT_DURATION,
|
||||
);
|
||||
} else if (!isHot && hot.has(p.el)) {
|
||||
hot.delete(p.el);
|
||||
run(
|
||||
p.el,
|
||||
{ fill: baseColor, fillOpacity: p.baseOpacity },
|
||||
COOL_DURATION,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", onMove, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
controls.forEach((c) => c.stop());
|
||||
const inst: HeatInstance = {
|
||||
svg,
|
||||
viewW,
|
||||
radius,
|
||||
baseColor: getComputedStyle(svg).color,
|
||||
pixels,
|
||||
hot: new Set(),
|
||||
controls: new Map(),
|
||||
};
|
||||
kaydol(inst);
|
||||
return () => ayril(inst);
|
||||
}, [svgRef, radius]);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@ import { Button } from "@/components/ui/button";
|
||||
import { ListemButonu } from "@/components/manuel-liste/listem-butonu";
|
||||
import { SignOutButton } from "./sign-out-button";
|
||||
|
||||
/**
|
||||
* Suspense fallback'i: girişsiz görünümle aynı yükseklik/genişlikte sessiz
|
||||
* bir pill — session sorgusu akarken header zıplamasın (CLS yok).
|
||||
*/
|
||||
export function UserNavFallback() {
|
||||
return (
|
||||
<div className="flex items-center gap-2" aria-hidden>
|
||||
<div className="h-12 w-28 rounded-full bg-slate-900/10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function UserNav() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user