diff --git a/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx b/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx index 95bba2d..2d024d7 100644 --- a/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx +++ b/src/app/[name]/(Dialogs)/cart/components/CartSummary.tsx @@ -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) => { )} -
+
{t('PayableAmountLabel')}
diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx index ecc2faa..d57ca2a 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/AddressSection.tsx @@ -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'; diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CarAddressSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CarAddressSection.tsx index c791156..48b2d3c 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CarAddressSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CarAddressSection.tsx @@ -1,4 +1,5 @@ 'use client'; +import { glassSurfaceCard } from '@/lib/styles/glassSurface'; import InputField from '@/components/input/InputField'; import { useCheckoutStore } from '../../store/Store'; diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CouponSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CouponSection.tsx index d89ac9d..fea1e0b 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CouponSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/CouponSection.tsx @@ -1,4 +1,5 @@ 'use client'; +import { glassSurfaceCard } from '@/lib/styles/glassSurface'; import InputField from '@/components/input/InputField'; import clsx from 'clsx'; diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/PaymentSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/PaymentSection.tsx index f6c0789..ef8c7f7 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/PaymentSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/PaymentSection.tsx @@ -1,4 +1,5 @@ 'use client'; +import { glassSurfaceCard } from '@/lib/styles/glassSurface'; import { Card, Icon, Wallet2 } from 'iconsax-react'; import { useCallback, useMemo } from 'react'; diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/ShippingSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/ShippingSection.tsx index 16bd6b2..38a046a 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/ShippingSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/ShippingSection.tsx @@ -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'; diff --git a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/TableSection.tsx b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/TableSection.tsx index 93364d9..102d67e 100644 --- a/src/app/[name]/(Dialogs)/order/checkout/[id]/components/TableSection.tsx +++ b/src/app/[name]/(Dialogs)/order/checkout/[id]/components/TableSection.tsx @@ -1,4 +1,5 @@ 'use client'; +import { glassSurfaceCard } from '@/lib/styles/glassSurface'; import { MinusIcon, PlusIcon } from 'lucide-react'; import { motion } from 'framer-motion'; diff --git a/src/app/[name]/(Main)/about/AboutPage.tsx b/src/app/[name]/(Main)/about/AboutPage.tsx index c0f9011..5b6b044 100644 --- a/src/app/[name]/(Main)/about/AboutPage.tsx +++ b/src/app/[name]/(Main)/about/AboutPage.tsx @@ -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 (
+ className={glassSurfaceCard('p-4')}>

{shop.name}

@@ -123,7 +124,7 @@ function AboutPage() { {(shop.phone || shop.telegram || shop.whatsapp || shop.instagram) && (
+ className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center')}>

ارتباط

{shop.phone && @@ -151,7 +152,7 @@ function AboutPage() { )} {shop.address && ( -
+

آدرس

{shop.address}

{shop.latitude && shop.longitude && ( @@ -166,7 +167,7 @@ function AboutPage() { {schedules.length > 0 && getTodaySchedule() && (
+ className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
باز
@@ -254,7 +255,7 @@ function AboutPage() {
+ className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
{averageRating.toFixed(1)}
@@ -267,10 +268,10 @@ function AboutPage() {
-
+

نظرات کاربران

- diff --git a/src/app/[name]/(Main)/about/components/AboutSkeleton.tsx b/src/app/[name]/(Main)/about/components/AboutSkeleton.tsx index 3dc9b6f..038803f 100644 --- a/src/app/[name]/(Main)/about/components/AboutSkeleton.tsx +++ b/src/app/[name]/(Main)/about/components/AboutSkeleton.tsx @@ -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 = () => (
- {/* اطلاعات فروشگاه */} -
+
@@ -30,8 +30,7 @@ const AboutSkeleton = () => {
- {/* ارتباط */} -
+
{[...Array(3)].map((_, index) => ( @@ -40,8 +39,7 @@ const AboutSkeleton = () => {
- {/* آدرس */} -
+
@@ -51,8 +49,7 @@ const AboutSkeleton = () => {
- {/* ساعات کاری امروز */} -
+
@@ -62,11 +59,10 @@ const AboutSkeleton = () => {
- {/* ساعات کاری هفته */} {[...Array(3)].map((_, index) => (
+ className={glassSurfaceCardFlat('leading-5 py-6 px-4 mt-4 flex justify-between items-center')}>
@@ -76,8 +72,7 @@ const AboutSkeleton = () => { const secondTabSkeleton = () => (
- {/* امتیاز کاربران */} -
+
{[...Array(5)].map((_, index) => ( @@ -86,8 +81,7 @@ const AboutSkeleton = () => {
- {/* نظرات کاربران */} -
+
@@ -130,4 +124,3 @@ const AboutSkeleton = () => { }; export default AboutSkeleton; - diff --git a/src/app/[name]/(Main)/about/hooks/useAboutData.ts b/src/app/[name]/(Main)/about/hooks/useAboutData.ts index fc5edfe..997143d 100644 --- a/src/app/[name]/(Main)/about/hooks/useAboutData.ts +++ b/src/app/[name]/(Main)/about/hooks/useAboutData.ts @@ -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, diff --git a/src/app/[name]/(Main)/about/types/Types.ts b/src/app/[name]/(Main)/about/types/Types.ts index 764bed0..aec1d6b 100644 --- a/src/app/[name]/(Main)/about/types/Types.ts +++ b/src/app/[name]/(Main)/about/types/Types.ts @@ -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; diff --git a/src/app/[name]/(Main)/components/CategoryScroll.tsx b/src/app/[name]/(Main)/components/CategoryScroll.tsx index 3bfe486..a32a64f 100644 --- a/src/app/[name]/(Main)/components/CategoryScroll.tsx +++ b/src/app/[name]/(Main)/components/CategoryScroll.tsx @@ -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 (
{ 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 ( - - - {item.title} + + + {item.title} ); })} @@ -120,7 +121,7 @@ const CategoryScroll = ({ categories, selectedCategory, onSelect, variant = "lar const isChildSelected = child.id === selectedCategory; return ( - + {child.avatarUrl && } {child.title} diff --git a/src/app/[name]/(Main)/hooks/useMenuData.ts b/src/app/[name]/(Main)/hooks/useMenuData.ts index add27f1..229a6d6 100644 --- a/src/app/[name]/(Main)/hooks/useMenuData.ts +++ b/src/app/[name]/(Main)/hooks/useMenuData.ts @@ -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 = () => { diff --git a/src/app/[name]/(Main)/page.tsx b/src/app/[name]/(Main)/page.tsx index 550b01f..b5685b4 100644 --- a/src/app/[name]/(Main)/page.tsx +++ b/src/app/[name]/(Main)/page.tsx @@ -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 = () => {
{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}
-
+
- @@ -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")} > diff --git a/src/app/[name]/(Profile)/profile/address/page.tsx b/src/app/[name]/(Profile)/profile/address/page.tsx index f360f93..b1605d1 100644 --- a/src/app/[name]/(Profile)/profile/address/page.tsx +++ b/src/app/[name]/(Profile)/profile/address/page.tsx @@ -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) => (

{address.title}

diff --git a/src/app/[name]/(Profile)/profile/edit/page.tsx b/src/app/[name]/(Profile)/profile/edit/page.tsx index 60c231e..837d841 100644 --- a/src/app/[name]/(Profile)/profile/edit/page.tsx +++ b/src/app/[name]/(Profile)/profile/edit/page.tsx @@ -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) {

ویرایش اطلاعات

-
+
-
+
diff --git a/src/app/[name]/(Profile)/profile/settings/page.tsx b/src/app/[name]/(Profile)/profile/settings/page.tsx index 3cf37f2..a553087 100644 --- a/src/app/[name]/(Profile)/profile/settings/page.tsx +++ b/src/app/[name]/(Profile)/profile/settings/page.tsx @@ -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 (
+ className={glassSurfaceCard('p-4')}>

{data.TabNotifications.Heading}

@@ -94,7 +95,7 @@ function UserSettingsIndex() { return (
+ className={glassSurfaceCard('p-4')}>

{data.TabPassword.Heading}

@@ -154,7 +155,7 @@ function UserSettingsIndex() { return (
+ className={glassSurfaceCard('p-4')}>

{data.TabAuthenticator.Heading}

@@ -236,7 +237,7 @@ function UserSettingsIndex() { />
-
+

{data.StatisticalParticipation.Heading}

+ className={glassSurfaceCard('mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between')}>
diff --git a/src/app/[name]/api/proxy-svg/route.ts b/src/app/[name]/api/proxy-svg/route.ts new file mode 100644 index 0000000..3046cd6 --- /dev/null +++ b/src/app/[name]/api/proxy-svg/route.ts @@ -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(" - {children} - - - + return {children} } catch (error) { if (error instanceof Error && error.message === 'SHOP_NOT_FOUND') { notFound() diff --git a/src/app/globals.css b/src/app/globals.css index fddb75d..866d06a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9325d26..b825ca1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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" > - + + + + + + + {children} + + + + + + ); +} diff --git a/src/components/ThemeBootScript.tsx b/src/components/ThemeBootScript.tsx new file mode 100644 index 0000000..74012c6 --- /dev/null +++ b/src/components/ThemeBootScript.tsx @@ -0,0 +1,10 @@ +import { getThemeBootScript } from "@/lib/helpers/themeBootScript"; + +export function ThemeBootScript() { + return ( +