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'>
|
<div className='w-full flex justify-end'>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={{ scale: 1.05 }}
|
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
|
<button
|
||||||
onClick={onIncrement}
|
onClick={onIncrement}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ function FoodPage({}: Props) {
|
|||||||
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
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 ? (
|
{quantity <= 0 ? (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ function AboutPage() {
|
|||||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
|
<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">
|
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
||||||
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
|
<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' />
|
<EqualizerIcon className='dark:text-white' />
|
||||||
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
|
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -175,11 +175,11 @@ const MenuIndex = () => {
|
|||||||
<div className="flex flex-wrap gap-2 items-center relative" ref={smallCategoriesRef}>
|
<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>
|
<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">
|
<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} />
|
<Candle2 className="stroke-foreground" size={16} />
|
||||||
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
|
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
|
||||||
</button>
|
</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" />
|
<TextAlignIcon className="text-foreground" />
|
||||||
<span className="text-xs leading-5 font-medium whitespace-nowrap">{activeSortingLabel}</span>
|
<span className="text-xs leading-5 font-medium whitespace-nowrap">{activeSortingLabel}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const AddressDetailsModal = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onChangePosition();
|
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}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
<Location color='#6b7280' variant='Bold' className='size-4' />
|
<Location color='#6b7280' variant='Bold' className='size-4' />
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ function UserAddressesPage({ }: Props) {
|
|||||||
onClick={() => handleSetDefault(address.id)}
|
onClick={() => handleSetDefault(address.id)}
|
||||||
disabled={isSettingDefault}
|
disabled={isSettingDefault}
|
||||||
pending={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' />
|
<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">
|
<div className="flex gap-2">
|
||||||
<Link href={getEditHref(address.id)}>
|
<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' />
|
<Edit2 className='stroke-foreground size-5 mb-0.5' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleDeleteClick(address.id)}
|
onClick={() => handleDeleteClick(address.id)}
|
||||||
disabled={isDeleting}
|
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' />
|
<Trash className='stroke-foreground size-5 mb-0.5' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ActiveChecker from "@/components/ActiveChecker";
|
import ActiveChecker from "@/components/ActiveChecker";
|
||||||
|
import AppBackground from "@/components/background/AppBackground";
|
||||||
import CartChecker from "@/components/CartChecker";
|
import CartChecker from "@/components/CartChecker";
|
||||||
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
|
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
|
||||||
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
|
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
|
||||||
@@ -15,7 +16,10 @@ export default function RestaurantLayoutClient({ children }: Props) {
|
|||||||
<>
|
<>
|
||||||
<RestaurantHeadManager />
|
<RestaurantHeadManager />
|
||||||
<RestaurantNotFoundGuard>
|
<RestaurantNotFoundGuard>
|
||||||
<PreferenceWrapper>{children}</PreferenceWrapper>
|
<PreferenceWrapper>
|
||||||
|
<AppBackground />
|
||||||
|
{children}
|
||||||
|
</PreferenceWrapper>
|
||||||
</RestaurantNotFoundGuard>
|
</RestaurantNotFoundGuard>
|
||||||
<CartChecker />
|
<CartChecker />
|
||||||
<ActiveChecker />
|
<ActiveChecker />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
||||||
}
|
}
|
||||||
const res = await fetchWithTimeout(url, {
|
const res = await fetchWithTimeout(url, {
|
||||||
timeoutMs: 5000,
|
timeoutMs: 15_000,
|
||||||
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
|
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -396,3 +396,21 @@ html[data-theme="dark"] {
|
|||||||
-webkit-mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
|
-webkit-mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
|
||||||
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>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
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 ? (
|
{quantity <= 0 ? (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default function SplashScreen({
|
|||||||
animate={{ opacity }}
|
animate={{ opacity }}
|
||||||
transition={{ duration: EXIT_MS / 1000, ease: 'easeOut' }}
|
transition={{ duration: EXIT_MS / 1000, ease: 'easeOut' }}
|
||||||
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background"
|
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background"
|
||||||
|
data-slot="splash-screen"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
<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