This commit is contained in:
hamid zarghami
2026-06-21 14:52:20 +03:30
parent ba82744219
commit de45b1f197
19 changed files with 622 additions and 157 deletions
+3
View File
@@ -0,0 +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+")"}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)return;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(t.patternDataUrl&&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+';background-image:url("'+t.patternDataUrl+'");background-repeat:repeat;background-size:auto;';document.body.appendChild(d)}}catch(u){}})();`;
}
+303
View File
@@ -0,0 +1,303 @@
import { calculateBrightness, hexToOklch } from "@/lib/helpers/colorUtils";
import {
hexToRgba,
isPatternBackground,
PATTERN_BACKGROUND_OPACITY,
PATTERN_VECTOR_OPACITY,
PRIMARY_LIGHT_OPACITY,
type RestaurantBackgroundSettings,
} from "@/lib/helpers/backgroundUtils";
import type { AboutResponse } from "@/app/[name]/(Main)/about/types/Types";
import type {
CategoriesResponse,
FoodsResponse,
} from "@/app/[name]/(Main)/types/Types";
import type { QueryClient } from "@tanstack/react-query";
export type CachedRestaurantTheme = {
version: 1;
menuColor: string;
bgUrl?: string | null;
isPattern: boolean;
patternDataUrl?: string | null;
primaryOklch?: string;
primaryForeground?: string;
backgroundTint?: string;
bgPatternVector?: string;
primaryLight?: string;
};
const CACHE_VERSION = 1;
const CACHE_KEY_PREFIX = "restaurant-theme";
const LEGACY_COLOR_KEY = "theme-primary-color";
const ABOUT_CACHE_PREFIX = "restaurant-about";
const MENU_CACHE_PREFIX = "restaurant-menu";
const CATEGORIES_CACHE_PREFIX = "restaurant-categories";
export const THEME_BG_ELEMENT_ID = "cached-theme-bg";
export function getThemeCacheKey(slug: string): string {
return `${CACHE_KEY_PREFIX}:${slug}`;
}
export function getAboutCacheKey(slug: string): string {
return `${ABOUT_CACHE_PREFIX}:${slug}`;
}
export function getMenuCacheKey(slug: string): string {
return `${MENU_CACHE_PREFIX}:${slug}`;
}
export function getCategoriesCacheKey(slug: string): string {
return `${CATEGORIES_CACHE_PREFIX}:${slug}`;
}
export function getRestaurantSlugFromPath(pathname?: string): string | null {
const path =
pathname ?? (typeof window !== "undefined" ? window.location.pathname : "");
const segment = path.split("/").filter(Boolean)[0];
if (!segment || segment === "auth") return null;
return segment;
}
export function computeThemeCssValues(menuColor: string) {
const primaryOklch = hexToOklch(menuColor);
const brightness = calculateBrightness(menuColor);
return {
primaryOklch: primaryOklch ?? undefined,
primaryForeground: brightness > 0.5 ? "oklch(0 0 0)" : "oklch(1 0 0)",
backgroundTint: hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY),
bgPatternVector: hexToRgba(menuColor, PATTERN_VECTOR_OPACITY),
primaryLight: hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
};
}
export function getThemeCache(slug: string): CachedRestaurantTheme | null {
if (!slug || typeof window === "undefined") return null;
try {
const raw = localStorage.getItem(getThemeCacheKey(slug));
if (!raw) return null;
const parsed = JSON.parse(raw) as CachedRestaurantTheme;
if (parsed.version !== CACHE_VERSION || !parsed.menuColor) return null;
if (!parsed.primaryOklch) {
const enriched: CachedRestaurantTheme = {
...parsed,
...computeThemeCssValues(parsed.menuColor),
version: CACHE_VERSION,
};
try {
localStorage.setItem(getThemeCacheKey(slug), JSON.stringify(enriched));
} catch {
// Ignore quota errors during migration
}
return enriched;
}
return parsed;
} catch {
return null;
}
}
export function getCachedMenuColor(slug?: string | null): string | null {
if (slug) {
const cached = getThemeCache(slug);
if (cached?.menuColor) return cached.menuColor;
}
if (typeof window === "undefined") return null;
try {
return localStorage.getItem(LEGACY_COLOR_KEY);
} catch {
return null;
}
}
export function getPersistedAboutData(slug: string): AboutResponse | undefined {
if (!slug || typeof window === "undefined") return undefined;
try {
const raw = localStorage.getItem(getAboutCacheKey(slug));
if (!raw) return undefined;
return JSON.parse(raw) as AboutResponse;
} catch {
return undefined;
}
}
export function setPersistedAboutData(slug: string, data: AboutResponse): void {
if (!slug || typeof window === "undefined") return;
try {
localStorage.setItem(getAboutCacheKey(slug), JSON.stringify(data));
} catch {
// Ignore quota errors
}
}
function readPersistedQueryData<T>(key: string): T | undefined {
try {
const raw = localStorage.getItem(key);
if (!raw) return undefined;
return JSON.parse(raw) as T;
} catch {
return undefined;
}
}
function writePersistedQueryData<T>(key: string, data: T): void {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch {
// Ignore quota errors
}
}
export function getPersistedMenuData(slug: string): FoodsResponse | undefined {
if (!slug || typeof window === "undefined") return undefined;
return readPersistedQueryData<FoodsResponse>(getMenuCacheKey(slug));
}
export function setPersistedMenuData(slug: string, data: FoodsResponse): void {
if (!slug || typeof window === "undefined") return;
writePersistedQueryData(getMenuCacheKey(slug), data);
}
export function getPersistedCategoriesData(
slug: string,
): CategoriesResponse | undefined {
if (!slug || typeof window === "undefined") return undefined;
return readPersistedQueryData<CategoriesResponse>(getCategoriesCacheKey(slug));
}
export function setPersistedCategoriesData(
slug: string,
data: CategoriesResponse,
): void {
if (!slug || typeof window === "undefined") return;
writePersistedQueryData(getCategoriesCacheKey(slug), data);
}
export function restorePersistedRestaurantQueries(
client: QueryClient,
slug: string,
): void {
const about = getPersistedAboutData(slug);
if (about) {
client.setQueryData(["about", slug], about);
}
const menu = getPersistedMenuData(slug);
if (menu) {
client.setQueryData(["menu", slug], menu);
}
const categories = getPersistedCategoriesData(slug);
if (categories) {
client.setQueryData(["categories", slug], categories);
}
}
export function setThemeCache(
slug: string,
theme: Omit<CachedRestaurantTheme, "version">,
): void {
if (!slug || typeof window === "undefined") return;
const existing = getThemeCache(slug);
const css = computeThemeCssValues(theme.menuColor);
const payload: CachedRestaurantTheme = {
...theme,
...css,
patternDataUrl: theme.patternDataUrl ?? existing?.patternDataUrl ?? null,
version: CACHE_VERSION,
};
try {
localStorage.setItem(getThemeCacheKey(slug), JSON.stringify(payload));
localStorage.setItem(LEGACY_COLOR_KEY, theme.menuColor);
} catch {
try {
const { patternDataUrl: _pattern, ...withoutPattern } = payload;
localStorage.setItem(
getThemeCacheKey(slug),
JSON.stringify({ ...withoutPattern, patternDataUrl: null }),
);
localStorage.setItem(LEGACY_COLOR_KEY, theme.menuColor);
} catch {
// Ignore quota errors
}
}
}
export function buildThemeCacheFromRestaurant(
settings: RestaurantBackgroundSettings,
): Pick<CachedRestaurantTheme, "menuColor" | "bgUrl" | "isPattern"> {
return {
menuColor: settings.menuColor || "#1E3A8A",
bgUrl: settings.bgUrl,
isPattern: isPatternBackground(settings),
};
}
export function svgToDataUrl(svg: string): string {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void {
if (typeof document === "undefined") return;
const root = document.documentElement;
if (theme.primaryOklch) {
root.style.setProperty("--primary", theme.primaryOklch);
root.style.setProperty(
"--primary-foreground",
theme.primaryForeground ?? "oklch(1 0 0)",
);
}
if (!theme.isPattern) return;
root.setAttribute("data-pattern-bg", "true");
if (theme.backgroundTint) {
root.style.setProperty("--background", theme.backgroundTint);
root.style.setProperty("--bg-pattern-base", theme.backgroundTint);
}
if (theme.bgPatternVector) {
root.style.setProperty("--bg-pattern-vector", theme.bgPatternVector);
}
if (theme.primaryLight) {
root.style.setProperty("--primary-light", theme.primaryLight);
}
if (!theme.patternDataUrl || !theme.backgroundTint) return;
let bgElement = document.getElementById(THEME_BG_ELEMENT_ID);
if (!bgElement) {
bgElement = document.createElement("div");
bgElement.id = THEME_BG_ELEMENT_ID;
bgElement.setAttribute("aria-hidden", "true");
bgElement.style.position = "fixed";
bgElement.style.inset = "0";
bgElement.style.pointerEvents = "none";
bgElement.style.zIndex = "0";
document.body.appendChild(bgElement);
}
bgElement.style.backgroundColor = theme.backgroundTint;
bgElement.style.backgroundImage = `url("${theme.patternDataUrl}")`;
bgElement.style.backgroundRepeat = "repeat";
bgElement.style.backgroundSize = "auto";
}
export function removeCachedThemeBackground(): void {
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
}