load pattern

This commit is contained in:
hamid zarghami
2026-06-21 11:08:11 +03:30
parent f6cfa369fc
commit d3ef8e1a98
13 changed files with 222 additions and 12 deletions
@@ -22,7 +22,7 @@ export const TableSection = ({ tableNumber, onIncrement, onDecrement }: TableSec
<div className='w-full flex justify-end'>
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
>
<button
onClick={onIncrement}
+1 -1
View File
@@ -231,7 +231,7 @@ function FoodPage({}: Props) {
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
<motion.div
whileTap={isAvailable ? { scale: 1.05 } : undefined}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
>
{quantity <= 0 ? (
<button
+1 -1
View File
@@ -270,7 +270,7 @@ function AboutPage() {
<section className="bg-container rounded-container shadow-container 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="rounded-xl h-8 bg-container 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>
@@ -175,11 +175,11 @@ const MenuIndex = () => {
<div className="flex flex-wrap gap-2 items-center relative" ref={smallCategoriesRef}>
<span className="sm:text-base text-sm font-medium max-w-[113px] sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
<div className="flex min-w-[247px] flex-1 gap-2 justify-end items-center">
<button onClick={toggleFilterModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
<button onClick={toggleFilterModal} className="rounded-xl h-8 bg-container pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
<Candle2 className="stroke-foreground" size={16} />
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
</button>
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center gap-2">
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container pattern-secondary-bg 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 whitespace-nowrap">{activeSortingLabel}</span>
</button>
@@ -58,7 +58,7 @@ export const AddressDetailsModal = ({
e.preventDefault();
onChangePosition();
}}
className='bg-background! w-fit! text-foreground! flex items-center gap-2 text-xs'
className='bg-background! pattern-secondary-bg w-fit! text-foreground! flex items-center gap-2 text-xs'
disabled={isPending}
>
<Location color='#6b7280' variant='Bold' className='size-4' />
@@ -132,7 +132,7 @@ function UserAddressesPage({ }: Props) {
onClick={() => handleSetDefault(address.id)}
disabled={isSettingDefault}
pending={isSettingDefault}
className='bg-background! text-foreground! flex items-center gap-2 text-xs h-8 flex-1'
className='bg-background! pattern-secondary-bg text-foreground! flex items-center gap-2 text-xs h-8 flex-1'
>
<TickCircle variant='Bold' className='fill-foreground stroke-background size-4.5 mb-0.5' />
تنظیم به عنوان فعال
@@ -140,14 +140,14 @@ function UserAddressesPage({ }: Props) {
)}
<div className="flex gap-2">
<Link href={getEditHref(address.id)}>
<Button className='bg-background! text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'>
<Button className='bg-background! pattern-secondary-bg text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'>
<Edit2 className='stroke-foreground size-5 mb-0.5' />
</Button>
</Link>
<Button
onClick={() => handleDeleteClick(address.id)}
disabled={isDeleting}
className='bg-background! text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'
className='bg-background! pattern-secondary-bg text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'
>
<Trash className='stroke-foreground size-5 mb-0.5' />
</Button>
+5 -1
View File
@@ -1,6 +1,7 @@
"use client";
import ActiveChecker from "@/components/ActiveChecker";
import AppBackground from "@/components/background/AppBackground";
import CartChecker from "@/components/CartChecker";
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
@@ -15,7 +16,10 @@ export default function RestaurantLayoutClient({ children }: Props) {
<>
<RestaurantHeadManager />
<RestaurantNotFoundGuard>
<PreferenceWrapper>{children}</PreferenceWrapper>
<PreferenceWrapper>
<AppBackground />
{children}
</PreferenceWrapper>
</RestaurantNotFoundGuard>
<CartChecker />
<ActiveChecker />
+1 -1
View File
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
}
const res = await fetchWithTimeout(url, {
timeoutMs: 5000,
timeoutMs: 15_000,
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
});
if (!res.ok) {
+18
View File
@@ -395,4 +395,22 @@ html[data-theme="dark"] {
background-color: var(--primary);
-webkit-mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
}
html[data-pattern-bg="true"] #root {
position: relative;
z-index: 1;
isolation: isolate;
}
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;
}
+107
View File
@@ -0,0 +1,107 @@
"use client";
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
import {
applyPatternCssVariables,
clearPatternCssVariables,
colorizeSvg,
hexToRgba,
isPatternBackground,
PATTERN_BACKGROUND_OPACITY,
} from "@/lib/helpers/backgroundUtils";
import { useParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
const DEFAULT_MENU_COLOR = "#1E3A8A";
export default function AppBackground() {
const { name: restaurantSlug } = useParams<{ name: string }>();
const { data: aboutData } = useGetAbout();
const restaurant = aboutData?.data;
const [mounted, setMounted] = useState(false);
const [patternImageUrl, setPatternImageUrl] = useState<string | null>(null);
const blobUrlRef = useRef<string | null>(null);
const isPattern = restaurant ? isPatternBackground(restaurant) : false;
const menuColor = restaurant?.menuColor || DEFAULT_MENU_COLOR;
const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const revokeBlobUrl = () => {
if (blobUrlRef.current) {
URL.revokeObjectURL(blobUrlRef.current);
blobUrlRef.current = null;
}
setPatternImageUrl(null);
};
if (!isPattern || !restaurant?.bgUrl || !restaurantSlug) {
clearPatternCssVariables();
revokeBlobUrl();
return;
}
applyPatternCssVariables(menuColor);
let cancelled = false;
const proxyUrl = `/${restaurantSlug}/api/proxy-svg?url=${encodeURIComponent(restaurant.bgUrl)}`;
fetch(proxyUrl)
.then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch background pattern");
}
return response.text();
})
.then((svg) => {
if (cancelled) return;
revokeBlobUrl();
const blob = new Blob([colorizeSvg(svg, menuColor)], {
type: "image/svg+xml;charset=utf-8",
});
const blobUrl = URL.createObjectURL(blob);
blobUrlRef.current = blobUrl;
setPatternImageUrl(blobUrl);
})
.catch(() => {
if (!cancelled) {
revokeBlobUrl();
}
});
return () => {
cancelled = true;
clearPatternCssVariables();
revokeBlobUrl();
};
}, [isPattern, menuColor, restaurant?.bgUrl, restaurantSlug]);
if (!mounted || !isPattern) {
return null;
}
return createPortal(
<div
aria-hidden
className="fixed inset-0 pointer-events-none"
style={{
zIndex: 0,
backgroundColor: baseTint,
...(patternImageUrl
? {
backgroundImage: `url("${patternImageUrl}")`,
backgroundRepeat: "repeat",
backgroundSize: "auto",
}
: {}),
}}
/>,
document.body,
);
}
+1 -1
View File
@@ -150,7 +150,7 @@ const MenuItem = ({ food }: MenuItemProps) => {
</div>
<motion.div
whileTap={isAvailable ? { scale: 1.05 } : undefined}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
>
{quantity <= 0 ? (
<button
+1
View File
@@ -63,6 +63,7 @@ export default function SplashScreen({
animate={{ opacity }}
transition={{ duration: EXIT_MS / 1000, ease: 'easeOut' }}
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background"
data-slot="splash-screen"
aria-hidden="true"
>
<div className="absolute inset-0 overflow-hidden pointer-events-none">
+80
View File
@@ -0,0 +1,80 @@
export const PATTERN_BACKGROUND_OPACITY = 0.05;
export const PATTERN_VECTOR_OPACITY = 0.1;
export const PRIMARY_LIGHT_OPACITY = 0.15;
export type RestaurantBackgroundSettings = {
bgUrl?: string | null;
bgBlur?: number | null;
bgOpacity?: number | null;
bgOverlay?: string | null;
menuColor?: string | null;
};
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;
export const isCustomImageBackground = (
settings: RestaurantBackgroundSettings,
): boolean => {
if (!settings.bgUrl) return false;
return (
!isUnsetBgValue(settings.bgBlur) ||
!isUnsetBgValue(settings.bgOpacity) ||
!isUnsetBgValue(settings.bgOverlay)
);
};
export const isPatternBackground = (settings: RestaurantBackgroundSettings): boolean =>
Boolean(settings.bgUrl) && !isCustomImageBackground(settings);
export const colorizeSvg = (svg: string, color: string): string => {
let result = svg.replace(/fill="(?!black|none)[^"]+"/g, `fill="${color}"`);
result = result.replace(
/fill-opacity="[^"]+"/g,
`fill-opacity="${PATTERN_VECTOR_OPACITY}"`,
);
result = result.replace(
/(<(?:path|circle|rect|ellipse|polygon)[^>]*fill="(?!black|none)[^"]+")(?![^>]*fill-opacity)/g,
`$1 fill-opacity="${PATTERN_VECTOR_OPACITY}"`,
);
return result;
};
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");
};