load fast
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-06-06 09:08:38 +03:30
parent ace6a0b288
commit e68543f869
12 changed files with 255 additions and 248 deletions
+6 -6
View File
@@ -8,7 +8,6 @@ import { getToken } from "@/lib/api/func";
import { ef } from "@/lib/helpers/utfNumbers"; import { ef } from "@/lib/helpers/utfNumbers";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react"; import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react";
import Image from "next/image";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData"; import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
@@ -131,13 +130,14 @@ function FoodPage({}: Props) {
return ( return (
<div className="not-lg:max-w-lg not-lg:place-self-center not-lg:w-full not-lg:mb-10 pt-6 not-lg:flex not-lg:flex-col lg:fixed lg:z-10 lg:top-20 xl:top-[5.5rem] lg:bottom-24 lg:left-5 lg:right-4 xl:right-[285px] lg:grid lg:grid-cols-2 lg:min-h-0 lg:overflow-hidden lg:rounded-2xl"> <div className="not-lg:max-w-lg not-lg:place-self-center not-lg:w-full not-lg:mb-10 pt-6 not-lg:flex not-lg:flex-col lg:fixed lg:z-10 lg:top-20 xl:top-[5.5rem] lg:bottom-24 lg:left-5 lg:right-4 xl:right-[285px] lg:grid lg:grid-cols-2 lg:min-h-0 lg:overflow-hidden lg:rounded-2xl">
<div className="relative w-full lg:h-full min-h-0 not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1"> <div className="relative w-full lg:h-full min-h-0 not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
<Image {/* eslint-disable-next-line @next/next/no-img-element */}
className="lg:object-contain h-auto object-cover bg-[#F2F2F9]" <img
className="lg:object-contain h-auto object-cover bg-[#F2F2F9] w-full"
src={foodImage} src={foodImage}
alt={foodName} alt={foodName}
width={1000} onError={(event) => {
height={100} event.currentTarget.src = "/assets/images/no-image.png";
priority }}
/> />
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center"> <div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold"> <div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
@@ -70,7 +70,19 @@ function CategoryImage({
); );
} }
return <img src={src} width={size} height={size} alt={alt} loading="lazy" />; return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
width={size}
height={size}
alt={alt}
loading="lazy"
onError={(event) => {
event.currentTarget.src = "/assets/images/food-image.png";
}}
/>
);
} }
type Variant = "large" | "small"; type Variant = "large" | "small";
@@ -118,6 +118,7 @@ function OrdersIndex() {
width={64} width={64}
height={64} height={64}
alt={foodName} alt={foodName}
unoptimized
/> />
</div> </div>
<div className='flex-1 min-w-0'> <div className='flex-1 min-w-0'>
@@ -203,6 +204,7 @@ function OrdersIndex() {
width={64} width={64}
height={64} height={64}
alt={foodName} alt={foodName}
unoptimized
/> />
</div> </div>
<div className='flex-1 min-w-0'> <div className='flex-1 min-w-0'>
+11 -13
View File
@@ -1,17 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { fetchWithTimeout } from "@/lib/helpers/fetchWithTimeout";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import sharp from "sharp"; import sharp from "sharp";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 0; export const revalidate = 0;
const FETCH_TIMEOUT_MS = 5000;
const DEFAULT_ICON = "/icons/web-app-manifest-192x192.png";
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<{ name: string }> }, { params }: { params: Promise<{ name: string }> },
) { ) {
try { try {
const { name } = await params; await params;
console.log("name", name);
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const logoUrl = searchParams.get("url"); const logoUrl = searchParams.get("url");
@@ -21,29 +24,27 @@ export async function GET(
return new NextResponse("Logo URL is required", { status: 400 }); return new NextResponse("Logo URL is required", { status: 400 });
} }
// دانلود تصویر const imageResponse = await fetchWithTimeout(logoUrl, {
const imageResponse = await fetch(logoUrl); timeoutMs: FETCH_TIMEOUT_MS,
});
if (!imageResponse.ok) { if (!imageResponse.ok) {
return new NextResponse("Failed to fetch logo", { status: 404 }); return NextResponse.redirect(new URL(DEFAULT_ICON, request.url), 302);
} }
const imageBuffer = Buffer.from(await imageResponse.arrayBuffer()); const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
// دریافت ابعاد تصویر
const metadata = await sharp(imageBuffer).metadata(); const metadata = await sharp(imageBuffer).metadata();
const { width, height } = metadata; const { width, height } = metadata;
if (!width || !height) { if (!width || !height) {
return new NextResponse("Invalid image", { status: 400 }); return NextResponse.redirect(new URL(DEFAULT_ICON, request.url), 302);
} }
// اگر تصویر از قبل مربع باشد، مستقیماً resize می‌کنیم
if (width === height) { if (width === height) {
const processedImage = sharp(imageBuffer).resize(size, size, { const processedImage = sharp(imageBuffer).resize(size, size, {
fit: "contain", fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 }, background: { r: 0, g: 0, b: 0, alpha: 0 },
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const outputBuffer: any = await processedImage.png().toBuffer(); const outputBuffer: any = await processedImage.png().toBuffer();
return new NextResponse(outputBuffer, { return new NextResponse(outputBuffer, {
@@ -54,13 +55,11 @@ export async function GET(
}); });
} }
// محاسبه نسبت برای resize اولیه (برای بهینه‌سازی)
const maxDimension = Math.max(width, height); const maxDimension = Math.max(width, height);
const scale = size / maxDimension; const scale = size / maxDimension;
const newWidth = Math.round(width * scale); const newWidth = Math.round(width * scale);
const newHeight = Math.round(height * scale); const newHeight = Math.round(height * scale);
// ابتدا resize می‌کنیم و سپس padding اضافه می‌کنیم
const paddingTop = Math.floor((size - newHeight) / 2); const paddingTop = Math.floor((size - newHeight) / 2);
const paddingBottom = size - newHeight - paddingTop; const paddingBottom = size - newHeight - paddingTop;
const paddingLeft = Math.floor((size - newWidth) / 2); const paddingLeft = Math.floor((size - newWidth) / 2);
@@ -79,7 +78,6 @@ export async function GET(
background: { r: 0, g: 0, b: 0, alpha: 0 }, background: { r: 0, g: 0, b: 0, alpha: 0 },
}); });
// تبدیل به PNG و برگرداندن
const outputBuffer: any = await processedImage.png().toBuffer(); const outputBuffer: any = await processedImage.png().toBuffer();
return new NextResponse(outputBuffer, { return new NextResponse(outputBuffer, {
@@ -90,6 +88,6 @@ export async function GET(
}); });
} catch (error) { } catch (error) {
console.error("Error processing logo:", error); console.error("Error processing logo:", error);
return new NextResponse("Internal Server Error", { status: 500 }); return NextResponse.redirect(new URL(DEFAULT_ICON, request.url), 302);
} }
} }
+3 -1
View File
@@ -1,3 +1,4 @@
import { fetchWithTimeout } from "@/lib/helpers/fetchWithTimeout";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -15,7 +16,8 @@ export async function GET(request: NextRequest) {
if (!["http:", "https:"].includes(parsed.protocol)) { if (!["http:", "https:"].includes(parsed.protocol)) {
return NextResponse.json({ error: "Invalid URL" }, { status: 400 }); return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
} }
const res = await fetch(url, { const res = await fetchWithTimeout(url, {
timeoutMs: 5000,
headers: { Accept: "image/svg+xml, text/xml, text/plain" }, headers: { Accept: "image/svg+xml, text/xml, text/plain" },
}); });
if (!res.ok) { if (!res.ok) {
+16 -5
View File
@@ -23,6 +23,17 @@ export async function generateMetadata({
try { try {
const restaurant = await getRestaurant(name) const restaurant = await getRestaurant(name)
const title = restaurant.seoTitle || restaurant.name const title = restaurant.seoTitle || restaurant.name
const logo = restaurant.logo
let iconUrl = defaultIcon
let icon192Url = defaultIcon
let icon512Url = defaultIcon512
if (logo && logo.trim() !== "") {
iconUrl = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`
icon192Url = iconUrl
icon512Url = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=512`
}
return { return {
title, title,
@@ -30,12 +41,12 @@ export async function generateMetadata({
manifest: `/${name}/manifest.webmanifest`, manifest: `/${name}/manifest.webmanifest`,
icons: { icons: {
icon: [ icon: [
{ url: defaultIcon }, { url: iconUrl },
{ url: defaultIcon, sizes: "192x192", type: "image/png" }, { url: icon192Url, sizes: "192x192", type: "image/png" },
{ url: defaultIcon512, sizes: "512x512", type: "image/png" }, { url: icon512Url, sizes: "512x512", type: "image/png" },
], ],
shortcut: defaultIcon, shortcut: iconUrl,
apple: defaultIcon, apple: iconUrl,
}, },
} }
} catch { } catch {
+42 -29
View File
@@ -4,6 +4,46 @@ import { getRestaurant } from "../lib/getRestaurant";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = 0; export const revalidate = 0;
function buildManifestIcons(name: string, logo?: string | null) {
const default192 = "/icons/web-app-manifest-192x192.png";
const default512 = "/icons/web-app-manifest-512x512.png";
if (logo && logo.trim() !== "") {
const logo192 = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`;
const logo512 = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=512`;
return [
{
src: logo192,
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: logo512,
sizes: "512x512",
type: "image/png",
purpose: "any",
},
];
}
return [
{
src: default192,
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: default512,
sizes: "512x512",
type: "image/png",
purpose: "any",
},
];
}
export async function GET( export async function GET(
request: Request, request: Request,
{ params }: { params: Promise<{ name: string }> } { params }: { params: Promise<{ name: string }> }
@@ -13,21 +53,7 @@ export async function GET(
const restaurant = await getRestaurant(name); const restaurant = await getRestaurant(name);
const themeColor = restaurant.menuColor || "#F4F5F9"; const themeColor = restaurant.menuColor || "#F4F5F9";
const icons = buildManifestIcons(name, restaurant.logo);
const icons = [
{
src: "/icons/web-app-manifest-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/icons/web-app-manifest-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
];
const manifest = { const manifest = {
name: restaurant.name, name: restaurant.name,
@@ -75,20 +101,7 @@ export async function GET(
orientation: "portrait", orientation: "portrait",
dir: "rtl", dir: "rtl",
lang: "fa-IR", lang: "fa-IR",
icons: [ icons: buildManifestIcons(name),
{
src: "/icons/web-app-manifest-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/icons/web-app-manifest-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
],
}; };
return NextResponse.json(manifest, { return NextResponse.json(manifest, {
+2
View File
@@ -74,6 +74,7 @@ function Combobox({ title, options, placeholder, icon: Icon, expanded, selectedI
alt="" alt=""
width={20} width={20}
height={20} height={20}
unoptimized
className="inline-block mr-1 dark:brightness-0 dark:invert" className="inline-block mr-1 dark:brightness-0 dark:invert"
/> />
); );
@@ -191,6 +192,7 @@ function Combobox({ title, options, placeholder, icon: Icon, expanded, selectedI
alt="" alt=""
width={16} width={16}
height={16} height={16}
unoptimized
className="inline-block mr-1 dark:brightness-0 dark:invert" className="inline-block mr-1 dark:brightness-0 dark:invert"
/> />
) : v?.icon && React.createElement(v.icon, { ) : v?.icon && React.createElement(v.icon, {
+9 -2
View File
@@ -8,7 +8,7 @@ import PlusIcon from "@/components/icons/PlusIcon";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { memo, useMemo } from "react"; import { memo, useEffect, useMemo, useState } from "react";
interface MenuItemProps { interface MenuItemProps {
food: Food; food: Food;
@@ -38,6 +38,12 @@ const MenuItem = ({ food }: MenuItemProps) => {
return fallbackImage; return fallbackImage;
}, [food.images]); }, [food.images]);
const [imageSrc, setImageSrc] = useState(resolvedImage);
useEffect(() => {
setImageSrc(resolvedImage);
}, [resolvedImage]);
const foodName = useMemo(() => food.title || "بدون نام", [food.title]); const foodName = useMemo(() => food.title || "بدون نام", [food.title]);
const foodContent = useMemo(() => { const foodContent = useMemo(() => {
@@ -95,11 +101,12 @@ const MenuItem = ({ food }: MenuItemProps) => {
<Link href={foodDetailUrl} className="cursor-pointer"> <Link href={foodDetailUrl} className="cursor-pointer">
<img <img
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 object-cover" className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 object-cover"
src={resolvedImage} src={imageSrc}
height={112} height={112}
width={112} width={112}
alt={foodName} alt={foodName}
loading="lazy" loading="lazy"
onError={() => setImageSrc(fallbackImage)}
/> />
</Link> </Link>
<div className="w-full inline-flex flex-col justify-between min-w-0"> <div className="w-full inline-flex flex-col justify-between min-w-0">
+71 -106
View File
@@ -1,178 +1,143 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion } from 'framer-motion';
import Image from 'next/image'; import Image from 'next/image';
const MAX_DISPLAY_MS = 1200;
const EXIT_MS = 350;
const LOGO_TIMEOUT_MS = 2500;
interface SplashScreenProps { interface SplashScreenProps {
logo?: string; logo?: string;
restaurantName?: string; restaurantName?: string;
duration?: number; onDismiss: () => void;
} }
export default function SplashScreen({ export default function SplashScreen({
logo, logo,
restaurantName, restaurantName,
duration = 2000 onDismiss,
}: SplashScreenProps) { }: SplashScreenProps) {
const [isVisible, setIsVisible] = useState(true); const [opacity, setOpacity] = useState(1);
const [imageLoaded, setImageLoaded] = useState(false); const [logoState, setLogoState] = useState<'idle' | 'loading' | 'ready' | 'error'>(
logo ? 'loading' : 'idle',
);
const onDismissRef = useRef(onDismiss);
onDismissRef.current = onDismiss;
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { setLogoState(logo ? 'loading' : 'idle');
// setIsVisible(false); }, [logo]);
}, duration);
return () => clearTimeout(timer); useEffect(() => {
}, [duration]); if (!logo || logoState !== 'loading') return;
const logoTimeout = setTimeout(() => {
setLogoState('error');
}, LOGO_TIMEOUT_MS);
return () => clearTimeout(logoTimeout);
}, [logo, logoState]);
useEffect(() => {
const fadeTimer = setTimeout(() => {
setOpacity(0);
}, MAX_DISPLAY_MS);
const dismissTimer = setTimeout(() => {
onDismissRef.current();
}, MAX_DISPLAY_MS + EXIT_MS);
return () => {
clearTimeout(fadeTimer);
clearTimeout(dismissTimer);
};
}, []);
const showLogoSpinner = Boolean(logo) && logoState === 'loading';
return ( return (
<AnimatePresence>
{isVisible && (
<motion.div <motion.div
initial={{ opacity: 1 }} initial={{ opacity: 1 }}
exit={{ opacity: 0 }} animate={{ opacity }}
transition={{ duration: 0.5 }} transition={{ duration: EXIT_MS / 1000, ease: 'easeOut' }}
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center" className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background"
aria-hidden="true"
> >
{/* دایره‌های تزئینی پس‌زمینه */} <div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute inset-0 overflow-hidden">
<motion.div <motion.div
animate={{ animate={{
scale: [1, 1.2, 1], scale: [1, 1.2, 1],
opacity: [0.1, 0.2, 0.1] opacity: [0.1, 0.2, 0.1],
}} }}
transition={{ transition={{
duration: 3, duration: 3,
repeat: Infinity, repeat: Infinity,
ease: "easeInOut" ease: 'easeInOut',
}} }}
className="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl" className="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
/> />
<motion.div <motion.div
animate={{ animate={{
scale: [1.2, 1, 1.2], scale: [1.2, 1, 1.2],
opacity: [0.1, 0.2, 0.1] opacity: [0.1, 0.2, 0.1],
}} }}
transition={{ transition={{
duration: 3, duration: 3,
delay: 0.5, delay: 0.5,
repeat: Infinity, repeat: Infinity,
ease: "easeInOut" ease: 'easeInOut',
}} }}
className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl" className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
/> />
</div> </div>
<motion.div <div className="flex flex-col items-center gap-8 px-8 w-full max-w-md relative z-10">
initial={{ scale: 0.8, opacity: 0 }} {logo && logoState !== 'error' && (
animate={{ scale: 1, opacity: 1 }} <div className="relative">
transition={{
duration: 0.5,
ease: "easeOut"
}}
className="flex flex-col items-center gap-8 px-8 w-full max-w-md relative z-10"
>
{logo && (
<motion.div
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
className="relative"
>
<div>
<Image <Image
src={logo} src={logo}
alt={restaurantName || 'Restaurant Logo'} alt={restaurantName || 'Restaurant Logo'}
width={200} width={200}
height={100} height={100}
unoptimized unoptimized
loading="lazy" priority
className="object-contain" className="object-contain"
onLoad={() => setImageLoaded(true)} onLoad={() => setLogoState('ready')}
style={{ opacity: imageLoaded ? 1 : 0, transition: 'opacity 0.3s' }} onError={() => setLogoState('error')}
style={{
opacity: logoState === 'ready' ? 1 : 0,
transition: 'opacity 0.3s',
}}
/> />
{!imageLoaded && ( {showLogoSpinner && (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" /> <div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
</div> </div>
)} )}
</div> </div>
{/* حلقه درخشان دور لوگو */}
<motion.div
animate={{
scale: [1, 1.05, 1],
opacity: [0.5, 0.8, 0.5]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
// className="absolute -inset-2 rounded-3xl bg-gradient-to-r from-primary/20 via-primary/40 to-primary/20 -z-10 blur-xl"
/>
</motion.div>
)} )}
{/*
{restaurantName && (
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.3, duration: 0.5 }}
className="text-3xl font-bold text-foreground text-center"
>
{restaurantName}
</motion.h1>
)} */}
<div className="flex gap-3 mt-2">
{[0, 0.2, 0.4].map((delay) => (
<motion.div <motion.div
initial={{ opacity: 0 }} key={delay}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.5 }}
className="flex gap-3 mt-2"
>
<motion.div
animate={{ animate={{
scale: [1, 1.3, 1], scale: [1, 1.3, 1],
opacity: [0.4, 1, 0.4] opacity: [0.4, 1, 0.4],
}} }}
transition={{ transition={{
duration: 1.5, duration: 1.5,
delay,
repeat: Infinity, repeat: Infinity,
ease: "easeInOut" ease: 'easeInOut',
}}
className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50"
/>
<motion.div
animate={{
scale: [1, 1.3, 1],
opacity: [0.4, 1, 0.4]
}}
transition={{
duration: 1.5,
delay: 0.2,
repeat: Infinity,
ease: "easeInOut"
}}
className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50"
/>
<motion.div
animate={{
scale: [1, 1.3, 1],
opacity: [0.4, 1, 0.4]
}}
transition={{
duration: 1.5,
delay: 0.4,
repeat: Infinity,
ease: "easeInOut"
}} }}
className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50" className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50"
/> />
))}
</div>
</div>
</motion.div> </motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
); );
} }
+17 -35
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import usePreference from '@/hooks/helpers/usePreference'; import usePreference from '@/hooks/helpers/usePreference';
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData'; import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
import SplashScreen from '@/components/overlays/SplashScreen'; import SplashScreen from '@/components/overlays/SplashScreen';
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils'; import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
@@ -10,7 +10,6 @@ type Props = {
children: React.ReactNode children: React.ReactNode
} }
// تابع برای اعمال رنگ primary
function applyPrimaryColor(colorHex: string) { function applyPrimaryColor(colorHex: string) {
const root = document.documentElement; const root = document.documentElement;
const oklchColor = hexToOklch(colorHex); const oklchColor = hexToOklch(colorHex);
@@ -28,47 +27,43 @@ function PreferenceWrapper({ children }: Props) {
const { state: nightMode } = usePreference('night-mode', false); const { state: nightMode } = usePreference('night-mode', false);
const { data: aboutData } = useGetAbout(); const { data: aboutData } = useGetAbout();
// بررسی اینکه آیا باید اسپلش نمایش داده بشه const [showSplash, setShowSplash] = useState(false);
const [showSplash, setShowSplash] = useState(() => {
if (typeof window === 'undefined') return true;
const navigationEntries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
const isReload = navigationEntries.length > 0 && navigationEntries[0].type === 'reload';
return !sessionStorage.getItem('app-loaded') || isReload;
});
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [cachedLogo, setCachedLogo] = useState<string | null>(null); const [cachedLogo, setCachedLogo] = useState<string | null>(null);
const [cachedName, setCachedName] = useState<string | null>(null); const [cachedName, setCachedName] = useState<string | null>(null);
useEffect(() => { const handleSplashDismiss = useCallback(() => {
setMounted(true); setShowSplash(false);
}, []); }, []);
// بارگذاری رنگ و اطلاعات از localStorage در اولین رندر
useEffect(() => { useEffect(() => {
// بارگذاری رنگ setMounted(true);
const savedColor = localStorage.getItem('theme-primary-color'); const savedColor = localStorage.getItem('theme-primary-color');
if (savedColor) { if (savedColor) {
applyPrimaryColor(savedColor); applyPrimaryColor(savedColor);
} }
// بارگذاری لوگو و نام
const savedLogo = localStorage.getItem('restaurant-logo'); const savedLogo = localStorage.getItem('restaurant-logo');
const savedName = localStorage.getItem('restaurant-name'); const savedName = localStorage.getItem('restaurant-name');
if (savedLogo) setCachedLogo(savedLogo); if (savedLogo) setCachedLogo(savedLogo);
if (savedName) setCachedName(savedName); if (savedName) setCachedName(savedName);
// ثبت اینکه app لود شده const navigationEntries = performance.getEntriesByType('navigation') as PerformanceNavigationTiming[];
const isReload = navigationEntries.length > 0 && navigationEntries[0].type === 'reload';
const isFirstLoadInSession = !sessionStorage.getItem('app-loaded') || isReload;
if (isFirstLoadInSession) {
setShowSplash(true);
}
sessionStorage.setItem('app-loaded', 'true'); sessionStorage.setItem('app-loaded', 'true');
}, []); }, []);
// اعمال رنگ primary از about (با ذخیره در localStorage)
useEffect(() => { useEffect(() => {
if (aboutData?.data?.menuColor) { if (aboutData?.data?.menuColor) {
// ذخیره رنگ و اطلاعات در localStorage
localStorage.setItem('theme-primary-color', aboutData.data.menuColor); localStorage.setItem('theme-primary-color', aboutData.data.menuColor);
// ذخیره لوگو و نام برای اسپلش
if (aboutData.data.logo) { if (aboutData.data.logo) {
localStorage.setItem('restaurant-logo', aboutData.data.logo); localStorage.setItem('restaurant-logo', aboutData.data.logo);
} }
@@ -81,23 +76,10 @@ function PreferenceWrapper({ children }: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [aboutData?.data?.menuColor]); }, [aboutData?.data?.menuColor]);
// اعمال تم تاریک/روشن
useEffect(() => { useEffect(() => {
const theme = nightMode; document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
document.documentElement.setAttribute("data-theme", theme ? "dark" : "light");
}, [nightMode]); }, [nightMode]);
// بستن اسپلش بعد از مدت زمان مشخص
useEffect(() => {
if (showSplash) {
const timer = setTimeout(() => {
setShowSplash(false);
}, 2500);
return () => clearTimeout(timer);
}
}, [showSplash]);
const splashLogo = mounted const splashLogo = mounted
? (aboutData?.data?.logo || cachedLogo || undefined) ? (aboutData?.data?.logo || cachedLogo || undefined)
: undefined; : undefined;
@@ -107,14 +89,14 @@ function PreferenceWrapper({ children }: Props) {
return ( return (
<> <>
{children}
{showSplash && ( {showSplash && (
<SplashScreen <SplashScreen
logo={splashLogo} logo={splashLogo}
restaurantName={splashName} restaurantName={splashName}
duration={2500} onDismiss={handleSplashDismiss}
/> />
)} )}
{!showSplash && children}
</> </>
) )
} }
+13
View File
@@ -0,0 +1,13 @@
const DEFAULT_TIMEOUT_MS = 5000;
export async function fetchWithTimeout(
url: string,
options: RequestInit & { timeoutMs?: number } = {},
): Promise<Response> {
const { timeoutMs = DEFAULT_TIMEOUT_MS, ...fetchOptions } = options;
return fetch(url, {
...fetchOptions,
signal: AbortSignal.timeout(timeoutMs),
});
}