diff --git a/src/app/[name]/(Main)/about/types/Types.ts b/src/app/[name]/(Main)/about/types/Types.ts index 609e485..e8acdbd 100644 --- a/src/app/[name]/(Main)/about/types/Types.ts +++ b/src/app/[name]/(Main)/about/types/Types.ts @@ -45,6 +45,7 @@ export interface Restaurant { domain: string; score: Score; plan: "base" | "premium"; + bgType?: "pattern" | "custom" | "color"; bgBlur?: number; bgOpacity?: number; bgOverlay?: string; diff --git a/src/components/background/PatternBackgroundProvider.tsx b/src/components/background/PatternBackgroundProvider.tsx index 26faa55..8e0dbcc 100644 --- a/src/components/background/PatternBackgroundProvider.tsx +++ b/src/components/background/PatternBackgroundProvider.tsx @@ -8,10 +8,9 @@ import { clearPatternCssVariables, colorizeSvg, hexToRgba, - isCustomImageBackground, - isPatternBackground, normalizeBgNumber, PATTERN_BACKGROUND_OPACITY, + resolveBgType, } from "@/lib/helpers/backgroundUtils"; import { applyCachedThemeToDom, @@ -65,13 +64,14 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) const [hydratedCache, setHydratedCache] = useState(null); const [patternImageUrl, setPatternImageUrl] = useState(null); - const isPattern = restaurant - ? isPatternBackground(restaurant) - : (hydratedCache?.isPattern ?? false); + const bgType = restaurant + ? resolveBgType(restaurant) + : hydratedCache + ? resolveBgType(hydratedCache) + : "color"; - const isCustomImage = restaurant - ? isCustomImageBackground(restaurant) - : (!hydratedCache?.isPattern && !!hydratedCache?.bgUrl); + const isPattern = bgType === "pattern"; + const isCustomImage = bgType === "custom"; const menuColor = restaurant?.menuColor || hydratedCache?.menuColor || DEFAULT_MENU_COLOR; const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY); @@ -93,6 +93,7 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) useEffect(() => { if (!restaurantSlug) { clearPatternCssVariables(); + clearCustomImageCssVariables(); removeCachedThemeBackground(); setPatternImageUrl(null); return; @@ -101,19 +102,22 @@ export function PatternBackgroundProvider({ children }: { children: ReactNode }) if (!restaurant) return; const themeBase = buildThemeCacheFromRestaurant(restaurant); + const currentBgType = themeBase.bgType; - if (!themeBase.isPattern) { + if (currentBgType === "color") { + clearPatternCssVariables(); + clearCustomImageCssVariables(); + removeCachedThemeBackground(); + setPatternImageUrl(null); + setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null }); + return; + } + + if (currentBgType === "custom") { clearPatternCssVariables(); setPatternImageUrl(null); - - if (isCustomImageBackground(restaurant)) { - applyCustomImageCssVariables(); - setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null }); - } else { - clearCustomImageCssVariables(); - removeCachedThemeBackground(); - setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null }); - } + applyCustomImageCssVariables(); + setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null }); return; } diff --git a/src/components/wrapper/PreferenceWrapper.tsx b/src/components/wrapper/PreferenceWrapper.tsx index 8b3be6a..c278070 100644 --- a/src/components/wrapper/PreferenceWrapper.tsx +++ b/src/components/wrapper/PreferenceWrapper.tsx @@ -134,6 +134,7 @@ function PreferenceWrapper({ children }: Props) { const themeBase = buildThemeCacheFromRestaurant(restaurant); setThemeCache(restaurantSlug, { menuColor, + bgType: themeBase.bgType, bgUrl: restaurant.bgUrl, bgBlur: themeBase.bgBlur, bgOpacity: themeBase.bgOpacity, diff --git a/src/lib/helpers/backgroundUtils.ts b/src/lib/helpers/backgroundUtils.ts index d09ae39..b85e078 100644 --- a/src/lib/helpers/backgroundUtils.ts +++ b/src/lib/helpers/backgroundUtils.ts @@ -2,12 +2,16 @@ 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 => { @@ -32,7 +36,7 @@ export const hexToRgba = (hex: string, alpha: number): string => { const isUnsetBgValue = (value: unknown): boolean => value == null || value === "" || value === 0; -export const isCustomImageBackground = ( +const isCustomImageBackgroundLegacy = ( settings: RestaurantBackgroundSettings, ): boolean => { if (!settings.bgUrl) return false; @@ -44,8 +48,40 @@ export const isCustomImageBackground = ( ); }; -export const isPatternBackground = (settings: RestaurantBackgroundSettings): boolean => - !isCustomImageBackground(settings); +export const resolveBgType = ( + settings: RestaurantBackgroundSettings, +): BgType => { + 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, diff --git a/src/lib/helpers/themeBootScript.ts b/src/lib/helpers/themeBootScript.ts index b7c6198..64e8aa4 100644 --- a/src/lib/helpers/themeBootScript.ts +++ b/src/lib/helpers/themeBootScript.ts @@ -1,3 +1,3 @@ export function getThemeBootScript(): string { - return `(function(){try{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){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)";document.documentElement.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)}var e=document.documentElement;if(t.primaryOklch){e.style.setProperty("--primary",t.primaryOklch);e.style.setProperty("--primary-foreground",t.primaryForeground||"oklch(1 0 0)")}if(!t.isPattern&&!isCustomImg(t))return;if(t.isPattern){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(isCustomImg(t)){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){}})();`; + return `(function(){try{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){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)";document.documentElement.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){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"}var e=document.documentElement;if(t.primaryOklch){e.style.setProperty("--primary",t.primaryOklch);e.style.setProperty("--primary-foreground",t.primaryForeground||"oklch(1 0 0)")}var bt=resolveBgType(t);if(bt==="color")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){}})();`; } diff --git a/src/lib/helpers/themeCache.ts b/src/lib/helpers/themeCache.ts index 2da4992..6d72864 100644 --- a/src/lib/helpers/themeCache.ts +++ b/src/lib/helpers/themeCache.ts @@ -1,12 +1,12 @@ import { calculateBrightness, hexToOklch } from "@/lib/helpers/colorUtils"; import { hexToRgba, - isCustomImageBackground, - isPatternBackground, + 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"; @@ -19,6 +19,7 @@ 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; @@ -250,15 +251,24 @@ export function buildThemeCacheFromRestaurant( settings: RestaurantBackgroundSettings, ): Pick< CachedRestaurantTheme, - "menuColor" | "bgUrl" | "bgBlur" | "bgOpacity" | "bgOverlay" | "isPattern" + | "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: isPatternBackground(settings), + isPattern: bgType === "pattern", }; } @@ -269,6 +279,7 @@ export function themeBackgroundMatches( 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 && @@ -309,11 +320,16 @@ export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void { ); } - const isCustomImage = isCustomImageBackground(theme); + const bgType = resolveBgType(theme); - if (!theme.isPattern && !isCustomImage) return; + if (bgType === "color") { + root.removeAttribute("data-pattern-bg"); + root.removeAttribute("data-image-bg"); + document.getElementById(THEME_BG_ELEMENT_ID)?.remove(); + return; + } - if (theme.isPattern) { + if (bgType === "pattern") { root.setAttribute("data-pattern-bg", "true"); root.removeAttribute("data-image-bg"); @@ -349,7 +365,7 @@ export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void { return; } - if (isCustomImage && theme.bgUrl) { + if (bgType === "custom" && theme.bgUrl) { root.setAttribute("data-image-bg", "true"); root.removeAttribute("data-pattern-bg");