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:
@@ -162,6 +162,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Landing bölümlerinin görünüme girerken belirmesi (bkz. landing/reveal.tsx).
|
||||
İçerik SSR'da GÖRÜNÜR; gizleme yalnızca script çalışan tarayıcılarda ve
|
||||
IO sınıfı ekleyene kadar geçerli — JS'siz istemcide sayfa asla boş kalmaz. */
|
||||
@media (scripting: enabled) {
|
||||
.kt-reveal {
|
||||
opacity: 0;
|
||||
translate: 0 var(--reveal-y, 28px);
|
||||
transition:
|
||||
opacity 0.55s cubic-bezier(0.21, 0.47, 0.32, 0.98),
|
||||
translate 0.55s cubic-bezier(0.21, 0.47, 0.32, 0.98);
|
||||
/* Emniyet: hydration hiç gelmezse 3 sn sonra içerik yine de belirir */
|
||||
animation: kt-reveal-emniyet 0.5s ease-out 3s forwards;
|
||||
}
|
||||
.kt-reveal.kt-reveal-in {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kt-reveal {
|
||||
translate: 0 0;
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kt-reveal-emniyet {
|
||||
to {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@@ -180,11 +212,7 @@
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
/* scroll-behavior: smooth KULLANMA — Lenis (ScrollFlow, anchors: true)
|
||||
kaydırmayı zaten yumuşatıyor; ikisi birlikte çift yumuşatma yapar. */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import localFont from "next/font/local";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
import { SiteTopBanner } from "@/components/site-top-banner";
|
||||
import { ListeCekmecesi } from "@/components/manuel-liste/liste-cekmecesi";
|
||||
import { ListeCekmecesiLazy } from "@/components/manuel-liste/liste-cekmecesi-lazy";
|
||||
import { HeaderGate } from "@/components/header-gate";
|
||||
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
|
||||
import { DevPanel } from "@/components/dev/dev-panel";
|
||||
@@ -31,6 +31,8 @@ const clarityCity = localFont({
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
// Yalnızca markdown/kod bloklarında kullanılıyor; landing'de preload israfı
|
||||
preload: false,
|
||||
});
|
||||
|
||||
const bricolage = Bricolage_Grotesque({
|
||||
@@ -81,7 +83,7 @@ export default function RootLayout({
|
||||
{children}
|
||||
</ScrollFlow>
|
||||
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
|
||||
<ListeCekmecesi />
|
||||
<ListeCekmecesiLazy />
|
||||
<Toaster position="top-center" />
|
||||
{/* Dev süperadmin paneli — prod build'de hiç render edilmez */}
|
||||
{process.env.NODE_ENV !== "production" ? <DevPanel /> : null}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -27,7 +27,7 @@ import { RaporListesi } from "@/components/rapor-listesi";
|
||||
import { ViewportPortal } from "@/components/viewport-portal";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { raporMaskele, ACIK_SATIR } from "@/lib/rapor-maske";
|
||||
import { SohbetClient } from "./sohbet-client";
|
||||
import { SohbetClient, type Mesaj } from "./sohbet-client";
|
||||
import { RevizyonKutusu } from "./revizyon-kutusu";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -43,18 +43,32 @@ export default async function ListemPage({
|
||||
}: {
|
||||
searchParams: Promise<{ acildi?: string }>;
|
||||
}) {
|
||||
await verifySession("/listem");
|
||||
const user = await getCurrentUser();
|
||||
const session = await verifySession("/listem");
|
||||
// Kullanıcı satırı, ?acildi parametresi ve rapor birbirinden bağımsız —
|
||||
// seri await zinciri yerine tek turda çöz.
|
||||
const [user, { acildi }, satir, sohbetGecmisi] = await Promise.all([
|
||||
getCurrentUser(),
|
||||
searchParams,
|
||||
appDb.query.reports.findFirst({
|
||||
where: eq(schema.reports.userId, session.user.id),
|
||||
}),
|
||||
// Sohbet geçmişi burada gelirse client mount sonrası /api/soru turu atmaz
|
||||
appDb
|
||||
.select({
|
||||
id: schema.chatMessages.id,
|
||||
role: schema.chatMessages.role,
|
||||
content: schema.chatMessages.content,
|
||||
})
|
||||
.from(schema.chatMessages)
|
||||
.where(eq(schema.chatMessages.userId, session.user.id))
|
||||
.orderBy(asc(schema.chatMessages.createdAt))
|
||||
.limit(100),
|
||||
]);
|
||||
if (!user) redirect("/giris?callback=/listem");
|
||||
|
||||
// ?acildi=1: ödeme dönüşü — kilidin kalktığı tek seferlik açılış deneyimi
|
||||
const { acildi } = await searchParams;
|
||||
const kilitAcilisi = Boolean(user.hasPaket && acildi === "1");
|
||||
|
||||
const satir = await appDb.query.reports.findFirst({
|
||||
where: eq(schema.reports.userId, user.id),
|
||||
});
|
||||
|
||||
// Liste yoksa önce sihirbazdan oluşturulmalı
|
||||
if (!satir?.result) {
|
||||
return (
|
||||
@@ -239,7 +253,10 @@ export default async function ListemPage({
|
||||
|
||||
<div id="sohbet" className="scroll-mt-24 lg:sticky lg:top-28">
|
||||
{user.hasPaket ? (
|
||||
<SohbetClient baslangicKredi={user.creditBalance} />
|
||||
<SohbetClient
|
||||
baslangicKredi={user.creditBalance}
|
||||
initialMesajlar={sohbetGecmisi as Mesaj[]}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6">
|
||||
<section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
@@ -317,6 +334,7 @@ export default async function ListemPage({
|
||||
tattırır (kalan kredi biterse composer paket CTA'sına döner). */}
|
||||
<SohbetClient
|
||||
baslangicKredi={user.creditBalance}
|
||||
initialMesajlar={sohbetGecmisi as Mesaj[]}
|
||||
className="h-[480px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ScrollButton } from "@/components/ui/scroll-button";
|
||||
import { DotsLoader } from "@/components/ui/loader";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface Mesaj {
|
||||
export interface Mesaj {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
@@ -42,19 +42,25 @@ const ORNEK_SORULAR = [
|
||||
*/
|
||||
export function SohbetClient({
|
||||
baslangicKredi,
|
||||
initialMesajlar,
|
||||
className,
|
||||
}: {
|
||||
baslangicKredi: number;
|
||||
/** Sunucu tarafında çekilmiş geçmiş; verilirse mount fetch'i atlanır */
|
||||
initialMesajlar?: Mesaj[];
|
||||
/** Boyutlandırma override'ı (ör. alt popup'ta h-full) */
|
||||
className?: string;
|
||||
}) {
|
||||
const [mesajlar, setMesajlar] = useState<Mesaj[]>([]);
|
||||
const [mesajlar, setMesajlar] = useState<Mesaj[]>(initialMesajlar ?? []);
|
||||
const [girdi, setGirdi] = useState("");
|
||||
const [bekliyor, setBekliyor] = useState(false);
|
||||
const [yuklendi, setYuklendi] = useState(false);
|
||||
const [yuklendi, setYuklendi] = useState(initialMesajlar != null);
|
||||
const [kredi, setKredi] = useState(baslangicKredi);
|
||||
|
||||
useEffect(() => {
|
||||
// Geçmiş sunucudan geldiyse (listem sayfası) ekstra tur atma;
|
||||
// popup gibi geç mount olan yerlerde eski davranış sürer.
|
||||
if (initialMesajlar != null) return;
|
||||
fetch("/api/soru")
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((data) => {
|
||||
@@ -63,7 +69,7 @@ export function SohbetClient({
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setYuklendi(true));
|
||||
}, []);
|
||||
}, [initialMesajlar]);
|
||||
|
||||
async function gonder(metin: string) {
|
||||
const mesaj = metin.trim();
|
||||
|
||||
19
src/app/odeme/sonuc/loading.tsx
Normal file
19
src/app/odeme/sonuc/loading.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
// /odeme/sonuc iyzico doğrulaması bekler — ortalanmış sonuç kartı iskeleti.
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main
|
||||
aria-live="polite"
|
||||
className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-16"
|
||||
>
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<Skeleton className="mx-auto size-12 rounded-full" />
|
||||
<Skeleton className="mx-auto mt-5 h-7 w-2/3" />
|
||||
<Skeleton className="mx-auto mt-3 h-5 w-full" />
|
||||
<Skeleton className="mx-auto mt-2 h-5 w-3/4" />
|
||||
<Skeleton className="mx-auto mt-6 h-12 w-full rounded-lg" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
23
src/app/paket/loading.tsx
Normal file
23
src/app/paket/loading.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
// /paket dinamik (kullanıcı kredisi) — statik kabuk için sayfa geometrisine
|
||||
// uygun iskelet: ortalanmış başlık bloğu + paket kartı şeridi.
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main
|
||||
aria-live="polite"
|
||||
className="mx-auto w-full max-w-3xl flex-1 px-4 py-16"
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<Skeleton className="h-6 w-56 rounded-full" />
|
||||
<Skeleton className="mt-4 h-10 w-3/4 max-w-xl" />
|
||||
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
|
||||
<Skeleton className="mt-2 h-5 w-1/2 max-w-md" />
|
||||
</div>
|
||||
<div className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]">
|
||||
<Skeleton className="h-80 w-full rounded-2xl" />
|
||||
<Skeleton className="h-80 w-full rounded-2xl sm:w-64" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -47,8 +47,7 @@ export default async function PaketPage({
|
||||
}: {
|
||||
searchParams: Promise<{ hata?: string }>;
|
||||
}) {
|
||||
const u = await getCurrentUser();
|
||||
const { hata } = await searchParams;
|
||||
const [u, { hata }] = await Promise.all([getCurrentUser(), searchParams]);
|
||||
// Ödeme callback'i doğrulama hatasında buraya ?hata=token|siparis ile döner
|
||||
const odemeHatasi = hata === "token" || hata === "siparis";
|
||||
|
||||
|
||||
21
src/app/rapor/yazdir/loading.tsx
Normal file
21
src/app/rapor/yazdir/loading.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
// /rapor/yazdir rapor sorgusunu bekler — belge düzeninde iskelet.
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main
|
||||
aria-live="polite"
|
||||
className="mx-auto w-full max-w-3xl bg-white p-8"
|
||||
>
|
||||
<div className="border-b-2 border-slate-200 pb-4">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="mt-2 h-4 w-40" />
|
||||
</div>
|
||||
<div className="mt-6 space-y-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-14 w-full rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -20,14 +20,16 @@ const DILIM_LABEL: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default async function YazdirPage() {
|
||||
await verifySession("/rapor/yazdir");
|
||||
const user = await getCurrentUser();
|
||||
const session = await verifySession("/rapor/yazdir");
|
||||
// Kullanıcı satırı ile rapor sorgusu bağımsız — paralel çöz
|
||||
const [user, satir] = await Promise.all([
|
||||
getCurrentUser(),
|
||||
appDb.query.reports.findFirst({
|
||||
where: eq(schema.reports.userId, session.user.id),
|
||||
}),
|
||||
]);
|
||||
if (!user) redirect("/giris");
|
||||
if (!user.hasPaket) redirect("/paket");
|
||||
|
||||
const satir = await appDb.query.reports.findFirst({
|
||||
where: eq(schema.reports.userId, user.id),
|
||||
});
|
||||
if (!satir?.result) redirect("/sonuc");
|
||||
|
||||
const rapor = satir.result as RaporSonuc;
|
||||
|
||||
@@ -71,14 +71,18 @@ export default async function SonucPage({
|
||||
);
|
||||
}
|
||||
|
||||
// Sihirbaz için: kullanıcı durumu, kayıtlı rapor ve dinamik facet'ler
|
||||
// Sihirbaz için: kullanıcı durumu, kayıtlı rapor ve dinamik facet'ler.
|
||||
// Rapor sorgusu yalnızca session.user.id'ye bağlı — getCurrentUser'ı
|
||||
// beklemeden paralel çalışır (seri DB zinciri kurma).
|
||||
const session = await getSession();
|
||||
const user = session ? await getCurrentUser() : null;
|
||||
const raporSatiri = user
|
||||
? await appDb.query.reports.findFirst({
|
||||
where: eq(schema.reports.userId, user.id),
|
||||
})
|
||||
: null;
|
||||
const [user, raporSatiri] = session
|
||||
? await Promise.all([
|
||||
getCurrentUser(),
|
||||
appDb.query.reports.findFirst({
|
||||
where: eq(schema.reports.userId, session.user.id),
|
||||
}),
|
||||
])
|
||||
: [null, null];
|
||||
|
||||
const facetler = rankWindowFacets(sira, turKey);
|
||||
// AI'sız ham liste: yalnızca sıralamayla ulaşılabilen programlar (dilimli)
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { SihirbazAdimlar } from "@/components/sihirbaz-adimlar";
|
||||
import { SihirbazAdimlarLazy } from "@/components/sihirbaz-adimlar-lazy";
|
||||
import type { SihirbazFacetleri } from "@/lib/db";
|
||||
import type { SihirbazSecimleri } from "@/lib/sihirbaz";
|
||||
|
||||
@@ -40,7 +40,7 @@ export function SihirbazModal({
|
||||
<DialogDescription className="sr-only">
|
||||
Sıralamana uygun 24 tercihlik listeni oluştur.
|
||||
</DialogDescription>
|
||||
<SihirbazAdimlar
|
||||
<SihirbazAdimlarLazy
|
||||
facetler={facetler}
|
||||
baslangicSecimler={baslangicSecimler}
|
||||
sonButonEtiketi={sonButonEtiketi}
|
||||
|
||||
@@ -5,11 +5,25 @@
|
||||
// spring ile açılır; çıkış girişten hızlı. Reduced motion'da cross-fade.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { MessageCircleMore, X } from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { SohbetClient } from "@/app/listem/sohbet-client";
|
||||
import { ViewportPortal } from "@/components/viewport-portal";
|
||||
|
||||
// Sohbet istemcisi (markdown zinciriyle birlikte) ağır; popup ilk açıldığında
|
||||
// yüklensin — /sonuc ilk yükünü şişirmesin.
|
||||
const SohbetClient = dynamic(
|
||||
() => import("@/app/listem/sohbet-client").then((m) => m.SohbetClient),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-full items-center justify-center rounded-2xl border border-slate-200 bg-white text-sm text-slate-500 shadow-2xl">
|
||||
Sohbet yükleniyor…
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export function SohbetPopup({ kredi }: { kredi: number }) {
|
||||
const [acik, setAcik] = useState(false);
|
||||
const azMotion = useReducedMotion();
|
||||
|
||||
Reference in New Issue
Block a user