diff --git a/src/pages/settings/components/DesignSettings.tsx b/src/pages/settings/components/DesignSettings.tsx index 85e7d8c..af23c1e 100644 --- a/src/pages/settings/components/DesignSettings.tsx +++ b/src/pages/settings/components/DesignSettings.tsx @@ -2,7 +2,7 @@ import ColorPicker from "@/components/ColorPicker"; import Radio from "@/components/Radio"; import { clx } from "@/helpers/utils"; import DesignSlider from "@/pages/settings/components/DesignSlider"; -import { PATTERN_BACKGROUND_OPACITY, usePatterns } from "@/pages/settings/hooks/usePatterns"; +import { PATTERN_BACKGROUND_OPACITY, useBackgroundPatterns } from "@/pages/settings/hooks/usePatterns"; import { DocumentUpload, Gallery, Trash } from "iconsax-react"; import { useCallback, useEffect, useState, type FC } from "react"; import { useDropzone } from "react-dropzone"; @@ -15,14 +15,20 @@ const UPLOAD_BOX_CLASS = "w-[200px] h-[362px] rounded-2xl border border-dashed b const DesignSettings: FC = () => { const [menuColor, setMenuColor] = useState("#000000"); const [appliedColor, setAppliedColor] = useState("#000000"); - const [selectedPatternId, setSelectedPatternId] = useState(1); + const [selectedPatternId, setSelectedPatternId] = useState(null); const [backgroundType, setBackgroundType] = useState("pattern"); const [customImage, setCustomImage] = useState(null); const [customImagePreview, setCustomImagePreview] = useState(null); const [imageOpacity, setImageOpacity] = useState(100); const [imageBlur, setImageBlur] = useState(0); const [overlayDarkness, setOverlayDarkness] = useState(0); - const patterns = usePatterns(appliedColor); + const { patterns, isLoading: isPatternsLoading, isError: isPatternsError } = useBackgroundPatterns(appliedColor); + + useEffect(() => { + if (patterns[0]?.id) { + setSelectedPatternId((current) => current ?? patterns[0].id); + } + }, [patterns]); useEffect(() => { const timer = setTimeout(() => setAppliedColor(menuColor), COLOR_APPLY_DELAY_MS); @@ -76,6 +82,16 @@ const DesignSettings: FC = () => { {backgroundType === "pattern" && (
+ {isPatternsLoading && + Array.from({ length: 6 }).map((_, index) => ( +
+ ))} + {!isPatternsLoading && isPatternsError && ( +
خطا در بارگذاری پترن‌ها
+ )} + {!isPatternsLoading && !isPatternsError && patterns.length === 0 && ( +
پترنی یافت نشد
+ )} {patterns.map((pattern) => ( ))}
diff --git a/src/pages/settings/hooks/usePatterns.ts b/src/pages/settings/hooks/usePatterns.ts index ca77dff..730bec3 100644 --- a/src/pages/settings/hooks/usePatterns.ts +++ b/src/pages/settings/hooks/usePatterns.ts @@ -1,4 +1,6 @@ -import { useMemo } from "react"; +import type { BackgroundItemType } from "@/pages/settings/types/Types"; +import { useEffect, useMemo, useState } from "react"; +import { useGetBackgrounds } from "./useSettingData"; import firstPattern from "@/assets/images/pattern/1.svg?raw"; import secondPattern from "@/assets/images/pattern/2.svg?raw"; import thirdPattern from "@/assets/images/pattern/3.svg?raw"; @@ -18,7 +20,14 @@ const PATTERNS = [ export const PATTERN_BACKGROUND_OPACITY = 0.05; export const PATTERN_VECTOR_OPACITY = 0.1; -const colorizeSvg = (svg: string, color: string) => { +export const getBackgroundPatternUrl = (item: BackgroundItemType) => { + const url = item.url ?? item.bgUrl ?? ""; + if (!url) return ""; + if (url.startsWith("http")) return url; + return `${import.meta.env.VITE_BASE_URL}${url.startsWith("/") ? url : `/${url}`}`; +}; + +export const colorizeSvg = (svg: string, color: 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( @@ -29,9 +38,84 @@ const colorizeSvg = (svg: string, color: string) => { return result; }; -const svgToDataUrl = (svg: string) => +export const svgToDataUrl = (svg: string) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; +type CachedPatternSource = { + id: string; + svg?: string; + fallbackUrl?: string; +}; + +const fetchPatternSource = async (item: BackgroundItemType): Promise => { + const url = getBackgroundPatternUrl(item); + if (!url) return null; + + try { + const response = await fetch(url); + if (!response.ok) { + return { id: item.id, fallbackUrl: url }; + } + + const content = await response.text(); + if (!content.includes(" { + const { data: backgrounds, isLoading, isError } = useGetBackgrounds(); + const [sources, setSources] = useState([]); + const [isFetching, setIsFetching] = useState(false); + const backgroundItems = useMemo(() => backgrounds?.data ?? [], [backgrounds]); + + useEffect(() => { + if (!backgroundItems.length) { + setSources([]); + setIsFetching(false); + return; + } + + let cancelled = false; + setIsFetching(true); + + Promise.all(backgroundItems.map(fetchPatternSource)).then((results) => { + if (cancelled) return; + setSources(results.filter((item): item is CachedPatternSource => item !== null)); + setIsFetching(false); + }); + + return () => { + cancelled = true; + }; + }, [backgroundItems]); + + const patterns = useMemo( + () => + sources.map((source) => ({ + id: source.id, + image: source.svg ? svgToDataUrl(colorizeSvg(source.svg, color)) : (source.fallbackUrl ?? ""), + })), + [sources, color], + ); + + return { + patterns, + isLoading: isLoading || isFetching, + isError, + }; +}; + export const usePatterns = (color: string) => { return useMemo( () => diff --git a/src/pages/settings/hooks/useSettingData.ts b/src/pages/settings/hooks/useSettingData.ts index 8c1a2e5..eeb8853 100644 --- a/src/pages/settings/hooks/useSettingData.ts +++ b/src/pages/settings/hooks/useSettingData.ts @@ -13,3 +13,16 @@ export const useUpdateRestaurant = () => { mutationFn: api.updateRestaurant, }); }; + +export const useGetBackgrounds = () => { + return useQuery({ + queryKey: ["backgrounds"], + queryFn: api.getBackgrounds, + }); +}; + +export const useSetBackground = () => { + return useMutation({ + mutationFn: api.setBackground, + }); +}; diff --git a/src/pages/settings/service/SettingService.ts b/src/pages/settings/service/SettingService.ts index cc44c9a..36e02e9 100644 --- a/src/pages/settings/service/SettingService.ts +++ b/src/pages/settings/service/SettingService.ts @@ -1,20 +1,22 @@ import axios from "@/config/axios"; -import type { - GetRestaurantResponse, - UpdateRestaurantType, -} from "../types/Types"; +import type { GetBackgroundsResponse, GetRestaurantResponse, SetBackgroundType, UpdateRestaurantType } from "../types/Types"; export const getRestaurant = async (): Promise => { - const { data } = await axios.get( - "/admin/restaurants/my-restaurant" - ); + const { data } = await axios.get("/admin/restaurants/my-restaurant"); return data; }; export const updateRestaurant = async (params: UpdateRestaurantType) => { - const { data } = await axios.patch( - "/admin/restaurants/my-restaurant", - params - ); + const { data } = await axios.patch("/admin/restaurants/my-restaurant", params); + return data; +}; + +export const getBackgrounds = async () => { + const { data } = await axios.get(`/admin/restaurants/background`); + return data; +}; + +export const setBackground = async (params: SetBackgroundType) => { + const { data } = await axios.patch(`/admin/restaurants/my-restaurant/background`, params); return data; }; diff --git a/src/pages/settings/types/Types.ts b/src/pages/settings/types/Types.ts index fab48d0..d5b07b6 100644 --- a/src/pages/settings/types/Types.ts +++ b/src/pages/settings/types/Types.ts @@ -70,3 +70,21 @@ export type Restaurant = { }; export type GetRestaurantResponse = IResponse; + +export type SetBackgroundType = { + bgUrl: string; + bgOpacity?: number; + bgBlur?: number; + bgOverlay?: string; +}; + +export type BackgroundItemType = { + createdAt: string; + deletedAt: string | null; + id: string; + updatedAt: string; + url?: string; + bgUrl?: string; +}; + +export type GetBackgroundsResponse = IResponse;