This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { glassSurfaceNav } from '@/lib/styles/glassSurface';
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
import { useGetProducts } from '@/app/[name]/(Main)/hooks/useMenuData';
|
||||
import type { Product } from '@/app/[name]/(Main)/types/Types';
|
||||
@@ -105,7 +106,7 @@ const CartSummary = ({ isPremium }: CartSummaryProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-container border-t border-border p-4 w-full'>
|
||||
<div className={glassSurfaceNav('fixed bottom-0 left-0 right-0 z-50 border-t border-border p-4 w-full')}>
|
||||
<div className='flex justify-between items-center gap-4'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-xs text-gray-400 dark:text-disabled-text'>{t('PayableAmountLabel')}</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import { useGetAddresses } from '@/app/[name]/(Profile)/profile/address/hooks/useAddressData';
|
||||
import { Address } from '@/app/[name]/(Profile)/profile/address/types/Types';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import InputField from '@/components/input/InputField';
|
||||
import { useCheckoutStore } from '../../store/Store';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import InputField from '@/components/input/InputField';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import { Card, Icon, Wallet2 } from 'iconsax-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import Combobox from '@/components/combobox/Combobox';
|
||||
import { Box, Location, Car, Building } from 'iconsax-react';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import { MinusIcon, PlusIcon } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
@@ -13,6 +13,7 @@ import Image from 'next/image';
|
||||
import React from 'react'
|
||||
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
|
||||
import AboutSkeleton from './components/AboutSkeleton';
|
||||
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from '@/lib/styles/glassSurface';
|
||||
|
||||
const sortings = [
|
||||
'جدیدترین',
|
||||
@@ -81,7 +82,7 @@ function AboutPage() {
|
||||
return (
|
||||
<section aria-labelledby="about-title" className='py-4'>
|
||||
<section
|
||||
className="bg-container rounded-container shadow-container p-4">
|
||||
className={glassSurfaceCard('p-4')}>
|
||||
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
||||
<div className="">
|
||||
<h2 className='text-sm2 font-bold leading-5'>{shop.name}</h2>
|
||||
@@ -123,7 +124,7 @@ function AboutPage() {
|
||||
|
||||
{(shop.phone || shop.telegram || shop.whatsapp || shop.instagram) && (
|
||||
<section
|
||||
className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
|
||||
className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center')}>
|
||||
<h2 className='text-sm2 font-medium leading-5'>ارتباط</h2>
|
||||
<div className='col-span-1 text-center flex justify-center gap-4'>
|
||||
{shop.phone &&
|
||||
@@ -151,7 +152,7 @@ function AboutPage() {
|
||||
)}
|
||||
|
||||
{shop.address && (
|
||||
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
|
||||
<section className={glassSurfaceCard('px-4 pt-6 pb-6 mt-4')}>
|
||||
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
|
||||
<p className='text-sm2 mt-[9px] leading-5'>{shop.address}</p>
|
||||
{shop.latitude && shop.longitude && (
|
||||
@@ -166,7 +167,7 @@ function AboutPage() {
|
||||
{schedules.length > 0 && getTodaySchedule() && (
|
||||
<section
|
||||
aria-label='ساعات کاری'
|
||||
className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
|
||||
className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||
<div className="flex items-center gap-2 justify-start">
|
||||
<Clock size={16} className='stroke-disabled-text' />
|
||||
<div className='text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]'>باز</div>
|
||||
@@ -254,7 +255,7 @@ function AboutPage() {
|
||||
<section aria-labelledby="reviews-title" className='py-4'>
|
||||
<section
|
||||
aria-label='امتیاز کاربران'
|
||||
className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
|
||||
className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
|
||||
<div className="text-center font-bold text-5xl">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
@@ -267,10 +268,10 @@ function AboutPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
|
||||
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
|
||||
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
||||
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
|
||||
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
|
||||
<button onClick={toggleSortingModal} className={glassSurfaceFlat('rounded-xl h-8 pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]')}>
|
||||
<EqualizerIcon className='dark:text-white' />
|
||||
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
|
||||
</button>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import TabContainer from "@/components/tab/TabContainer";
|
||||
import { TabHeader } from "@/components/tab/TabHeader";
|
||||
import { glassSurfaceCard, glassSurfaceCardFlat } from '@/lib/styles/glassSurface';
|
||||
import { InfoCircle, Star1 } from 'iconsax-react';
|
||||
|
||||
const AboutSkeleton = () => {
|
||||
const firstTabSkeleton = () => (
|
||||
<section className='py-4'>
|
||||
{/* اطلاعات فروشگاه */}
|
||||
<section className="bg-container rounded-container shadow-container p-4">
|
||||
<section className={glassSurfaceCard('p-4')}>
|
||||
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
||||
<div className="flex-1">
|
||||
<Skeleton className="h-6 w-32 rounded-md mb-4" />
|
||||
@@ -30,8 +30,7 @@ const AboutSkeleton = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ارتباط */}
|
||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
|
||||
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center')}>
|
||||
<Skeleton className="h-5 w-16 rounded-md" />
|
||||
<div className='col-span-1 text-center flex justify-center gap-4'>
|
||||
{[...Array(3)].map((_, index) => (
|
||||
@@ -40,8 +39,7 @@ const AboutSkeleton = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* آدرس */}
|
||||
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
|
||||
<section className={glassSurfaceCard('px-4 pt-6 pb-6 mt-4')}>
|
||||
<Skeleton className="h-5 w-12 rounded-md mb-[9px]" />
|
||||
<Skeleton className="h-4 w-full rounded-md mb-2" />
|
||||
<Skeleton className="h-4 w-3/4 rounded-md mb-[23px]" />
|
||||
@@ -51,8 +49,7 @@ const AboutSkeleton = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ساعات کاری امروز */}
|
||||
<section className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
|
||||
<section className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-4 h-4 rounded-full" />
|
||||
<Skeleton className="h-5 w-8 rounded-md" />
|
||||
@@ -62,11 +59,10 @@ const AboutSkeleton = () => {
|
||||
<Skeleton className="w-4 h-4 rounded-md" />
|
||||
</section>
|
||||
|
||||
{/* ساعات کاری هفته */}
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-[#F6F6FA] dark:bg-border rounded-container leading-5 py-6 px-4 mt-4 flex justify-between items-center">
|
||||
className={glassSurfaceCardFlat('leading-5 py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||
<Skeleton className="h-5 w-20 rounded-md" />
|
||||
<Skeleton className="h-5 w-24 rounded-md" />
|
||||
</div>
|
||||
@@ -76,8 +72,7 @@ const AboutSkeleton = () => {
|
||||
|
||||
const secondTabSkeleton = () => (
|
||||
<section className='py-4'>
|
||||
{/* امتیاز کاربران */}
|
||||
<section className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
|
||||
<section className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
|
||||
<Skeleton className="h-16 w-20 rounded-md mx-auto" />
|
||||
<div className="flex flex-col w-fit items-end gap-1">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
@@ -86,8 +81,7 @@ const AboutSkeleton = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* نظرات کاربران */}
|
||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
|
||||
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
|
||||
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
||||
<Skeleton className="h-5 w-24 rounded-md" />
|
||||
<Skeleton className="h-8 w-28 rounded-xl" />
|
||||
@@ -130,4 +124,3 @@ const AboutSkeleton = () => {
|
||||
};
|
||||
|
||||
export default AboutSkeleton;
|
||||
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/AboutService";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import * as api from "../service/AboutService";
|
||||
import {
|
||||
getPersistedAboutData,
|
||||
setPersistedAboutData,
|
||||
} from "@/lib/helpers/themeCache";
|
||||
|
||||
export const useGetAbout = () => {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
return useQuery({
|
||||
queryKey: ["about"],
|
||||
queryFn: () => api.getAbout(name),
|
||||
const query = useQuery({
|
||||
queryKey: ["about", name],
|
||||
queryFn: async () => {
|
||||
const data = await api.getAbout(name);
|
||||
setPersistedAboutData(name, data);
|
||||
return data;
|
||||
},
|
||||
enabled: !!name,
|
||||
retry: false,
|
||||
staleTime: 60 * 60_000,
|
||||
gcTime: 24 * 60 * 60_000,
|
||||
refetchOnMount: "always",
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: () => (name ? getPersistedAboutData(name) : undefined),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (name && query.data) {
|
||||
setPersistedAboutData(name, query.data);
|
||||
}
|
||||
}, [name, query.data]);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const useGetReviews = () => {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
return useQuery({
|
||||
queryKey: ["reviews"],
|
||||
queryKey: ["reviews", name],
|
||||
queryFn: () => api.getReviews(name),
|
||||
enabled: !!name,
|
||||
retry: false,
|
||||
@@ -25,7 +47,7 @@ export const useGetReviews = () => {
|
||||
export const useGetSchedules = () => {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
return useQuery({
|
||||
queryKey: ["schedules"],
|
||||
queryKey: ["schedules", name],
|
||||
queryFn: () => api.getSchedules(name),
|
||||
enabled: !!name,
|
||||
retry: false,
|
||||
|
||||
@@ -26,6 +26,11 @@ export interface Shop {
|
||||
logo: string | null;
|
||||
address: string | null;
|
||||
menuColor: string | null;
|
||||
bgType?: "pattern" | "custom" | "color";
|
||||
bgBlur?: number;
|
||||
bgOpacity?: number;
|
||||
bgOverlay?: string;
|
||||
bgUrl?: string;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
serviceArea: ServiceArea;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Category } from "@/app/[name]/(Main)/types/Types";
|
||||
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
||||
import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer";
|
||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||
import { GLASS_SURFACE_DISABLED, GLASS_SURFACE_SELECTED } from "@/lib/styles/glassSurface";
|
||||
import clsx from "clsx";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
@@ -19,14 +20,14 @@ const SVG_MASK_STYLE = {
|
||||
WebkitMaskPosition: "center",
|
||||
} as const;
|
||||
|
||||
function CategoryImage({ src, size, alt, tintWithPrimary = true }: { src: string; size: number; alt: string; tintWithPrimary?: boolean }) {
|
||||
function CategoryImage({ src, size, alt, tintWithPrimary = true, disabled = false }: { src: string; size: number; alt: string; tintWithPrimary?: boolean; disabled?: boolean }) {
|
||||
const isSvg = src.endsWith(".svg");
|
||||
const shouldUseSvgMask = isSvg && tintWithPrimary;
|
||||
|
||||
if (shouldUseSvgMask) {
|
||||
return (
|
||||
<div
|
||||
className="shrink-0 bg-primary"
|
||||
className={clsx("shrink-0 bg-primary", disabled && "opacity-40")}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -48,7 +49,7 @@ function CategoryImage({ src, size, alt, tintWithPrimary = true }: { src: string
|
||||
height={size}
|
||||
alt={alt}
|
||||
loading="lazy"
|
||||
className="shrink-0 object-contain"
|
||||
className={clsx("shrink-0 object-contain", disabled && "opacity-40")}
|
||||
onError={(event) => {
|
||||
event.currentTarget.src = "/assets/images/food-image.png";
|
||||
}}
|
||||
@@ -106,9 +107,9 @@ const CategoryScroll = ({ categories, selectedCategory, onSelect, variant = "lar
|
||||
const isSelected = item.id === selectedCategory || item.children?.some((ch) => ch.id === selectedCategory);
|
||||
|
||||
return (
|
||||
<Renderer key={item.id} className={clsx(isSelected && "bg-container!")} onClick={handleSelect(item.id)}>
|
||||
<CategoryImage src={item.avatarUrl || "/assets/images/food-image.png"} size={imageSize} alt="category image" tintWithPrimary={!usesColoredSvg} />
|
||||
<span className="text-xs text-foreground text-center">{item.title}</span>
|
||||
<Renderer key={item.id} className={clsx(!isSelected && GLASS_SURFACE_DISABLED, isSelected && GLASS_SURFACE_SELECTED)} onClick={handleSelect(item.id)}>
|
||||
<CategoryImage src={item.avatarUrl || "/assets/images/food-image.png"} size={imageSize} alt="category image" tintWithPrimary={!usesColoredSvg} disabled={!item.isActive} />
|
||||
<span className={clsx("text-xs text-foreground text-center", !item.isActive && "text-disabled-text")}>{item.title}</span>
|
||||
</Renderer>
|
||||
);
|
||||
})}
|
||||
@@ -120,7 +121,7 @@ const CategoryScroll = ({ categories, selectedCategory, onSelect, variant = "lar
|
||||
const isChildSelected = child.id === selectedCategory;
|
||||
|
||||
return (
|
||||
<CategorySmallItemRenderer key={child.id} compact className={clsx(isChildSelected && "bg-container!")} onClick={handleSelect(child.id)}>
|
||||
<CategorySmallItemRenderer key={child.id} compact className={clsx(isChildSelected && GLASS_SURFACE_SELECTED, !isChildSelected && GLASS_SURFACE_DISABLED)} onClick={handleSelect(child.id)}>
|
||||
{child.avatarUrl && <CategoryImage src={child.avatarUrl} size={16} alt="category image" tintWithPrimary={!usesColoredSvg} />}
|
||||
<span className="text-[10px] text-foreground whitespace-nowrap">{child.title}</span>
|
||||
</CategorySmallItemRenderer>
|
||||
|
||||
@@ -1,23 +1,56 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/MenuService";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
getPersistedCategoriesData,
|
||||
getPersistedMenuData,
|
||||
setPersistedCategoriesData,
|
||||
setPersistedMenuData,
|
||||
} from "@/lib/helpers/themeCache";
|
||||
|
||||
export const useGetProducts = () => {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
return useQuery({
|
||||
queryKey: ["menu"],
|
||||
queryFn: () => api.getProducts(name),
|
||||
const query = useQuery({
|
||||
queryKey: ["menu", name],
|
||||
queryFn: async () => {
|
||||
const data = await api.getProducts(name);
|
||||
setPersistedMenuData(name, data);
|
||||
return data;
|
||||
},
|
||||
enabled: !!name,
|
||||
placeholderData: () => (name ? getPersistedMenuData(name) : undefined),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (name && query.data) {
|
||||
setPersistedMenuData(name, query.data);
|
||||
}
|
||||
}, [name, query.data]);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const useGetCategories = () => {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
return useQuery({
|
||||
queryKey: ["categories"],
|
||||
queryFn: () => api.getCategories(name),
|
||||
const query = useQuery({
|
||||
queryKey: ["categories", name],
|
||||
queryFn: async () => {
|
||||
const data = await api.getCategories(name);
|
||||
setPersistedCategoriesData(name, data);
|
||||
return data;
|
||||
},
|
||||
enabled: !!name,
|
||||
placeholderData: () => (name ? getPersistedCategoriesData(name) : undefined),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (name && query.data) {
|
||||
setPersistedCategoriesData(name, query.data);
|
||||
}
|
||||
}, [name, query.data]);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const useGetNotificationsCount = () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import MenuItem, { type MenuItemViewMode } from "@/components/listview/MenuItem"
|
||||
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
||||
import VerticalScrollView from "@/components/listview/VerticalScrollView";
|
||||
import useToggle from "@/hooks/helpers/useToggle";
|
||||
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||
import clsx from "clsx";
|
||||
import { motion } from "framer-motion";
|
||||
import { RowHorizontal, RowVertical } from "iconsax-react";
|
||||
@@ -185,7 +186,7 @@ const MenuIndex = () => {
|
||||
<div className="flex gap-2 justify-between items-center relative" ref={smallCategoriesRef}>
|
||||
<span className="sm:text-base text-sm font-medium">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
|
||||
<div className="inline-flex gap-2 justify-end items-center">
|
||||
<div className="inline-flex rounded-xl h-8 bg-container p-1">
|
||||
<div className={glassSurfaceFlat("inline-flex rounded-xl h-8 p-1")}>
|
||||
<button
|
||||
onClick={() => setViewModeAndPersist("list")}
|
||||
className={clsx(
|
||||
@@ -207,7 +208,7 @@ const MenuIndex = () => {
|
||||
<RowHorizontal color="black" className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={toggleFilterSortModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center gap-2">
|
||||
<button onClick={toggleFilterSortModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center gap-2")}>
|
||||
<TextAlignIcon className="text-foreground" />
|
||||
<span className="text-xs leading-5 font-medium">{tMenu("MenuFilterDrawer.Label")}</span>
|
||||
</button>
|
||||
@@ -218,7 +219,7 @@ const MenuIndex = () => {
|
||||
initial={{ y: smallCategoriesVisible ? 0 : -50, opacity: smallCategoriesVisible ? 1 : 0 }}
|
||||
animate={{ y: smallCategoriesVisible ? 0 : -50, opacity: smallCategoriesVisible ? 1 : 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
className={clsx("fixed left-0 z-10 top-0 px-4 pt-16 bg-[#F4F5F9CC] dark:bg-background/70 backdrop-blur-[44px] right-0 xl:pr-72 xl:pt-20", ``)}
|
||||
className={clsx(glassSurfaceFlat("fixed left-0 z-10 top-0 px-4 pt-16 right-0 xl:pr-72 xl:pt-20 border-x-0 border-t-0 rounded-none"), !smallCategoriesVisible && "pointer-events-none")}
|
||||
>
|
||||
<CategoryScroll categories={categories} selectedCategory={selectedCategory} onSelect={updateCategory} variant="small" />
|
||||
</motion.div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
import { ef } from '@/lib/helpers/utfNumbers';
|
||||
import { ArrowLeft, Edit2, TickCircle, Trash } from 'iconsax-react';
|
||||
@@ -102,7 +104,7 @@ function UserAddressesPage({ }: Props) {
|
||||
addresses.map((address) => (
|
||||
<div
|
||||
key={address.id}
|
||||
className={`bg-container rounded-container w-full shadow-xl px-4 py-4 flex flex-col justify-between ${address.isDefault ? 'border border-primary' : ''}`}
|
||||
className={glassSurfaceCard('w-full px-4 py-4 flex flex-col justify-between', address.isDefault && 'border border-primary')}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h2 className='text-sm font-medium'>{address.title}</h2>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
import { Button as ShadButton } from '@/components/ui/button';
|
||||
@@ -197,7 +198,7 @@ function ProfileIndex({ }: Props) {
|
||||
<h1 className='text-sm2 place-self-center font-medium'>ویرایش اطلاعات</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={formik.handleSubmit} className="mt-8 flex-1 bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
|
||||
<form onSubmit={formik.handleSubmit} className={glassSurfaceCard('mt-8 flex-1 w-full py-6 px-4 flex flex-col justify-between')}>
|
||||
<div className="bg-inherit">
|
||||
<div className="justify-self-center w-fit relative text-center pb-10">
|
||||
<input
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
// import { useProfile } from '@/hooks/auth/useProfile';
|
||||
@@ -43,7 +44,7 @@ function ProfileIndex({ }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex-1 h-full bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
|
||||
<div className={glassSurfaceCard('mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between')}>
|
||||
|
||||
<div className="">
|
||||
<div className="flex items-center justify-start gap-3 pb-6 border-b-[1.5px] border-border">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
import PasswordField from '@/components/input/PasswordField';
|
||||
@@ -38,7 +39,7 @@ function UserSettingsIndex() {
|
||||
return (
|
||||
<section aria-labelledby="about-title" className='py-4 flex-1 h-full'>
|
||||
<section
|
||||
className="bg-container rounded-container shadow-container p-4">
|
||||
className={glassSurfaceCard('p-4')}>
|
||||
|
||||
<div className="flex justify-between items-center pb-[25px]">
|
||||
<h2 className='text-sm leading-5'>{data.TabNotifications.Heading}</h2>
|
||||
@@ -94,7 +95,7 @@ function UserSettingsIndex() {
|
||||
return (
|
||||
<section aria-labelledby="reviews-title" className='py-4'>
|
||||
<section
|
||||
className="bg-container rounded-container shadow-container p-4">
|
||||
className={glassSurfaceCard('p-4')}>
|
||||
|
||||
<div className="flex justify-between items-center pb-[25px]">
|
||||
<h2 className='text-sm leading-5'>{data.TabPassword.Heading}</h2>
|
||||
@@ -154,7 +155,7 @@ function UserSettingsIndex() {
|
||||
return (
|
||||
<section aria-labelledby="reviews-title" className='py-4'>
|
||||
<div
|
||||
className="bg-container rounded-container shadow-container p-4">
|
||||
className={glassSurfaceCard('p-4')}>
|
||||
|
||||
<div className="pb-4">
|
||||
<h2 className='text-sm leading-5'>{data.TabAuthenticator.Heading}</h2>
|
||||
@@ -236,7 +237,7 @@ function UserSettingsIndex() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className='bg-container rounded-container shadow-container p-4'>
|
||||
<section className={glassSurfaceCard('p-4')}>
|
||||
<div className='flex justify-between items-center pb-4'>
|
||||
<h2 className='text-sm leading-5'>{data.StatisticalParticipation.Heading}</h2>
|
||||
<Switch
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
import Button from '@/components/button/PrimaryButton';
|
||||
import InputField from '@/components/input/InputField';
|
||||
@@ -34,7 +35,7 @@ function UserWalletIndex({ }: Props) {
|
||||
|
||||
<form
|
||||
onSubmit={chargeAction}
|
||||
className="mt-8 flex-1 h-full bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
|
||||
className={glassSurfaceCard('mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between')}>
|
||||
|
||||
<div className="">
|
||||
<div className="flex items-center justify-center text-center gap-4 pb-6 border-b-[1.5px] border-border">
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { fetchWithTimeout } from "@/lib/helpers/fetchWithTimeout";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
const CACHE_MAX_AGE = "public, max-age=3600";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.searchParams.get("url");
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "url is required" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
||||
}
|
||||
const res = await fetchWithTimeout(url, {
|
||||
timeoutMs: 15_000,
|
||||
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch SVG" },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
const text = await res.text();
|
||||
if (!text.trim().toLowerCase().includes("<svg")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Response is not SVG" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return new NextResponse(text, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Cache-Control": CACHE_MAX_AGE,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch SVG" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import PreferenceWrapper from '@/components/wrapper/PreferenceWrapper'
|
||||
import RestaurantLayoutClient from '@/components/RestaurantLayoutClient'
|
||||
import React from 'react'
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import { getShop } from './lib/getShop'
|
||||
import { notFound } from 'next/navigation'
|
||||
import CartChecker from '@/components/CartChecker'
|
||||
import ActiveChecker from '@/components/ActiveChecker'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
@@ -97,11 +95,7 @@ export default async function Layout({
|
||||
try {
|
||||
const { name } = await params
|
||||
await getShop(name)
|
||||
return <>
|
||||
<PreferenceWrapper>{children}</PreferenceWrapper>
|
||||
<CartChecker />
|
||||
<ActiveChecker />
|
||||
</>
|
||||
return <RestaurantLayoutClient>{children}</RestaurantLayoutClient>
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'SHOP_NOT_FOUND') {
|
||||
notFound()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "../../public/assets/css/fonts.css";
|
||||
@import "tw-animate-css";
|
||||
@import "../styles/glass.css";
|
||||
|
||||
/* @custom-variant dark (&:is(.dark *)); */
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
@@ -386,3 +387,37 @@ html[data-theme="dark"] {
|
||||
.game-explanation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] #root {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] [data-slot="dialog-content"] {
|
||||
background-color: var(--container) !important;
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] [data-slot="splash-screen"] {
|
||||
background-color: var(--container) !important;
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] .pattern-secondary-bg {
|
||||
background-color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
html[data-image-bg="true"] #root {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
html[data-image-bg="true"] [data-slot="dialog-content"] {
|
||||
background-color: var(--container) !important;
|
||||
}
|
||||
|
||||
html[data-image-bg="true"] [data-slot="splash-screen"] {
|
||||
background-color: var(--container) !important;
|
||||
}
|
||||
|
||||
html[data-image-bg="true"] .pattern-secondary-bg {
|
||||
background-color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
+4
-1
@@ -7,6 +7,7 @@ import initTranslations from '@/lib/i18n';
|
||||
import TranslationsProvider from "@/components/utils/TranslationsProdiver";
|
||||
import ToastContainer from "@/components/Toast";
|
||||
import { ThemeColorSetter } from "@/components/ThemeColorSetter";
|
||||
import { ThemeBootScript } from "@/components/ThemeBootScript";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard',
|
||||
@@ -43,7 +44,9 @@ export default async function RootLayout({
|
||||
dir='rtl'
|
||||
className="h-svh overflow-hidden"
|
||||
data-theme="light" >
|
||||
<head />
|
||||
<head>
|
||||
<ThemeBootScript />
|
||||
</head>
|
||||
<body
|
||||
|
||||
className={`antialiased bg-background h-svh overflow-hidden`}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import ActiveChecker from "@/components/ActiveChecker";
|
||||
import AppBackground from "@/components/background/AppBackground";
|
||||
import CartChecker from "@/components/CartChecker";
|
||||
import { PatternBackgroundProvider } from "@/components/background/PatternBackgroundProvider";
|
||||
import PreferenceWrapper from "@/components/wrapper/PreferenceWrapper";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function RestaurantLayoutClient({ children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<PatternBackgroundProvider>
|
||||
<PreferenceWrapper>
|
||||
<AppBackground />
|
||||
{children}
|
||||
</PreferenceWrapper>
|
||||
</PatternBackgroundProvider>
|
||||
<CartChecker />
|
||||
<ActiveChecker />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getThemeBootScript } from "@/lib/helpers/themeBootScript";
|
||||
|
||||
export function ThemeBootScript() {
|
||||
return (
|
||||
<script
|
||||
id="theme-boot"
|
||||
dangerouslySetInnerHTML={{ __html: getThemeBootScript() }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils'
|
||||
import { getCachedMenuColor, getRestaurantSlugFromPath, getThemeCache, applyCachedThemeToDom } from '@/lib/helpers/themeCache'
|
||||
|
||||
export function ThemeColorSetter() {
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
try {
|
||||
const savedColor = localStorage.getItem('theme-primary-color')
|
||||
const slug = getRestaurantSlugFromPath()
|
||||
const cachedTheme = slug ? getThemeCache(slug) : null
|
||||
if (cachedTheme) {
|
||||
applyCachedThemeToDom(cachedTheme)
|
||||
return
|
||||
}
|
||||
|
||||
const savedColor = getCachedMenuColor(slug)
|
||||
if (savedColor) {
|
||||
const oklchColor = hexToOklch(savedColor)
|
||||
|
||||
@@ -25,4 +33,3 @@ export function ThemeColorSetter() {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { usePatternBackground } from "@/components/background/PatternBackgroundProvider";
|
||||
import usePreference from "@/hooks/helpers/usePreference";
|
||||
import { THEME_BG_ELEMENT_ID } from "@/lib/helpers/themeCache";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
const THEME_BG_IMG_ID = "cached-theme-bg-img";
|
||||
const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay";
|
||||
|
||||
export default function AppBackground() {
|
||||
const { state: nightMode } = usePreference("night-mode", false);
|
||||
const { isPattern, backgroundStyle, isCustomImage, customImageSettings } =
|
||||
usePatternBackground();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (nightMode || (!isPattern && !isCustomImage)) {
|
||||
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
let element = document.getElementById(THEME_BG_ELEMENT_ID);
|
||||
if (!element) {
|
||||
element = document.createElement("div");
|
||||
element.id = THEME_BG_ELEMENT_ID;
|
||||
element.setAttribute("aria-hidden", "true");
|
||||
element.style.position = "fixed";
|
||||
element.style.inset = "0";
|
||||
element.style.pointerEvents = "none";
|
||||
element.style.zIndex = "0";
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
|
||||
if (isPattern) {
|
||||
element.style.overflow = "";
|
||||
document.getElementById(THEME_BG_IMG_ID)?.remove();
|
||||
document.getElementById(THEME_BG_OVERLAY_ID)?.remove();
|
||||
Object.assign(element.style, backgroundStyle as Record<string, string>);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCustomImage && customImageSettings) {
|
||||
const { bgUrl, bgBlur, bgOpacity, bgOverlay } = customImageSettings;
|
||||
|
||||
element.style.backgroundColor = "";
|
||||
element.style.backgroundImage = "";
|
||||
element.style.overflow = "hidden";
|
||||
|
||||
let imgLayer = document.getElementById(THEME_BG_IMG_ID);
|
||||
if (!imgLayer) {
|
||||
imgLayer = document.createElement("div");
|
||||
imgLayer.id = THEME_BG_IMG_ID;
|
||||
imgLayer.style.position = "absolute";
|
||||
imgLayer.style.inset = "-40px";
|
||||
imgLayer.style.backgroundSize = "cover";
|
||||
imgLayer.style.backgroundPosition = "center";
|
||||
imgLayer.style.backgroundRepeat = "no-repeat";
|
||||
element.appendChild(imgLayer);
|
||||
}
|
||||
|
||||
imgLayer.style.backgroundImage = `url("${bgUrl}")`;
|
||||
imgLayer.style.filter = bgBlur ? `blur(${bgBlur}px)` : "";
|
||||
imgLayer.style.opacity =
|
||||
bgOpacity != null ? String(bgOpacity / 100) : "1";
|
||||
|
||||
let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID);
|
||||
if (!overlayLayer) {
|
||||
overlayLayer = document.createElement("div");
|
||||
overlayLayer.id = THEME_BG_OVERLAY_ID;
|
||||
overlayLayer.style.position = "absolute";
|
||||
overlayLayer.style.inset = "0";
|
||||
element.appendChild(overlayLayer);
|
||||
}
|
||||
|
||||
const overlayOpacity =
|
||||
bgOverlay != null ? Number(bgOverlay) / 100 : 0;
|
||||
overlayLayer.style.backgroundColor =
|
||||
overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : "";
|
||||
}
|
||||
}, [backgroundStyle, isCustomImage, isPattern, customImageSettings, nightMode]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
|
||||
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
|
||||
import {
|
||||
applyColorBackgroundCssVariables,
|
||||
applyCustomImageCssVariables,
|
||||
applyPatternCssVariables,
|
||||
clearCustomImageCssVariables,
|
||||
clearPatternCssVariables,
|
||||
colorizeSvg,
|
||||
hexToRgba,
|
||||
normalizeBgNumber,
|
||||
PATTERN_BACKGROUND_OPACITY,
|
||||
resolveBgType,
|
||||
} from "@/lib/helpers/backgroundUtils";
|
||||
import usePreference from "@/hooks/helpers/usePreference";
|
||||
import {
|
||||
applyCachedThemeToDom,
|
||||
buildThemeCacheFromRestaurant,
|
||||
clearRestaurantBackgroundOverrides,
|
||||
getThemeCache,
|
||||
removeCachedThemeBackground,
|
||||
setThemeCache,
|
||||
svgToDataUrl,
|
||||
themeBackgroundMatches,
|
||||
type CachedRestaurantTheme,
|
||||
} from "@/lib/helpers/themeCache";
|
||||
import { useParams } from "next/navigation";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
const DEFAULT_MENU_COLOR = "#1E3A8A";
|
||||
|
||||
export type CustomImageSettings = {
|
||||
bgUrl: string;
|
||||
bgBlur: number | null;
|
||||
bgOpacity: number | null;
|
||||
bgOverlay: string | null;
|
||||
};
|
||||
|
||||
type PatternBackgroundContextValue = {
|
||||
isPattern: boolean;
|
||||
isCustomImage: boolean;
|
||||
backgroundStyle: CSSProperties;
|
||||
customImageSettings: CustomImageSettings | null;
|
||||
};
|
||||
|
||||
const PatternBackgroundContext = createContext<PatternBackgroundContextValue>({
|
||||
isPattern: false,
|
||||
isCustomImage: false,
|
||||
backgroundStyle: {},
|
||||
customImageSettings: null,
|
||||
});
|
||||
|
||||
export function PatternBackgroundProvider({ children }: { children: ReactNode }) {
|
||||
const { name: restaurantSlug } = useParams<{ name: string }>();
|
||||
const { state: nightMode } = usePreference("night-mode", false);
|
||||
const { data: aboutData } = useGetAbout();
|
||||
const restaurant = aboutData?.data;
|
||||
const [hydratedCache, setHydratedCache] = useState<CachedRestaurantTheme | null>(null);
|
||||
const [patternImageUrl, setPatternImageUrl] = useState<string | null>(null);
|
||||
|
||||
const bgType = restaurant
|
||||
? resolveBgType(restaurant)
|
||||
: hydratedCache
|
||||
? resolveBgType(hydratedCache)
|
||||
: "color";
|
||||
|
||||
const isPattern = !nightMode && bgType === "pattern";
|
||||
const isCustomImage = !nightMode && bgType === "custom";
|
||||
const menuColor =
|
||||
restaurant?.menuColor || hydratedCache?.menuColor || DEFAULT_MENU_COLOR;
|
||||
const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!restaurantSlug) return;
|
||||
|
||||
const cached = getThemeCache(restaurantSlug);
|
||||
setHydratedCache(cached);
|
||||
|
||||
if (!cached) return;
|
||||
|
||||
applyCachedThemeToDom(cached);
|
||||
if (!nightMode && cached.patternDataUrl) {
|
||||
setPatternImageUrl(cached.patternDataUrl);
|
||||
}
|
||||
}, [nightMode, restaurantSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!restaurantSlug) {
|
||||
clearPatternCssVariables();
|
||||
clearCustomImageCssVariables();
|
||||
removeCachedThemeBackground();
|
||||
setPatternImageUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nightMode) {
|
||||
clearPatternCssVariables();
|
||||
clearCustomImageCssVariables();
|
||||
clearRestaurantBackgroundOverrides();
|
||||
setPatternImageUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!restaurant) return;
|
||||
|
||||
const themeBase = buildThemeCacheFromRestaurant(restaurant);
|
||||
const currentBgType = themeBase.bgType;
|
||||
|
||||
if (currentBgType === "color") {
|
||||
clearCustomImageCssVariables();
|
||||
removeCachedThemeBackground();
|
||||
setPatternImageUrl(null);
|
||||
applyColorBackgroundCssVariables(menuColor);
|
||||
setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentBgType === "custom") {
|
||||
clearPatternCssVariables();
|
||||
setPatternImageUrl(null);
|
||||
applyCustomImageCssVariables();
|
||||
setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null });
|
||||
return;
|
||||
}
|
||||
|
||||
clearCustomImageCssVariables();
|
||||
|
||||
applyPatternCssVariables(menuColor);
|
||||
|
||||
if (!themeBase.bgUrl) {
|
||||
setPatternImageUrl(null);
|
||||
setThemeCache(restaurantSlug, {
|
||||
...themeBase,
|
||||
patternDataUrl: null,
|
||||
});
|
||||
const saved = getThemeCache(restaurantSlug);
|
||||
if (saved) {
|
||||
applyCachedThemeToDom(saved);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = getThemeCache(restaurantSlug);
|
||||
const cacheHit =
|
||||
cached &&
|
||||
themeBackgroundMatches(cached, restaurant) &&
|
||||
cached.isPattern &&
|
||||
cached.patternDataUrl;
|
||||
|
||||
if (cacheHit) {
|
||||
setPatternImageUrl(cached.patternDataUrl!);
|
||||
setThemeCache(restaurantSlug, {
|
||||
...themeBase,
|
||||
patternDataUrl: cached.patternDataUrl,
|
||||
});
|
||||
applyCachedThemeToDom(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const proxyUrl = `/${restaurantSlug}/api/proxy-svg?url=${encodeURIComponent(themeBase.bgUrl)}`;
|
||||
|
||||
fetch(proxyUrl)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch background pattern");
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((svg) => {
|
||||
if (cancelled) return;
|
||||
|
||||
const dataUrl = svgToDataUrl(colorizeSvg(svg, menuColor));
|
||||
setPatternImageUrl(dataUrl);
|
||||
|
||||
const nextCache = {
|
||||
...themeBase,
|
||||
patternDataUrl: dataUrl,
|
||||
};
|
||||
setThemeCache(restaurantSlug, nextCache);
|
||||
|
||||
const saved = getThemeCache(restaurantSlug);
|
||||
if (saved) {
|
||||
applyCachedThemeToDom(saved);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled && cached?.patternDataUrl) {
|
||||
setPatternImageUrl(cached.patternDataUrl);
|
||||
applyCachedThemeToDom(cached);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [menuColor, nightMode, restaurant, restaurantSlug]);
|
||||
|
||||
const backgroundStyle = useMemo<CSSProperties>(
|
||||
() => ({
|
||||
backgroundColor: baseTint,
|
||||
...(patternImageUrl
|
||||
? {
|
||||
backgroundImage: `url("${patternImageUrl}")`,
|
||||
backgroundRepeat: "repeat",
|
||||
backgroundSize: "auto",
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
[baseTint, patternImageUrl],
|
||||
);
|
||||
|
||||
const customImageSettings = useMemo<CustomImageSettings | null>(() => {
|
||||
const src = restaurant?.bgUrl || hydratedCache?.bgUrl;
|
||||
if (!isCustomImage || !src) return null;
|
||||
return {
|
||||
bgUrl: src,
|
||||
bgBlur: normalizeBgNumber(restaurant?.bgBlur ?? hydratedCache?.bgBlur),
|
||||
bgOpacity: normalizeBgNumber(restaurant?.bgOpacity ?? hydratedCache?.bgOpacity),
|
||||
bgOverlay: restaurant?.bgOverlay ?? hydratedCache?.bgOverlay ?? null,
|
||||
};
|
||||
}, [isCustomImage, restaurant, hydratedCache]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ isPattern, isCustomImage, backgroundStyle, customImageSettings }),
|
||||
[backgroundStyle, isPattern, isCustomImage, customImageSettings],
|
||||
);
|
||||
|
||||
return (
|
||||
<PatternBackgroundContext.Provider value={value}>
|
||||
{children}
|
||||
</PatternBackgroundContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePatternBackground() {
|
||||
return useContext(PatternBackgroundContext);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import SearchIcon from '../icons/SearchIcon'
|
||||
import clsx from 'clsx'
|
||||
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
|
||||
|
||||
interface SearchboxProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'results'> {
|
||||
@@ -19,9 +19,9 @@ export default function SearchBox({
|
||||
}: SearchboxProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-container inline-flex rounded-xl px-4 h-10 w-full items-center content-center',
|
||||
className
|
||||
className={glassSurfaceFlat(
|
||||
'inline-flex rounded-xl px-4 h-10 w-full items-center content-center',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<SearchIcon stroke='#8C90A3' />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
@@ -8,10 +9,10 @@ type Props = {
|
||||
|
||||
function CategoryItemRenderer({ children, ...rest }: Props) {
|
||||
return (
|
||||
<div className={`${rest.className} cursor-pointer transition-all duration-200 ease-out gradient-border overflow-hidden rounded-xl backdrop-blur-md bg-container/40`}>
|
||||
<div className={glassSurface(rest.className, 'cursor-pointer transition-[background-color,box-shadow,opacity] duration-200 ease-out rounded-xl outline-none [-webkit-tap-highlight-color:transparent]')}>
|
||||
<div
|
||||
{...rest}
|
||||
className="rounded-normal w-[88px] h-[88px] flex flex-col justify-center items-center gap-2"
|
||||
className="rounded-normal w-[108px] h-[88px] flex flex-col justify-center items-center gap-2 overflow-hidden"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
@@ -9,7 +10,7 @@ type Props = {
|
||||
|
||||
function CategorySmallItemRenderer({ children, compact = false, className, ...rest }: Props) {
|
||||
return (
|
||||
<div className={`${className} cursor-pointer transition-all duration-200 ease-out gradient-border overflow-hidden rounded-xl backdrop-blur-md bg-container/40 dark:bg-background`}>
|
||||
<div className={glassSurface(className, 'cursor-pointer transition-[background-color,box-shadow,opacity] duration-200 ease-out rounded-xl outline-none [-webkit-tap-highlight-color:transparent]')}>
|
||||
<div
|
||||
{...rest}
|
||||
className={
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
@@ -12,18 +13,20 @@ function MenuItemRendererComponent({ children, variant = 'list', ...rest }: Prop
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${
|
||||
isGrid
|
||||
? 'p-3 flex flex-col'
|
||||
: 'relative py-4 pb-4! px-4 flex items-stretch overflow-visible'
|
||||
}`}
|
||||
className={glassSurface(
|
||||
rest.className,
|
||||
`transition-all duration-200 ease-out w-full rounded-3xl ${
|
||||
isGrid
|
||||
? 'p-3 flex flex-col'
|
||||
: 'relative py-4 pb-4! px-4 flex items-stretch overflow-visible'
|
||||
}`,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Memoize with shallow comparison of props
|
||||
const MenuItemRenderer = React.memo(MenuItemRendererComponent, (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.className === nextProps.className &&
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||
import Image from 'next/image';
|
||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { toast } from '../Toast';
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
|
||||
type MenuItemType = {
|
||||
href: string | undefined;
|
||||
@@ -324,13 +325,13 @@ function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeS
|
||||
data-visible={menuState}
|
||||
data-isvisible={menuStateMemo}
|
||||
onClick={(e) => { e.stopPropagation(); }}
|
||||
className="fixed top-0 bottom-0 right-0 bg-container w-[200px] h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!"
|
||||
className={glassSurface("fixed top-0 bottom-0 right-0 w-[200px] h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!")}
|
||||
>
|
||||
{renderMenu()}
|
||||
</motion.nav>
|
||||
</BlurredOverlayContainer>
|
||||
<nav
|
||||
className="hidden fixed top-4 bottom-4 right-4 bg-container xl:w-[250px] xl:flex flex-col z-40 overflow-clip xl:rounded-4xl"
|
||||
className={glassSurface('hidden fixed top-4 bottom-4 right-4 xl:w-[250px] xl:flex flex-col z-40 overflow-clip xl:rounded-4xl')}
|
||||
>
|
||||
{renderMenu()}
|
||||
</nav>
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import BottomNavLink from './BottomNavLink'
|
||||
import BottomNavHighlightLink from './BottomNavLinkBig'
|
||||
import BagIcon from '../icons/BagIcon'
|
||||
import HeartIcon from '../icons/HeartIcon'
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation'
|
||||
import HomeIcon from '../icons/HomeIcon'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart';
|
||||
import { Building, Receipt21 } from 'iconsax-react';
|
||||
import clsx from 'clsx';
|
||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { glassSurfaceNav } from '@/lib/styles/glassSurface';
|
||||
import clsx from 'clsx';
|
||||
import type { Icon } from 'iconsax-react';
|
||||
import { Bag2, Building, Heart, Home2, Receipt21 } from 'iconsax-react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from '../Toast';
|
||||
|
||||
function BottomNavBar() {
|
||||
type NavItemProps = {
|
||||
href?: string;
|
||||
isActive: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
Icon: Icon;
|
||||
badge?: number;
|
||||
};
|
||||
|
||||
function NavItem({ href, isActive, onClick, Icon, badge }: NavItemProps) {
|
||||
const className = clsx('flex-1 flex items-center justify-center h-full rounded-full', isActive && 'bg-primary/12');
|
||||
|
||||
const icon = (
|
||||
<span className="relative">
|
||||
<Icon size={20} color="currentColor" variant={isActive ? 'Bold' : 'Linear'} className={isActive ? 'text-primary' : 'text-icon-deactive'} />
|
||||
{badge != null && badge > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1 flex min-w-3.5 h-3.5 items-center justify-center rounded-full bg-primary px-0.5 text-[8px] font-medium text-container dark:bg-white dark:text-black">
|
||||
{badge > 99 ? '99+' : badge}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Link href={href ?? '#'} onClick={onClick} className={className}>
|
||||
{icon}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function BottomNavBar() {
|
||||
const params = useParams();
|
||||
const { name } = params;
|
||||
const name = params.name as string;
|
||||
const pathname = usePathname();
|
||||
const isHomeRoute = pathname === `/${name}`;
|
||||
const isAboutRoute = pathname.includes('about');
|
||||
const isOrderRoute = pathname.includes('order');
|
||||
const buildingVariant = React.useMemo(() => {
|
||||
return (isAboutRoute ? 'Bold' : 'Outline') as 'Bold' | 'Outline';
|
||||
}, [isAboutRoute]);
|
||||
const orderVariant = React.useMemo(() => {
|
||||
return (isOrderRoute ? 'Bold' : 'Outline') as 'Bold' | 'Outline';
|
||||
}, [isOrderRoute]);
|
||||
const { isSuccess } = useGetProfile()
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common', {
|
||||
keyPrefix: 'BottomNavbar'
|
||||
});
|
||||
const router = useRouter();
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { items } = useCart();
|
||||
|
||||
const cartItemsCount = React.useMemo(() => {
|
||||
return Object.values(items).reduce((total, item) => {
|
||||
return total + (item?.quantity || 0);
|
||||
}, 0);
|
||||
}, [items]);
|
||||
const cartItemCount = useMemo(
|
||||
() => Object.values(items).reduce((sum, item) => sum + (item?.quantity ?? 0), 0),
|
||||
[items],
|
||||
);
|
||||
|
||||
const isCartActive = pathname.startsWith(`/${name}/cart`);
|
||||
const isOrderActive = pathname.includes('/order');
|
||||
const isHomeActive = pathname === `/${name}`;
|
||||
const isAboutActive = pathname.startsWith(`/${name}/about`);
|
||||
const isFavoriteActive = pathname.startsWith(`/${name}/favorite`);
|
||||
|
||||
const handleFavoritesClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -51,156 +68,17 @@ function BottomNavBar() {
|
||||
toast('ابتدا لاگین کنید', 'error');
|
||||
router.replace(`/${name}/auth`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 w-full bg-transparent pointer-events-none">
|
||||
<div className="max-w-md mx-auto w-full aspect-436/104 relative z-30">
|
||||
<svg
|
||||
viewBox="0 0 436 104"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-full block"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<g filter="url(#filter0_d_9095_18036)" className='fill-container'>
|
||||
<path
|
||||
className='pointer-events-auto'
|
||||
d="M218 16C229.034 16 238.711 22.0313 244.148 31.0943C245.844 33.9222 248.724 36 252.022 36H406C414.837 36 422 43.1634 422 52V80C422 88.8366 414.837 96 406 96H30C21.1634 96 14 88.8366 14 80V52C14 43.1634 21.1634 36 30 36H183.979C187.277 36 190.157 33.9222 191.853 31.0943C197.29 22.0315 206.966 16.0001 218 16Z"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* 🔽 Your HTML content inside SVG */}
|
||||
<foreignObject x="0" y="10" width="100%" height="80">
|
||||
<nav
|
||||
className="h-full px-4 grid grid-cols-5 gap-x-1 text-[10px] items-end"
|
||||
>
|
||||
<BottomNavLink
|
||||
href={`/${name}/cart`}
|
||||
icon={<BagIcon width={20} height={20} />}
|
||||
value={t('Cart')}
|
||||
/>
|
||||
|
||||
<Link
|
||||
href={`/${name}/order/history`}
|
||||
className={clsx(
|
||||
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary min-w-0 dark:text-white dark:**:stroke-white'
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 text-primary dark:text-white">
|
||||
<Receipt21
|
||||
key={`receipt-${orderVariant}-${isOrderRoute}`}
|
||||
size={20}
|
||||
variant={orderVariant}
|
||||
color="currentColor"
|
||||
className='stroke-0'
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs2 text-center truncate w-full dark:text-white">
|
||||
{t('Orders')}
|
||||
</span>
|
||||
</Link>
|
||||
{/* <BottomNavLink href={`/${name}/order/history`} icon={<ReceiptIcon width={20} height={20} />} value={t('Orders')} /> */}
|
||||
<BottomNavHighlightLink
|
||||
href={`/${name}`}
|
||||
icon={
|
||||
<HomeIcon
|
||||
className={clsx(
|
||||
'transition-all duration-200 stroke-primary dark:stroke-black! dark:text-white'
|
||||
)}
|
||||
fill="white"
|
||||
stroke="currentColor"
|
||||
variant={isHomeRoute ? 'Bold' : 'Outline'}
|
||||
width={20}
|
||||
height={20} />}
|
||||
value={t('Menu')} />
|
||||
<Link
|
||||
href={`/${name}/about`}
|
||||
className={clsx(
|
||||
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary min-w-0 dark:text-white dark:**:stroke-white'
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 text-primary dark:text-white">
|
||||
<Building
|
||||
key={`building-${buildingVariant}-${isAboutRoute}`}
|
||||
size={20}
|
||||
variant={buildingVariant as 'Bold' | 'Outline'}
|
||||
color="currentColor"
|
||||
className='stroke-0'
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs2 text-center truncate w-full dark:text-white">
|
||||
{t('about')}
|
||||
</span>
|
||||
</Link>
|
||||
<BottomNavLink href={`/${name}/favorite`} onClick={handleFavoritesClick} icon={<HeartIcon width={20} height={20} />} value={t('Favorites')} />
|
||||
</nav>
|
||||
</foreignObject>
|
||||
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_9095_18036"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="-2" />
|
||||
<feGaussianBlur stdDeviation="7" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_9095_18036"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_9095_18036"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* Badge Layer - خارج از SVG - دقیقاً مثل foreignObject */}
|
||||
{cartItemsCount > 0 && (
|
||||
<div
|
||||
className="absolute left-0 right-0 pointer-events-none"
|
||||
style={{
|
||||
top: 'calc(10 / 104 * 100%)',
|
||||
height: 'calc(80 / 104 * 100%)'
|
||||
}}
|
||||
>
|
||||
<div className="h-full px-4 grid grid-cols-5 gap-x-1 items-end">
|
||||
<div className="flex flex-col items-center gap-[5px]">
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-5 h-5 opacity-0"></div>
|
||||
<span className="absolute sm:-top-1 sm:-right-1 top-0 -right-1 flex items-center justify-center min-w-[14px] min-h-[14px] px-1 text-[8px] font-medium text-white dark:bg-white dark:text-black bg-primary rounded-full z-50">
|
||||
{cartItemsCount > 99 ? '99+' : cartItemsCount}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs2 opacity-0">{t('Cart')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={glassSurfaceNav('mx-auto w-full rounded-full flex items-center h-14 max-w-md gap-2.5')}>
|
||||
<NavItem href={`/${name}/cart`} isActive={isCartActive} Icon={Bag2} badge={cartItemCount} />
|
||||
<NavItem href={`/${name}/order/history`} isActive={isOrderActive} Icon={Receipt21} />
|
||||
<NavItem href={`/${name}`} isActive={isHomeActive} Icon={Home2} />
|
||||
<NavItem href={`/${name}/about`} isActive={isAboutActive} Icon={Building} />
|
||||
<NavItem href={`/${name}/favorite`} isActive={isFavoriteActive} onClick={handleFavoritesClick} Icon={Heart} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default BottomNavBar
|
||||
export default BottomNavBar;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { glassSurfaceNav } from "@/lib/styles/glassSurface";
|
||||
import React, { ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
import HorizontalScrollView from '../listview/HorizontalScrollView'
|
||||
import { TabHeader, TabItemProps } from './TabHeader';
|
||||
@@ -38,7 +39,7 @@ export type TabContainerClassName = {
|
||||
|
||||
export const TabContainerClassNames: TabContainerClassName = {
|
||||
wrapper: 'h-full',
|
||||
scrollView: 'h-[72px] gradient-border !pt-3 !pb-2 w-full overflow-y-hidden bg-[#FFFFFF70] dark:bg-container/70 justify-center rounded-[32px] inline-flex items-center gap-10',
|
||||
scrollView: glassSurfaceNav('h-[72px] gradient-border !pt-3 !pb-2 w-full overflow-y-hidden justify-center rounded-[32px] inline-flex items-center gap-10 !shadow-none'),
|
||||
header: '',
|
||||
headerDeactive: '',
|
||||
headerActive: '',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import TopBar from '../topbar/TopBar'
|
||||
import SideMenu from '../menu/SideMenu'
|
||||
import BottomNavBar from '../navigation/BottomNavBar'
|
||||
import useToggle from '@/hooks/helpers/useToggle';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import usePreference from '@/hooks/helpers/usePreference';
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Props = {} & React.AllHTMLAttributes<HTMLBodyElementEventMap>
|
||||
|
||||
@@ -15,7 +17,6 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
const { state: menuState, toggle: _toggleMenuState, set: setMenuState } = useToggle(false);
|
||||
const { state: searchModal, toggle: _toggleSearchModal } = useToggle(false);
|
||||
const { state: nightMode, toggle: _toggleNightMode } = usePreference<boolean>('night-mode', false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const toggleProfileDrop = () => {
|
||||
@@ -35,6 +36,12 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
}
|
||||
|
||||
const location = usePathname();
|
||||
const hideBottomNav = pathname.endsWith('/cart');
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const navContainerRef = useRef<HTMLDivElement>(null);
|
||||
const targetProgressRef = useRef(0);
|
||||
const currentProgressRef = useRef(0);
|
||||
const rafIdRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuState) {
|
||||
@@ -43,10 +50,54 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollEl = mainRef.current;
|
||||
const navEl = navContainerRef.current;
|
||||
|
||||
if (!navEl) return;
|
||||
|
||||
if (!scrollEl || hideBottomNav) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
navEl.style.transform = '';
|
||||
targetProgressRef.current = 0;
|
||||
currentProgressRef.current = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const SCROLL_RANGE = 150;
|
||||
const LERP_FACTOR = 0.1;
|
||||
const MIN_SCALE = 0.82;
|
||||
|
||||
const tick = () => {
|
||||
const diff = targetProgressRef.current - currentProgressRef.current;
|
||||
if (Math.abs(diff) < 0.0005) {
|
||||
currentProgressRef.current = targetProgressRef.current;
|
||||
} else {
|
||||
currentProgressRef.current += diff * LERP_FACTOR;
|
||||
rafIdRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
const scale = 1 - (1 - MIN_SCALE) * currentProgressRef.current;
|
||||
navEl.style.transform = `scale(${scale.toFixed(4)})`;
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
targetProgressRef.current = Math.min(1, Math.max(0, scrollEl.scrollTop / SCROLL_RANGE));
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
rafIdRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
scrollEl.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
scrollEl.removeEventListener('scroll', onScroll);
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
};
|
||||
}, [hideBottomNav, location]);
|
||||
|
||||
return (
|
||||
<div className="h-svh overflow-hidden pt-10 flex flex-col pb-4 xl:pt-12">
|
||||
|
||||
<div className="z-50 fixed right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-4 bg-container justify-between rounded-[32px] xl:w-[calc(100%-305px)]">
|
||||
<div data-menu-topbar className={glassSurface('z-50 fixed right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-4 justify-between rounded-[32px] xl:w-[calc(100%-305px)]')}>
|
||||
<TopBar
|
||||
profileDropState={profileDrop}
|
||||
toggleProfileDropState={toggleProfileDrop}
|
||||
@@ -59,11 +110,16 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 z-45 px-4">
|
||||
<BottomNavBar />
|
||||
</div>
|
||||
|
||||
{!hideBottomNav && (
|
||||
<div
|
||||
ref={navContainerRef}
|
||||
data-menu-bottom-nav
|
||||
className="fixed bottom-7 left-0 right-0 z-45 px-4"
|
||||
style={{ willChange: 'transform', transformOrigin: 'center bottom' }}
|
||||
>
|
||||
<BottomNavBar />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SideMenu
|
||||
nightModeState={nightMode}
|
||||
@@ -72,15 +128,14 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
toggleMenuState={toggleMenuState}
|
||||
/>
|
||||
|
||||
|
||||
<main
|
||||
className="noscrollbar overflow-y-auto h-svh pt-6 xl:pt-8 pb-12 px-4 xl:pr-0 xl:pl-5 w-full xl:w-[calc(100%-285px)] md:self-end"
|
||||
ref={mainRef}
|
||||
className={clsx('relative noscrollbar overflow-y-auto h-svh pt-6 xl:pt-8 px-4 xl:pr-0 xl:pl-5 w-full xl:w-[calc(100%-285px)] md:self-end', hideBottomNav ? 'pb-4' : 'pb-12')}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default ClientMenuRouteWrapper
|
||||
export default ClientMenuRouteWrapper
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
|
||||
import usePreference from '@/hooks/helpers/usePreference';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||
import SplashScreen from '@/components/overlays/SplashScreen';
|
||||
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
|
||||
import {
|
||||
applyCachedThemeToDom,
|
||||
buildThemeCacheFromRestaurant,
|
||||
clearRestaurantBackgroundOverrides,
|
||||
getCachedMenuColor,
|
||||
getThemeCache,
|
||||
setThemeCache,
|
||||
} from '@/lib/helpers/themeCache';
|
||||
|
||||
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);
|
||||
@@ -25,10 +33,12 @@ function applyPrimaryColor(colorHex: string) {
|
||||
}
|
||||
|
||||
function PreferenceWrapper({ children }: Props) {
|
||||
const params = useParams();
|
||||
const restaurantSlug = typeof params.name === 'string' ? params.name : '';
|
||||
|
||||
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[];
|
||||
@@ -39,50 +49,75 @@ function PreferenceWrapper({ children }: Props) {
|
||||
const [cachedLogo, setCachedLogo] = useState<string | null>(null);
|
||||
const [cachedName, setCachedName] = useState<string | null>(null);
|
||||
|
||||
// بارگذاری رنگ و اطلاعات از localStorage در اولین رندر
|
||||
useEffect(() => {
|
||||
// بارگذاری رنگ
|
||||
const savedColor = localStorage.getItem('theme-primary-color');
|
||||
if (savedColor) {
|
||||
const savedColor = getCachedMenuColor(restaurantSlug);
|
||||
const cachedTheme = restaurantSlug ? getThemeCache(restaurantSlug) : null;
|
||||
if (cachedTheme) {
|
||||
applyCachedThemeToDom(cachedTheme);
|
||||
} else if (savedColor) {
|
||||
applyPrimaryColor(savedColor);
|
||||
}
|
||||
|
||||
// بارگذاری لوگو و نام
|
||||
const savedLogo = localStorage.getItem('shop-logo');
|
||||
const savedName = localStorage.getItem('shop-name');
|
||||
if (savedLogo) setCachedLogo(savedLogo);
|
||||
if (savedName) setCachedName(savedName);
|
||||
|
||||
// ثبت اینکه app لود شده
|
||||
sessionStorage.setItem('app-loaded', 'true');
|
||||
}, []);
|
||||
}, [restaurantSlug]);
|
||||
|
||||
// اعمال رنگ primary از about (با ذخیره در localStorage)
|
||||
useEffect(() => {
|
||||
if (aboutData?.data?.menuColor) {
|
||||
// ذخیره رنگ و اطلاعات در localStorage
|
||||
localStorage.setItem('theme-primary-color', aboutData.data.menuColor);
|
||||
const menuColor = aboutData?.data?.menuColor;
|
||||
if (!menuColor || !restaurantSlug) return;
|
||||
|
||||
// ذخیره لوگو و نام برای اسپلش
|
||||
if (aboutData.data.logo) {
|
||||
localStorage.setItem('shop-logo', aboutData.data.logo);
|
||||
}
|
||||
if (aboutData.data.name) {
|
||||
localStorage.setItem('shop-name', aboutData.data.name);
|
||||
}
|
||||
const restaurant = aboutData.data;
|
||||
const cached = getThemeCache(restaurantSlug);
|
||||
const themeBase = buildThemeCacheFromRestaurant(restaurant);
|
||||
|
||||
applyPrimaryColor(aboutData.data.menuColor);
|
||||
setThemeCache(restaurantSlug, {
|
||||
menuColor,
|
||||
bgType: themeBase.bgType,
|
||||
bgUrl: restaurant.bgUrl,
|
||||
bgBlur: themeBase.bgBlur,
|
||||
bgOpacity: themeBase.bgOpacity,
|
||||
bgOverlay: themeBase.bgOverlay,
|
||||
isPattern: themeBase.isPattern,
|
||||
patternDataUrl: cached?.patternDataUrl ?? null,
|
||||
});
|
||||
|
||||
const updatedTheme = getThemeCache(restaurantSlug);
|
||||
if (updatedTheme) {
|
||||
applyCachedThemeToDom(updatedTheme);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [aboutData?.data?.menuColor]);
|
||||
|
||||
// اعمال تم تاریک/روشن
|
||||
localStorage.setItem('theme-primary-color', menuColor);
|
||||
|
||||
if (restaurant.logo) {
|
||||
localStorage.setItem('shop-logo', restaurant.logo);
|
||||
}
|
||||
if (restaurant.name) {
|
||||
localStorage.setItem('shop-name', restaurant.name);
|
||||
}
|
||||
|
||||
applyPrimaryColor(menuColor);
|
||||
}, [aboutData?.data, restaurantSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = nightMode;
|
||||
document.documentElement.setAttribute("data-theme", theme ? "dark" : "light");
|
||||
}, [nightMode]);
|
||||
document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
|
||||
|
||||
if (nightMode) {
|
||||
clearRestaurantBackgroundOverrides();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!restaurantSlug) return;
|
||||
|
||||
const cached = getThemeCache(restaurantSlug);
|
||||
if (cached) {
|
||||
applyCachedThemeToDom(cached);
|
||||
}
|
||||
}, [nightMode, restaurantSlug]);
|
||||
|
||||
// بستن اسپلش بعد از مدت زمان مشخص
|
||||
useEffect(() => {
|
||||
if (showSplash) {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -93,7 +128,6 @@ function PreferenceWrapper({ children }: Props) {
|
||||
}
|
||||
}, [showSplash]);
|
||||
|
||||
// استفاده از دادههای cache شده برای اسپلش
|
||||
const splashLogo = aboutData?.data?.logo || cachedLogo || undefined;
|
||||
const splashName = aboutData?.data?.name || cachedName || undefined;
|
||||
|
||||
@@ -111,4 +145,4 @@ function PreferenceWrapper({ children }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default PreferenceWrapper
|
||||
export default PreferenceWrapper
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
// import LoadingOverlay from '@/components/overlays/LoadingOverlay';
|
||||
import React from 'react'
|
||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||
|
||||
type Props = {
|
||||
isPending: boolean,
|
||||
@@ -24,7 +25,7 @@ function AuthFormWrapper({ children, isPending, onSubmit, ...restProps }: Props)
|
||||
onSubmit={handleSubmit}
|
||||
className='p-6 lg:py-[75px] w-full flex items-center justify-center py-4 lg:items-center lg:px-10 px-4 drop-shadow-black h-full'
|
||||
>
|
||||
<div className='relative h-full w-full px-4 sm:px-6 lg:p-0 flex flex-col max-h-[812px] max-w-[1200px] bg-container rounded-container overflow-clip lg:flex-row justify-between'>
|
||||
<div className={glassSurfaceCard('relative h-full w-full px-4 sm:px-6 lg:p-0 flex flex-col max-h-[812px] max-w-[1200px] overflow-clip lg:flex-row justify-between')}>
|
||||
{children}
|
||||
{/* <LoadingOverlay visible={isPending} bgOpacity={0} /> */}
|
||||
</div>
|
||||
@@ -32,4 +33,4 @@ function AuthFormWrapper({ children, isPending, onSubmit, ...restProps }: Props)
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthFormWrapper
|
||||
export default AuthFormWrapper
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import { useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
getRestaurantSlugFromPath,
|
||||
restorePersistedRestaurantQueries,
|
||||
} from "@/lib/helpers/themeCache";
|
||||
|
||||
export function ReactQueryProvider({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient())
|
||||
function createQueryClient() {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
retryOnMount: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const slug = getRestaurantSlugFromPath();
|
||||
if (slug) {
|
||||
restorePersistedRestaurantQueries(client, slug);
|
||||
}
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export function ReactQueryProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [queryClient] = useState(createQueryClient);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
export const PATTERN_BACKGROUND_OPACITY = 0.05;
|
||||
export const PATTERN_VECTOR_OPACITY = 0.1;
|
||||
export const PRIMARY_LIGHT_OPACITY = 0.15;
|
||||
|
||||
export type BgType = "pattern" | "custom" | "color";
|
||||
|
||||
export type RestaurantBackgroundSettings = {
|
||||
bgType?: BgType | string | null;
|
||||
bgUrl?: string | null;
|
||||
bgBlur?: number | string | null;
|
||||
bgOpacity?: number | string | null;
|
||||
bgOverlay?: string | null;
|
||||
menuColor?: string | null;
|
||||
isPattern?: boolean;
|
||||
};
|
||||
|
||||
export const normalizeBgNumber = (value: unknown): number | null => {
|
||||
if (value == null || value === "") return null;
|
||||
const parsed = typeof value === "string" ? Number(value) : value;
|
||||
if (typeof parsed !== "number" || Number.isNaN(parsed)) return null;
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const hexToRgba = (hex: string, alpha: number): string => {
|
||||
const normalized = hex.replace("#", "");
|
||||
if (normalized.length !== 6) {
|
||||
return `rgba(0, 0, 0, ${alpha})`;
|
||||
}
|
||||
|
||||
const r = parseInt(normalized.slice(0, 2), 16);
|
||||
const g = parseInt(normalized.slice(2, 4), 16);
|
||||
const b = parseInt(normalized.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
const isUnsetBgValue = (value: unknown): boolean =>
|
||||
value == null || value === "" || value === 0;
|
||||
|
||||
const isCustomImageBackgroundLegacy = (
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): boolean => {
|
||||
if (!settings.bgUrl) return false;
|
||||
|
||||
return (
|
||||
!isUnsetBgValue(settings.bgBlur) ||
|
||||
!isUnsetBgValue(settings.bgOpacity) ||
|
||||
!isUnsetBgValue(settings.bgOverlay)
|
||||
);
|
||||
};
|
||||
|
||||
export const resolveBgType = (
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): BgType => {
|
||||
if (settings.bgType === null) return "color";
|
||||
|
||||
const normalized = settings.bgType?.toLowerCase();
|
||||
if (
|
||||
normalized === "pattern" ||
|
||||
normalized === "custom" ||
|
||||
normalized === "color"
|
||||
) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (settings.isPattern === true) return "pattern";
|
||||
if (settings.isPattern === false && settings.bgUrl && isCustomImageBackgroundLegacy(settings)) {
|
||||
return "custom";
|
||||
}
|
||||
if (settings.isPattern === false) return "color";
|
||||
if (isCustomImageBackgroundLegacy(settings)) return "custom";
|
||||
if (settings.bgUrl) return "pattern";
|
||||
|
||||
return "color";
|
||||
};
|
||||
|
||||
export const isCustomImageBackground = (
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): boolean => resolveBgType(settings) === "custom";
|
||||
|
||||
export const isPatternBackground = (
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): boolean => resolveBgType(settings) === "pattern";
|
||||
|
||||
export const isColorBackground = (
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): boolean => resolveBgType(settings) === "color";
|
||||
|
||||
export const colorizeSvg = (
|
||||
svg: string,
|
||||
color: string,
|
||||
vectorOpacity: number = PATTERN_VECTOR_OPACITY,
|
||||
): string => {
|
||||
let result = svg.replace(/fill="(?!black|none)[^"]+"/g, `fill="${color}"`);
|
||||
result = result.replace(
|
||||
/fill-opacity="[^"]+"/g,
|
||||
`fill-opacity="${vectorOpacity}"`,
|
||||
);
|
||||
result = result.replace(
|
||||
/(<(?:path|circle|rect|ellipse|polygon)[^>]*fill="(?!black|none)[^"]+")(?![^>]*fill-opacity)/g,
|
||||
`$1 fill-opacity="${vectorOpacity}"`,
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const applyColorBackgroundCssVariables = (menuColor: string) => {
|
||||
const root = document.documentElement;
|
||||
root.removeAttribute("data-pattern-bg");
|
||||
root.removeAttribute("data-image-bg");
|
||||
root.style.removeProperty("--bg-pattern-base");
|
||||
root.style.removeProperty("--bg-pattern-vector");
|
||||
root.style.setProperty(
|
||||
"--background",
|
||||
hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY),
|
||||
);
|
||||
root.style.setProperty(
|
||||
"--primary-light",
|
||||
hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
|
||||
);
|
||||
};
|
||||
|
||||
export const applyPatternCssVariables = (menuColor: string) => {
|
||||
const root = document.documentElement;
|
||||
const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY);
|
||||
|
||||
root.setAttribute("data-pattern-bg", "true");
|
||||
root.style.setProperty("--bg-pattern-base", baseTint);
|
||||
root.style.setProperty(
|
||||
"--bg-pattern-vector",
|
||||
hexToRgba(menuColor, PATTERN_VECTOR_OPACITY),
|
||||
);
|
||||
root.style.setProperty(
|
||||
"--primary-light",
|
||||
hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
|
||||
);
|
||||
root.style.setProperty("--background", baseTint);
|
||||
};
|
||||
|
||||
export const clearPatternCssVariables = () => {
|
||||
const root = document.documentElement;
|
||||
root.removeAttribute("data-pattern-bg");
|
||||
root.style.removeProperty("--bg-pattern-base");
|
||||
root.style.removeProperty("--bg-pattern-vector");
|
||||
root.style.removeProperty("--primary-light");
|
||||
root.style.removeProperty("--background");
|
||||
};
|
||||
|
||||
export const applyCustomImageCssVariables = () => {
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-image-bg", "true");
|
||||
root.removeAttribute("data-pattern-bg");
|
||||
};
|
||||
|
||||
export const clearCustomImageCssVariables = () => {
|
||||
document.documentElement.removeAttribute("data-image-bg");
|
||||
};
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getThemeBootScript(): string {
|
||||
return `(function(){try{function isNight(){try{var p=localStorage.getItem("preference-storage");if(!p)return false;var o=JSON.parse(p);return o&&o.state&&o.state.items&&o.state.items["night-mode"]&&o.state.items["night-mode"].value===true}catch(v){return false}}var night=isNight(),e=document.documentElement;e.setAttribute("data-theme",night?"dark":"light");var s=location.pathname.split("/").filter(Boolean)[0],t=null;if(s&&s!=="auth"){var r=localStorage.getItem("restaurant-theme:"+s);if(r)t=JSON.parse(r)}if(!t||t.version!==1||!t.menuColor){var l=localStorage.getItem("theme-primary-color");if(l&&!night){var x=l.replace("#","");if(x.length===6){var p=parseInt(x.slice(0,2),16),f=parseInt(x.slice(2,4),16),m=parseInt(x.slice(4,6),16),b="rgba("+p+","+f+","+m+",0.05)";e.style.setProperty("--background",b)}}return}function h(n,a){var u=n.replace("#","");if(u.length!==6)return null;var v=parseInt(u.slice(0,2),16),g=parseInt(u.slice(2,4),16),y=parseInt(u.slice(4,6),16);return"rgba("+v+","+g+","+y+","+a+")"}function isUnset(v){return v==null||v===""||v===0}function isCustomImg(th){if(!th.bgUrl)return false;return !isUnset(th.bgBlur)||!isUnset(th.bgOpacity)||!isUnset(th.bgOverlay)}function resolveBgType(th){if(th.bgType===null)return"color";var bt=(th.bgType||"").toLowerCase();if(bt==="pattern"||bt==="custom"||bt==="color")return bt;if(th.isPattern===true)return"pattern";if(th.isPattern===false&&th.bgUrl&&isCustomImg(th))return"custom";if(th.isPattern===false)return"color";if(isCustomImg(th))return"custom";if(th.bgUrl)return"pattern";return"color"}if(t.primaryOklch){e.style.setProperty("--primary",t.primaryOklch);e.style.setProperty("--primary-foreground",t.primaryForeground||"oklch(1 0 0)")}if(night)return;var bt=resolveBgType(t);if(bt==="color"){var k=t.backgroundTint||h(t.menuColor,0.05),z=t.primaryLight||h(t.menuColor,0.15);k&&e.style.setProperty("--background",k);z&&e.style.setProperty("--primary-light",z);return};if(bt==="pattern"){e.setAttribute("data-pattern-bg","true");var k=t.backgroundTint||h(t.menuColor,0.05),w=t.bgPatternVector||h(t.menuColor,0.1),z=t.primaryLight||h(t.menuColor,0.15);k&&(e.style.setProperty("--background",k),e.style.setProperty("--bg-pattern-base",k));w&&e.style.setProperty("--bg-pattern-vector",w);z&&e.style.setProperty("--primary-light",z);if(k){var d=document.createElement("div");d.id="cached-theme-bg";d.setAttribute("aria-hidden","true");d.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;background-color:"+k+(t.patternDataUrl?';background-image:url("'+t.patternDataUrl+'");background-repeat:repeat;background-size:auto':"")+";";document.body.appendChild(d)}}else if(bt==="custom"){e.setAttribute("data-image-bg","true");var d2=document.createElement("div");d2.id="cached-theme-bg";d2.setAttribute("aria-hidden","true");d2.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;";var i=document.createElement("div");i.id="cached-theme-bg-img";i.style.cssText="position:absolute;inset:-40px;background-size:cover;background-position:center;background-repeat:no-repeat;background-image:url(\\""+t.bgUrl+"\\");"+(t.bgBlur?"filter:blur("+t.bgBlur+"px);":"")+"opacity:"+(t.bgOpacity!=null?t.bgOpacity/100:1)+";";d2.appendChild(i);var ov=document.createElement("div");ov.id="cached-theme-bg-overlay";ov.style.cssText="position:absolute;inset:0;"+(t.bgOverlay&&Number(t.bgOverlay)>0?"background-color:rgba(0,0,0,"+Number(t.bgOverlay)/100+");":"");d2.appendChild(ov);document.body.appendChild(d2)}}catch(u){}})();`;
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
import { calculateBrightness, hexToOklch } from "@/lib/helpers/colorUtils";
|
||||
import {
|
||||
hexToRgba,
|
||||
resolveBgType,
|
||||
normalizeBgNumber,
|
||||
PATTERN_BACKGROUND_OPACITY,
|
||||
PATTERN_VECTOR_OPACITY,
|
||||
PRIMARY_LIGHT_OPACITY,
|
||||
type BgType,
|
||||
type RestaurantBackgroundSettings,
|
||||
} from "@/lib/helpers/backgroundUtils";
|
||||
import type { AboutResponse } from "@/app/[name]/(Main)/about/types/Types";
|
||||
import type {
|
||||
CategoriesResponse,
|
||||
ProductsResponse,
|
||||
} from "@/app/[name]/(Main)/types/Types";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
export type CachedRestaurantTheme = {
|
||||
version: 1;
|
||||
menuColor: string;
|
||||
bgType?: BgType;
|
||||
bgUrl?: string | null;
|
||||
bgBlur?: number | null;
|
||||
bgOpacity?: number | null;
|
||||
bgOverlay?: string | null;
|
||||
isPattern: boolean;
|
||||
patternDataUrl?: string | null;
|
||||
primaryOklch?: string;
|
||||
primaryForeground?: string;
|
||||
backgroundTint?: string;
|
||||
bgPatternVector?: string;
|
||||
primaryLight?: string;
|
||||
};
|
||||
|
||||
const CACHE_VERSION = 1;
|
||||
const CACHE_KEY_PREFIX = "restaurant-theme";
|
||||
const LEGACY_COLOR_KEY = "theme-primary-color";
|
||||
const ABOUT_CACHE_PREFIX = "restaurant-about";
|
||||
const MENU_CACHE_PREFIX = "restaurant-menu";
|
||||
const CATEGORIES_CACHE_PREFIX = "restaurant-categories";
|
||||
export const THEME_BG_ELEMENT_ID = "cached-theme-bg";
|
||||
const THEME_BG_IMG_ID = "cached-theme-bg-img";
|
||||
const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay";
|
||||
const PREFERENCE_STORAGE_KEY = "preference-storage";
|
||||
|
||||
export function isNightModeEnabled(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(PREFERENCE_STORAGE_KEY);
|
||||
if (!raw) return false;
|
||||
const parsed = JSON.parse(raw) as {
|
||||
state?: { items?: Record<string, { value?: unknown }> };
|
||||
};
|
||||
return parsed?.state?.items?.["night-mode"]?.value === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearRestaurantBackgroundOverrides(): void {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const root = document.documentElement;
|
||||
root.removeAttribute("data-pattern-bg");
|
||||
root.removeAttribute("data-image-bg");
|
||||
root.style.removeProperty("--background");
|
||||
root.style.removeProperty("--bg-pattern-base");
|
||||
root.style.removeProperty("--bg-pattern-vector");
|
||||
root.style.removeProperty("--primary-light");
|
||||
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||
}
|
||||
|
||||
export function getThemeCacheKey(slug: string): string {
|
||||
return `${CACHE_KEY_PREFIX}:${slug}`;
|
||||
}
|
||||
|
||||
export function getAboutCacheKey(slug: string): string {
|
||||
return `${ABOUT_CACHE_PREFIX}:${slug}`;
|
||||
}
|
||||
|
||||
export function getMenuCacheKey(slug: string): string {
|
||||
return `${MENU_CACHE_PREFIX}:${slug}`;
|
||||
}
|
||||
|
||||
export function getCategoriesCacheKey(slug: string): string {
|
||||
return `${CATEGORIES_CACHE_PREFIX}:${slug}`;
|
||||
}
|
||||
|
||||
export function getRestaurantSlugFromPath(pathname?: string): string | null {
|
||||
const path =
|
||||
pathname ?? (typeof window !== "undefined" ? window.location.pathname : "");
|
||||
const segment = path.split("/").filter(Boolean)[0];
|
||||
if (!segment || segment === "auth") return null;
|
||||
return segment;
|
||||
}
|
||||
|
||||
export function computeThemeCssValues(menuColor: string) {
|
||||
const primaryOklch = hexToOklch(menuColor);
|
||||
const brightness = calculateBrightness(menuColor);
|
||||
|
||||
return {
|
||||
primaryOklch: primaryOklch ?? undefined,
|
||||
primaryForeground: brightness > 0.5 ? "oklch(0 0 0)" : "oklch(1 0 0)",
|
||||
backgroundTint: hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY),
|
||||
bgPatternVector: hexToRgba(menuColor, PATTERN_VECTOR_OPACITY),
|
||||
primaryLight: hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
|
||||
};
|
||||
}
|
||||
|
||||
export function getThemeCache(slug: string): CachedRestaurantTheme | null {
|
||||
if (!slug || typeof window === "undefined") return null;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(getThemeCacheKey(slug));
|
||||
if (!raw) return null;
|
||||
|
||||
const parsed = JSON.parse(raw) as CachedRestaurantTheme;
|
||||
if (parsed.version !== CACHE_VERSION || !parsed.menuColor) return null;
|
||||
|
||||
if (!parsed.primaryOklch) {
|
||||
const enriched: CachedRestaurantTheme = {
|
||||
...parsed,
|
||||
...computeThemeCssValues(parsed.menuColor),
|
||||
version: CACHE_VERSION,
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem(getThemeCacheKey(slug), JSON.stringify(enriched));
|
||||
} catch {
|
||||
// Ignore quota errors during migration
|
||||
}
|
||||
|
||||
return enriched;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedMenuColor(slug?: string | null): string | null {
|
||||
if (slug) {
|
||||
const cached = getThemeCache(slug);
|
||||
if (cached?.menuColor) return cached.menuColor;
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
try {
|
||||
return localStorage.getItem(LEGACY_COLOR_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPersistedAboutData(slug: string): AboutResponse | undefined {
|
||||
if (!slug || typeof window === "undefined") return undefined;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(getAboutCacheKey(slug));
|
||||
if (!raw) return undefined;
|
||||
return JSON.parse(raw) as AboutResponse;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function setPersistedAboutData(slug: string, data: AboutResponse): void {
|
||||
if (!slug || typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(getAboutCacheKey(slug), JSON.stringify(data));
|
||||
} catch {
|
||||
// Ignore quota errors
|
||||
}
|
||||
}
|
||||
|
||||
function readPersistedQueryData<T>(key: string): T | undefined {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) return undefined;
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function writePersistedQueryData<T>(key: string, data: T): void {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
} catch {
|
||||
// Ignore quota errors
|
||||
}
|
||||
}
|
||||
|
||||
export function getPersistedMenuData(slug: string): ProductsResponse | undefined {
|
||||
if (!slug || typeof window === "undefined") return undefined;
|
||||
return readPersistedQueryData<ProductsResponse>(getMenuCacheKey(slug));
|
||||
}
|
||||
|
||||
export function setPersistedMenuData(slug: string, data: ProductsResponse): void {
|
||||
if (!slug || typeof window === "undefined") return;
|
||||
writePersistedQueryData(getMenuCacheKey(slug), data);
|
||||
}
|
||||
|
||||
export function getPersistedCategoriesData(
|
||||
slug: string,
|
||||
): CategoriesResponse | undefined {
|
||||
if (!slug || typeof window === "undefined") return undefined;
|
||||
return readPersistedQueryData<CategoriesResponse>(getCategoriesCacheKey(slug));
|
||||
}
|
||||
|
||||
export function setPersistedCategoriesData(
|
||||
slug: string,
|
||||
data: CategoriesResponse,
|
||||
): void {
|
||||
if (!slug || typeof window === "undefined") return;
|
||||
writePersistedQueryData(getCategoriesCacheKey(slug), data);
|
||||
}
|
||||
|
||||
export function restorePersistedRestaurantQueries(
|
||||
client: QueryClient,
|
||||
slug: string,
|
||||
): void {
|
||||
const about = getPersistedAboutData(slug);
|
||||
if (about) {
|
||||
client.setQueryData(["about", slug], about);
|
||||
}
|
||||
|
||||
const menu = getPersistedMenuData(slug);
|
||||
if (menu) {
|
||||
client.setQueryData(["menu", slug], menu);
|
||||
}
|
||||
|
||||
const categories = getPersistedCategoriesData(slug);
|
||||
if (categories) {
|
||||
client.setQueryData(["categories", slug], categories);
|
||||
}
|
||||
}
|
||||
|
||||
export function setThemeCache(
|
||||
slug: string,
|
||||
theme: Omit<CachedRestaurantTheme, "version">,
|
||||
): void {
|
||||
if (!slug || typeof window === "undefined") return;
|
||||
|
||||
const existing = getThemeCache(slug);
|
||||
const css = computeThemeCssValues(theme.menuColor);
|
||||
const payload: CachedRestaurantTheme = {
|
||||
...theme,
|
||||
...css,
|
||||
patternDataUrl:
|
||||
theme.patternDataUrl !== undefined
|
||||
? theme.patternDataUrl
|
||||
: (existing?.patternDataUrl ?? null),
|
||||
version: CACHE_VERSION,
|
||||
};
|
||||
|
||||
try {
|
||||
localStorage.setItem(getThemeCacheKey(slug), JSON.stringify(payload));
|
||||
localStorage.setItem(LEGACY_COLOR_KEY, theme.menuColor);
|
||||
} catch {
|
||||
try {
|
||||
const { patternDataUrl: _pattern, ...withoutPattern } = payload;
|
||||
localStorage.setItem(
|
||||
getThemeCacheKey(slug),
|
||||
JSON.stringify({ ...withoutPattern, patternDataUrl: null }),
|
||||
);
|
||||
localStorage.setItem(LEGACY_COLOR_KEY, theme.menuColor);
|
||||
} catch {
|
||||
// Ignore quota errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildThemeCacheFromRestaurant(
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): Pick<
|
||||
CachedRestaurantTheme,
|
||||
| "menuColor"
|
||||
| "bgType"
|
||||
| "bgUrl"
|
||||
| "bgBlur"
|
||||
| "bgOpacity"
|
||||
| "bgOverlay"
|
||||
| "isPattern"
|
||||
> {
|
||||
const bgType = resolveBgType(settings);
|
||||
|
||||
return {
|
||||
menuColor: settings.menuColor || "#1E3A8A",
|
||||
bgType,
|
||||
bgUrl: settings.bgUrl,
|
||||
bgBlur: normalizeBgNumber(settings.bgBlur),
|
||||
bgOpacity: normalizeBgNumber(settings.bgOpacity),
|
||||
bgOverlay: settings.bgOverlay ?? null,
|
||||
isPattern: bgType === "pattern",
|
||||
};
|
||||
}
|
||||
|
||||
export function themeBackgroundMatches(
|
||||
cached: CachedRestaurantTheme,
|
||||
settings: RestaurantBackgroundSettings,
|
||||
): boolean {
|
||||
const next = buildThemeCacheFromRestaurant(settings);
|
||||
return (
|
||||
cached.menuColor === next.menuColor &&
|
||||
cached.bgType === next.bgType &&
|
||||
cached.bgUrl === next.bgUrl &&
|
||||
cached.bgBlur === next.bgBlur &&
|
||||
cached.bgOpacity === next.bgOpacity &&
|
||||
cached.bgOverlay === next.bgOverlay &&
|
||||
cached.isPattern === next.isPattern
|
||||
);
|
||||
}
|
||||
|
||||
export function svgToDataUrl(svg: string): string {
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
function ensureBgElement(): HTMLElement {
|
||||
let bgElement = document.getElementById(THEME_BG_ELEMENT_ID);
|
||||
if (!bgElement) {
|
||||
bgElement = document.createElement("div");
|
||||
bgElement.id = THEME_BG_ELEMENT_ID;
|
||||
bgElement.setAttribute("aria-hidden", "true");
|
||||
bgElement.style.position = "fixed";
|
||||
bgElement.style.inset = "0";
|
||||
bgElement.style.pointerEvents = "none";
|
||||
bgElement.style.zIndex = "0";
|
||||
document.body.appendChild(bgElement);
|
||||
}
|
||||
return bgElement;
|
||||
}
|
||||
|
||||
export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
if (theme.primaryOklch) {
|
||||
root.style.setProperty("--primary", theme.primaryOklch);
|
||||
root.style.setProperty(
|
||||
"--primary-foreground",
|
||||
theme.primaryForeground ?? "oklch(1 0 0)",
|
||||
);
|
||||
}
|
||||
|
||||
if (isNightModeEnabled()) {
|
||||
clearRestaurantBackgroundOverrides();
|
||||
return;
|
||||
}
|
||||
|
||||
const bgType = resolveBgType(theme);
|
||||
|
||||
if (bgType === "color") {
|
||||
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||
const tint =
|
||||
theme.backgroundTint ??
|
||||
hexToRgba(theme.menuColor, PATTERN_BACKGROUND_OPACITY);
|
||||
const primaryLight =
|
||||
theme.primaryLight ??
|
||||
hexToRgba(theme.menuColor, PRIMARY_LIGHT_OPACITY);
|
||||
root.removeAttribute("data-pattern-bg");
|
||||
root.removeAttribute("data-image-bg");
|
||||
root.style.removeProperty("--bg-pattern-base");
|
||||
root.style.removeProperty("--bg-pattern-vector");
|
||||
root.style.setProperty("--background", tint);
|
||||
root.style.setProperty("--primary-light", primaryLight);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bgType === "pattern") {
|
||||
root.setAttribute("data-pattern-bg", "true");
|
||||
root.removeAttribute("data-image-bg");
|
||||
|
||||
if (theme.backgroundTint) {
|
||||
root.style.setProperty("--background", theme.backgroundTint);
|
||||
root.style.setProperty("--bg-pattern-base", theme.backgroundTint);
|
||||
}
|
||||
if (theme.bgPatternVector) {
|
||||
root.style.setProperty("--bg-pattern-vector", theme.bgPatternVector);
|
||||
}
|
||||
if (theme.primaryLight) {
|
||||
root.style.setProperty("--primary-light", theme.primaryLight);
|
||||
}
|
||||
|
||||
if (!theme.backgroundTint) return;
|
||||
|
||||
const bgElement = ensureBgElement();
|
||||
bgElement.style.overflow = "";
|
||||
|
||||
document.getElementById(THEME_BG_IMG_ID)?.remove();
|
||||
document.getElementById(THEME_BG_OVERLAY_ID)?.remove();
|
||||
|
||||
bgElement.style.backgroundColor = theme.backgroundTint;
|
||||
if (theme.patternDataUrl) {
|
||||
bgElement.style.backgroundImage = `url("${theme.patternDataUrl}")`;
|
||||
bgElement.style.backgroundRepeat = "repeat";
|
||||
bgElement.style.backgroundSize = "auto";
|
||||
} else {
|
||||
bgElement.style.removeProperty("background-image");
|
||||
bgElement.style.removeProperty("background-repeat");
|
||||
bgElement.style.removeProperty("background-size");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bgType === "custom" && theme.bgUrl) {
|
||||
root.setAttribute("data-image-bg", "true");
|
||||
root.removeAttribute("data-pattern-bg");
|
||||
|
||||
const bgElement = ensureBgElement();
|
||||
bgElement.style.backgroundColor = "";
|
||||
bgElement.style.backgroundImage = "";
|
||||
bgElement.style.overflow = "hidden";
|
||||
|
||||
let imgLayer = document.getElementById(THEME_BG_IMG_ID);
|
||||
if (!imgLayer) {
|
||||
imgLayer = document.createElement("div");
|
||||
imgLayer.id = THEME_BG_IMG_ID;
|
||||
imgLayer.style.position = "absolute";
|
||||
imgLayer.style.inset = "-40px";
|
||||
imgLayer.style.backgroundSize = "cover";
|
||||
imgLayer.style.backgroundPosition = "center";
|
||||
imgLayer.style.backgroundRepeat = "no-repeat";
|
||||
bgElement.appendChild(imgLayer);
|
||||
}
|
||||
|
||||
imgLayer.style.backgroundImage = `url("${theme.bgUrl}")`;
|
||||
imgLayer.style.filter = theme.bgBlur ? `blur(${theme.bgBlur}px)` : "";
|
||||
imgLayer.style.opacity =
|
||||
theme.bgOpacity != null ? String(theme.bgOpacity / 100) : "1";
|
||||
|
||||
let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID);
|
||||
if (!overlayLayer) {
|
||||
overlayLayer = document.createElement("div");
|
||||
overlayLayer.id = THEME_BG_OVERLAY_ID;
|
||||
overlayLayer.style.position = "absolute";
|
||||
overlayLayer.style.inset = "0";
|
||||
bgElement.appendChild(overlayLayer);
|
||||
}
|
||||
|
||||
const overlayOpacity =
|
||||
theme.bgOverlay != null ? Number(theme.bgOverlay) / 100 : 0;
|
||||
overlayLayer.style.backgroundColor =
|
||||
overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : "";
|
||||
}
|
||||
}
|
||||
|
||||
export function removeCachedThemeBackground(): void {
|
||||
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
|
||||
export const GLASS_SURFACE = "glass-surface";
|
||||
export const GLASS_SURFACE_FLAT = "glass-surface--flat";
|
||||
export const GLASS_SURFACE_SELECTED = "glass-surface--selected";
|
||||
export const GLASS_SURFACE_DISABLED = "glass-surface--disabled";
|
||||
export const GLASS_SURFACE_NAV = "glass-surface-nav";
|
||||
|
||||
export function glassSurfaceNav(...extra: ClassValue[]) {
|
||||
return clsx(GLASS_SURFACE_NAV, extra);
|
||||
}
|
||||
|
||||
export function glassSurface(...extra: ClassValue[]) {
|
||||
return clsx(GLASS_SURFACE, extra);
|
||||
}
|
||||
|
||||
export function glassSurfaceFlat(...extra: ClassValue[]) {
|
||||
return clsx(GLASS_SURFACE, GLASS_SURFACE_FLAT, extra);
|
||||
}
|
||||
|
||||
export function glassSurfaceSelected(...extra: ClassValue[]) {
|
||||
return clsx(GLASS_SURFACE, GLASS_SURFACE_SELECTED, extra);
|
||||
}
|
||||
|
||||
/** Page-level card sections (about, cart, profile, etc.) */
|
||||
export function glassSurfaceCard(...extra: ClassValue[]) {
|
||||
return clsx(GLASS_SURFACE, "rounded-container", extra);
|
||||
}
|
||||
|
||||
/** Nested / secondary card sections with no shadow */
|
||||
export function glassSurfaceCardFlat(...extra: ClassValue[]) {
|
||||
return clsx(GLASS_SURFACE, GLASS_SURFACE_FLAT, "rounded-container", extra);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* Glass surface — Figma: white 60%, border, shadow, backdrop blur */
|
||||
|
||||
.glass-surface {
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
backdrop-filter: blur(3px);
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] .glass-surface,
|
||||
html[data-image-bg="true"] .glass-surface {
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .glass-surface {
|
||||
background-color: color-mix(in oklch, var(--container) 60%, transparent);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.glass-surface--flat {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.glass-surface--selected {
|
||||
background-color: rgba(255, 255, 255, 0.85) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .glass-surface--selected {
|
||||
background-color: color-mix(in oklch, var(--container) 85%, transparent) !important;
|
||||
}
|
||||
|
||||
/* Figma disabled category: white 40%, 2px border, blur 12 */
|
||||
.glass-surface--disabled {
|
||||
background-color: rgba(255, 255, 255, 0.4) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
box-shadow: none;
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] .glass-surface--disabled,
|
||||
html[data-image-bg="true"] .glass-surface--disabled {
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .glass-surface--disabled {
|
||||
background-color: color-mix(in oklch, var(--container) 40%, transparent) !important;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.glass-surface-nav {
|
||||
background-color: rgba(255, 255, 255, 0.32);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0px -2px 14px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
html[data-pattern-bg="true"] .glass-surface-nav,
|
||||
html[data-image-bg="true"] .glass-surface-nav {
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .glass-surface-nav {
|
||||
background-color: color-mix(in oklch, var(--container) 32%, transparent);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
Reference in New Issue
Block a user