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
+2
View File
@@ -74,6 +74,7 @@ function Combobox({ title, options, placeholder, icon: Icon, expanded, selectedI
alt=""
width={20}
height={20}
unoptimized
className="inline-block mr-1 dark:brightness-0 dark:invert"
/>
);
@@ -191,6 +192,7 @@ function Combobox({ title, options, placeholder, icon: Icon, expanded, selectedI
alt=""
width={16}
height={16}
unoptimized
className="inline-block mr-1 dark:brightness-0 dark:invert"
/>
) : 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 Link from "next/link";
import { useParams } from "next/navigation";
import { memo, useMemo } from "react";
import { memo, useEffect, useMemo, useState } from "react";
interface MenuItemProps {
food: Food;
@@ -38,6 +38,12 @@ const MenuItem = ({ food }: MenuItemProps) => {
return fallbackImage;
}, [food.images]);
const [imageSrc, setImageSrc] = useState(resolvedImage);
useEffect(() => {
setImageSrc(resolvedImage);
}, [resolvedImage]);
const foodName = useMemo(() => food.title || "بدون نام", [food.title]);
const foodContent = useMemo(() => {
@@ -95,11 +101,12 @@ const MenuItem = ({ food }: MenuItemProps) => {
<Link href={foodDetailUrl} className="cursor-pointer">
<img
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 object-cover"
src={resolvedImage}
src={imageSrc}
height={112}
width={112}
alt={foodName}
loading="lazy"
onError={() => setImageSrc(fallbackImage)}
/>
</Link>
<div className="w-full inline-flex flex-col justify-between min-w-0">
+118 -153
View File
@@ -1,178 +1,143 @@
'use client';
import { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useEffect, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import Image from 'next/image';
const MAX_DISPLAY_MS = 1200;
const EXIT_MS = 350;
const LOGO_TIMEOUT_MS = 2500;
interface SplashScreenProps {
logo?: string;
restaurantName?: string;
duration?: number;
onDismiss: () => void;
}
export default function SplashScreen({
logo,
restaurantName,
duration = 2000
onDismiss,
}: SplashScreenProps) {
const [isVisible, setIsVisible] = useState(true);
const [imageLoaded, setImageLoaded] = useState(false);
const [opacity, setOpacity] = useState(1);
const [logoState, setLogoState] = useState<'idle' | 'loading' | 'ready' | 'error'>(
logo ? 'loading' : 'idle',
);
const onDismissRef = useRef(onDismiss);
onDismissRef.current = onDismiss;
useEffect(() => {
const timer = setTimeout(() => {
// setIsVisible(false);
}, duration);
setLogoState(logo ? 'loading' : 'idle');
}, [logo]);
return () => clearTimeout(timer);
}, [duration]);
useEffect(() => {
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 (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 1 }}
animate={{ opacity }}
transition={{ duration: EXIT_MS / 1000, ease: 'easeOut' }}
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">
<motion.div
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center"
>
{/* دایره‌های تزئینی پس‌زمینه */}
<div className="absolute inset-0 overflow-hidden">
<motion.div
animate={{
scale: [1, 1.2, 1],
opacity: [0.1, 0.2, 0.1]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
className="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
/>
<motion.div
animate={{
scale: [1.2, 1, 1.2],
opacity: [0.1, 0.2, 0.1]
}}
transition={{
duration: 3,
delay: 0.5,
repeat: Infinity,
ease: "easeInOut"
}}
className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
/>
</div>
animate={{
scale: [1, 1.2, 1],
opacity: [0.1, 0.2, 0.1],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
/>
<motion.div
animate={{
scale: [1.2, 1, 1.2],
opacity: [0.1, 0.2, 0.1],
}}
transition={{
duration: 3,
delay: 0.5,
repeat: Infinity,
ease: 'easeInOut',
}}
className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
/>
</div>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
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
src={logo}
alt={restaurantName || 'Restaurant Logo'}
width={200}
height={100}
unoptimized
loading="lazy"
className="object-contain"
onLoad={() => setImageLoaded(true)}
style={{ opacity: imageLoaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
{!imageLoaded && (
<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>
)}
</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>
<div className="flex flex-col items-center gap-8 px-8 w-full max-w-md relative z-10">
{logo && logoState !== 'error' && (
<div className="relative">
<Image
src={logo}
alt={restaurantName || 'Restaurant Logo'}
width={200}
height={100}
unoptimized
priority
className="object-contain"
onLoad={() => setLogoState('ready')}
onError={() => setLogoState('error')}
style={{
opacity: logoState === 'ready' ? 1 : 0,
transition: 'opacity 0.3s',
}}
/>
{showLogoSpinner && (
<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>
)}
{/*
{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>
)}
<div className="flex gap-3 mt-2">
{[0, 0.2, 0.4].map((delay) => (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.5 }}
className="flex gap-3 mt-2"
>
<motion.div
animate={{
scale: [1, 1.3, 1],
opacity: [0.4, 1, 0.4]
}}
transition={{
duration: 1.5,
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.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"
/>
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
key={delay}
animate={{
scale: [1, 1.3, 1],
opacity: [0.4, 1, 0.4],
}}
transition={{
duration: 1.5,
delay,
repeat: Infinity,
ease: 'easeInOut',
}}
className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50"
/>
))}
</div>
</div>
</motion.div>
);
}
+20 -38
View File
@@ -1,7 +1,7 @@
'use client';
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 SplashScreen from '@/components/overlays/SplashScreen';
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
@@ -10,14 +10,13 @@ type Props = {
children: React.ReactNode
}
// تابع برای اعمال رنگ primary
function applyPrimaryColor(colorHex: string) {
const root = document.documentElement;
const oklchColor = hexToOklch(colorHex);
if (oklchColor) {
root.style.setProperty('--primary', oklchColor);
const brightness = calculateBrightness(colorHex);
const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)';
root.style.setProperty('--primary-foreground', foregroundOklch);
@@ -28,47 +27,43 @@ function PreferenceWrapper({ children }: Props) {
const { state: nightMode } = usePreference('night-mode', false);
const { data: aboutData } = useGetAbout();
// بررسی اینکه آیا باید اسپلش نمایش داده بشه
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 [showSplash, setShowSplash] = useState(false);
const [mounted, setMounted] = useState(false);
const [cachedLogo, setCachedLogo] = useState<string | null>(null);
const [cachedName, setCachedName] = useState<string | null>(null);
useEffect(() => {
setMounted(true);
const handleSplashDismiss = useCallback(() => {
setShowSplash(false);
}, []);
// بارگذاری رنگ و اطلاعات از localStorage در اولین رندر
useEffect(() => {
// بارگذاری رنگ
setMounted(true);
const savedColor = localStorage.getItem('theme-primary-color');
if (savedColor) {
applyPrimaryColor(savedColor);
}
// بارگذاری لوگو و نام
const savedLogo = localStorage.getItem('restaurant-logo');
const savedName = localStorage.getItem('restaurant-name');
if (savedLogo) setCachedLogo(savedLogo);
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');
}, []);
// اعمال رنگ primary از about (با ذخیره در localStorage)
useEffect(() => {
if (aboutData?.data?.menuColor) {
// ذخیره رنگ و اطلاعات در localStorage
localStorage.setItem('theme-primary-color', aboutData.data.menuColor);
// ذخیره لوگو و نام برای اسپلش
if (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
}, [aboutData?.data?.menuColor]);
// اعمال تم تاریک/روشن
useEffect(() => {
const theme = nightMode;
document.documentElement.setAttribute("data-theme", theme ? "dark" : "light");
document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
}, [nightMode]);
// بستن اسپلش بعد از مدت زمان مشخص
useEffect(() => {
if (showSplash) {
const timer = setTimeout(() => {
setShowSplash(false);
}, 2500);
return () => clearTimeout(timer);
}
}, [showSplash]);
const splashLogo = mounted
? (aboutData?.data?.logo || cachedLogo || undefined)
: undefined;
@@ -107,16 +89,16 @@ function PreferenceWrapper({ children }: Props) {
return (
<>
{children}
{showSplash && (
<SplashScreen
logo={splashLogo}
restaurantName={splashName}
duration={2500}
onDismiss={handleSplashDismiss}
/>
)}
{!showSplash && children}
</>
)
}
export default PreferenceWrapper
export default PreferenceWrapper