load pattern
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
Reference in New Issue
Block a user