add primary color and splash screen

This commit is contained in:
hamid zarghami
2025-12-17 16:01:08 +03:30
parent 4fa360dc84
commit 32c197a105
17 changed files with 436 additions and 31 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -92,7 +92,7 @@ function FoodPage({ }: Props) {
{categoryName || '-'}
</div>
<button onClick={() => router.back()} className='p-2 rounded-full bg-container/40'>
<ArrowLeft size={18} className='stroke-black' />
<ArrowLeft size={18} className='stroke-primary dark:stroke-foreground' />
</button>
</div>
</div>
@@ -103,7 +103,7 @@ function FoodPage({ }: Props) {
{foodName}
</h5>
<button className='p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg'>
<Heart variant='Bold' size={24} className='fill-black dark:fill-foreground' />
<Heart variant='Bold' size={24} className='fill-primary dark:fill-foreground' />
</button>
</div>
@@ -172,7 +172,7 @@ function RatingOrderIndex({ }: Props) {
title: 'text-sm2! font-normal mt-1',
header: 'rounded-lg h-7! w-full',
headerActive: 'bg-white',
titleActive: 'text-black'
titleActive: 'text-primary'
}}>
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={badPoitns()}></TabHeader>
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={goodPoitns()}></TabHeader>
@@ -60,10 +60,7 @@ function TransactionsIndex() {
</div>
<div
className='max-w-36 place-self-end col-span-1 h-full text-end py-4 px-5.5 flex flex-col justify-between rounded-e-normal'
style={{
background: 'linear-gradient(165.51deg, #000000 -8.44%, #C3C7DD 100%)',
}}
className='max-w-36 place-self-end col-span-1 h-full text-end py-4 px-5.5 flex flex-col justify-between rounded-e-normal bg-linear-to-br from-primary to-menu-header'
>
<p className='text-sm2 text-white leading-tight'>
<span className='font-normal'>Customer</span><br />
@@ -50,10 +50,7 @@ function TransactionsIndex() {
</div>
<div
className='col-span-1 h-full text-end py-4 px-5.5 flex flex-col justify-between rounded-e-normal'
style={{
background: 'linear-gradient(165.51deg, #000000 -8.44%, #C3C7DD 100%)',
}}
className='col-span-1 h-full text-end py-4 px-5.5 flex flex-col justify-between rounded-e-normal bg-linear-to-br from-primary to-menu-header'
>
<p className='text-sm2 text-white leading-tight'>
<span className='font-medium'>Your</span><br />
+42
View File
@@ -42,6 +42,48 @@ export default async function RootLayout({
dir='rtl'
className="h-svh overflow-hidden"
data-theme="light" >
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
const savedColor = localStorage.getItem('theme-primary-color');
if (savedColor) {
const hex = savedColor.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
const lightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const chroma = (max - min) * 0.15;
let hue = 0;
if (chroma !== 0) {
if (max === r) {
hue = ((g - b) / (max - min) + (g < b ? 6 : 0)) * 60;
} else if (max === g) {
hue = ((b - r) / (max - min) + 2) * 60;
} else {
hue = ((r - g) / (max - min) + 4) * 60;
}
}
const oklchColor = 'oklch(' + lightness.toFixed(3) + ' ' + chroma.toFixed(3) + ' ' + hue.toFixed(1) + ')';
document.documentElement.style.setProperty('--primary', oklchColor);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)';
document.documentElement.style.setProperty('--primary-foreground', foregroundOklch);
}
} catch (e) {}
})();
`,
}}
/>
</head>
<body
className={`antialiased bg-background h-svh overflow-hidden`}
+1 -1
View File
@@ -109,7 +109,7 @@ function ToggleButton({
whileTap={{ scale: 0.9 }}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
>
<Refresh stroke="#333333" size={24} />
<Refresh className="stroke-primary" size={24} />
</motion.div>
</motion.div>
)}
+1 -1
View File
@@ -330,7 +330,7 @@ function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeS
{tInstallPwaModal('AlreadyInstalled')}
</p>
) : isIOSDevice ? (
<div className="mt-5 rounded-3xl bg-black/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
<div className="mt-5 rounded-3xl bg-primary/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
<p className="text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
{tInstallPwaModal('IOSDescription')}
</p>
+3 -3
View File
@@ -16,13 +16,13 @@ export default function SideMenuItem({ href, title, icon, className, children, .
className={`
inline-flex gap-3 ps-6 xl:ps-9 h-full w-full items-center overflow-hidden text-nowrap
text-xs font-light text-disabled-text border-transparent
data-[active]:border-s-6 data-[active]:border-black data-[active]:text-black
dark:data-[active]:border-foreground dark:data-[active]:text-foreground
data-active:border-s-6 data-active:border-primary data-active:text-primary
dark:data-active:border-foreground dark:data-active:text-foreground
${className}
`}
>
{children}
<span className="flex-shrink-0">{icon}</span>
<span className="shrink-0">{icon}</span>
<span className='mt-0.5'>{title}</span>
</Link>
)
+177
View File
@@ -0,0 +1,177 @@
'use client';
import { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
interface SplashScreenProps {
logo?: string;
restaurantName?: string;
duration?: number;
}
export default function SplashScreen({
logo,
restaurantName,
duration = 2000
}: SplashScreenProps) {
const [isVisible, setIsVisible] = useState(true);
const [imageLoaded, setImageLoaded] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
// setIsVisible(false);
}, duration);
return () => clearTimeout(timer);
}, [duration]);
return (
<AnimatePresence>
{isVisible && (
<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>
<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}
priority
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>
)}
{/*
{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>
)} */}
<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>
);
}
+2 -2
View File
@@ -42,10 +42,10 @@ export const TabContainerClassNames: TabContainerClassName = {
headerActive: '',
icon: 'text-xs transition-all duration-150',
iconDeactive: 'stroke-disabled-text',
iconActive: 'stroke-black dark:stroke-foreground',
iconActive: 'stroke-primary dark:stroke-foreground',
title: 'text-xs transition-all duration-150',
titleDeactive: 'text-disabled-text',
titleActive: 'text-black dark:text-foreground'
titleActive: 'text-primary dark:text-foreground'
}
function TabContainer({ className = TabContainerClassNames, changeType = TabContainerRenderType.RENDERER, defaultIndex = 0, itemRenderer, children, ...restProps }: Props) {
+1 -1
View File
@@ -44,7 +44,7 @@ function TopBar({ profileDropState, toggleProfileDropState, toggleMenuState }: P
S
</div> */}
{/* <div
className={`z-50 top-0 left-0 w-full h-full px-5 py-15 md:px-15 xl:p-0 fixed xl:relative bg-[#00000044] xl:bg-transparent backdrop-blur-sm xl:backdrop-blur-none justify-center xl:block ${searchModalState ? 'flex' : 'hidden'}`}>
className={`z-50 top-0 left-0 w-full h-full px-5 py-15 md:px-15 xl:p-0 fixed xl:relative bg-primary/25 xl:bg-transparent backdrop-blur-sm xl:backdrop-blur-none justify-center xl:block ${searchModalState ? 'flex' : 'hidden'}`}>
<div
className={`w-full h-full absolute`}
onClick={toggleSearchModalState}>
+1 -1
View File
@@ -38,7 +38,7 @@ function DialogOverlay({
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-primary/50",
className
)}
{...props}
+1 -1
View File
@@ -21,7 +21,7 @@ function RateBar({ percentage, content, className = '' }: Props) {
>
</motion.span>
</span>
<Star1 className='fill-black dark:fill-foreground me-1 mb-1' size={12} />
<Star1 className='fill-primary dark:fill-foreground me-1 mb-1' size={12} />
<span className='text-xs font-medium'>{content}</span>
</div>
)
+6 -6
View File
@@ -13,11 +13,11 @@ type Props = {
}
const defaultContent: Array<{ value: string, icon: React.ReactElement }> = [
{ value: '5', icon: <Star className='fill-black dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '4', icon: <Star className='fill-black dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '3', icon: <Star className='fill-black dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '2', icon: <Star className='fill-black dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '1', icon: <Star className='fill-black dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '5', icon: <Star className='fill-primary dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '4', icon: <Star className='fill-primary dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '3', icon: <Star className='fill-primary dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '2', icon: <Star className='fill-primary dark:fill-foreground me-1 mb-1' size={24} /> },
{ value: '1', icon: <Star className='fill-primary dark:fill-foreground me-1 mb-1' size={24} /> },
]
function RateSelectionBar({ name, content = defaultContent, className = '', onValueChange, defaultValue = [5] }: Props) {
@@ -36,7 +36,7 @@ function RateSelectionBar({ name, content = defaultContent, className = '', onVa
{content?.map((value, index) => {
return (
<span key={index} className='flex items-center gap-1'>
<Star1 className='fill-black dark:fill-neutral-200 me-0.5 mb-1' size={12} />
<Star1 className='fill-primary dark:fill-neutral-200 me-0.5 mb-1' size={12} />
<span className='text-xs font-medium'>{value.value}</span>
</span>
)
+108 -3
View File
@@ -1,23 +1,128 @@
'use client';
import usePreference from '@/hooks/helpers/usePreference';
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react';
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
import SplashScreen from '@/components/overlays/SplashScreen';
type Props = {
children: React.ReactNode
}
// تابع برای اعمال رنگ primary
function applyPrimaryColor(colorHex: string) {
const root = document.documentElement;
const hex = colorHex.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
const lightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const chroma = (max - min) * 0.15;
let hue = 0;
if (chroma !== 0) {
if (max === r) {
hue = ((g - b) / (max - min) + (g < b ? 6 : 0)) * 60;
} else if (max === g) {
hue = ((b - r) / (max - min) + 2) * 60;
} else {
hue = ((r - g) / (max - min) + 4) * 60;
}
}
const oklchColor = `oklch(${lightness.toFixed(3)} ${chroma.toFixed(3)} ${hue.toFixed(1)})`;
root.style.setProperty('--primary', oklchColor);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
const foregroundOklch = brightness > 0.5 ? 'oklch(0 0 0)' : 'oklch(1 0 0)';
root.style.setProperty('--primary-foreground', foregroundOklch);
}
function PreferenceWrapper({ children }: Props) {
const { state: nightMode } = usePreference('night-mode', false);
const { data: aboutData, isSuccess } = 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 [cachedLogo, setCachedLogo] = useState<string | null>(null);
const [cachedName, setCachedName] = useState<string | null>(null);
// بارگذاری رنگ و اطلاعات از localStorage در اولین رندر
useEffect(() => {
const theme = nightMode //|| window.matchMedia("(prefers-color-scheme: dark)").matches;
// بارگذاری رنگ
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 لود شده
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);
}
if (aboutData.data.name) {
localStorage.setItem('restaurant-name', aboutData.data.name);
}
applyPrimaryColor(aboutData.data.menuColor);
}
}, [aboutData?.data?.menuColor]);
// اعمال تم تاریک/روشن
useEffect(() => {
const theme = nightMode;
document.documentElement.setAttribute("data-theme", theme ? "dark" : "light");
}, [nightMode]);
// بستن اسپلش بعد از مدت زمان مشخص
useEffect(() => {
if (showSplash) {
const timer = setTimeout(() => {
setShowSplash(false);
}, 2500);
return () => clearTimeout(timer);
}
}, [showSplash]);
// استفاده از داده‌های cache شده برای اسپلش
const splashLogo = aboutData?.data?.logo || cachedLogo || undefined;
const splashName = aboutData?.data?.name || cachedName || undefined;
return (
<>
{children}
{showSplash && (
<SplashScreen
logo={splashLogo}
restaurantName={splashName}
duration={2500}
/>
)}
{!showSplash && children}
</>
)
}
+87
View File
@@ -0,0 +1,87 @@
"use client";
import { useEffect } from "react";
interface UseThemeColorProps {
primaryColor?: string | null;
enabled?: boolean;
}
/**
* تبدیل hex به RGB normalized (0-1)
*/
function hexToRgbNormalized(
hex: string
): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255,
}
: null;
}
/**
* تبدیل RGB به OKLCH (تقریبی)
*/
function rgbToOklch(r: number, g: number, b: number): string {
// محاسبه Lightness تقریبی
const lightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
// محاسبه Chroma و Hue (ساده شده)
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const chroma = (max - min) * 0.15; // مقدار تقریبی
let hue = 0;
if (chroma !== 0) {
if (max === r) {
hue = ((g - b) / (max - min) + (g < b ? 6 : 0)) * 60;
} else if (max === g) {
hue = ((b - r) / (max - min) + 2) * 60;
} else {
hue = ((r - g) / (max - min) + 4) * 60;
}
}
return `oklch(${lightness.toFixed(3)} ${chroma.toFixed(3)} ${hue.toFixed(
1
)})`;
}
/**
* هوک برای تغییر دینامیک رنگ primary بر اساس رنگ دریافتی از about
*/
export function useThemeColor({
primaryColor,
enabled = true,
}: UseThemeColorProps) {
useEffect(() => {
if (!enabled || !primaryColor) return;
try {
const root = document.documentElement;
// اگر رنگ hex هست، آن را به oklch تبدیل می‌کنیم
if (primaryColor.startsWith("#")) {
const rgb = hexToRgbNormalized(primaryColor);
if (rgb) {
const oklchColor = rgbToOklch(rgb.r, rgb.g, rgb.b);
// اعمال رنگ primary
root.style.setProperty("--primary", oklchColor);
// محاسبه رنگ foreground متناسب
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
const foregroundOklch =
brightness > 0.5 ? "oklch(0 0 0)" : "oklch(1 0 0)";
root.style.setProperty("--primary-foreground", foregroundOklch);
}
}
} catch (error) {
console.error("Error setting theme color:", error);
}
}, [primaryColor, enabled]);
}