Compare commits
6 Commits
b7751a566b
...
c6e38184f5
| Author | SHA1 | Date | |
|---|---|---|---|
| c6e38184f5 | |||
| 18dcc5b2ba | |||
| f8c401f2ed | |||
| fe78b218cc | |||
| f639cbd3e6 | |||
| 5638f91c81 |
@@ -1,28 +1,96 @@
|
||||
import Button from "@/components/Button";
|
||||
import ColorPicker from "@/components/ColorPicker";
|
||||
import Radio from "@/components/Radio";
|
||||
import { extractErrorMessage } from "@/config/func";
|
||||
import type { ErrorType } from "@/helpers/types";
|
||||
import { clx } from "@/helpers/utils";
|
||||
import DesignSlider from "@/pages/settings/components/DesignSlider";
|
||||
import { PATTERN_BACKGROUND_OPACITY, usePatterns } from "@/pages/settings/hooks/usePatterns";
|
||||
import { DocumentUpload, Gallery, Trash } from "iconsax-react";
|
||||
import { useCallback, useEffect, useState, type FC } from "react";
|
||||
import { getBackgroundPatternUrl, PATTERN_BACKGROUND_OPACITY, useBackgroundPatterns } from "@/pages/settings/hooks/usePatterns";
|
||||
import { useGetBackgrounds, useGetRestaurant, useSetBackground, useUpdateRestaurant } from "@/pages/settings/hooks/useSettingData";
|
||||
import type { BackgroundItemType, SetBackgroundType } from "@/pages/settings/types/Types";
|
||||
import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData";
|
||||
import { DocumentUpload, Gallery, TickCircle, Trash } from "iconsax-react";
|
||||
import { useCallback, useEffect, useMemo, useState, type FC } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const COLOR_APPLY_DELAY_MS = 500;
|
||||
type BackgroundType = "pattern" | "custom";
|
||||
|
||||
const UPLOAD_BOX_CLASS = "w-[200px] h-[362px] rounded-2xl border border-dashed border-description";
|
||||
|
||||
const getPatternBgUrl = (item: BackgroundItemType) => item.url ?? item.bgUrl ?? "";
|
||||
|
||||
const normalizeBgUrl = (url: string) => {
|
||||
if (!url) return "";
|
||||
if (url.startsWith("http")) return url;
|
||||
return `${import.meta.env.VITE_BASE_URL}${url.startsWith("/") ? url : `/${url}`}`;
|
||||
};
|
||||
|
||||
const resolveBgType = (type: BackgroundType, selectedPatternId: string | null): SetBackgroundType["bgType"] => {
|
||||
if (type === "custom") return "custom";
|
||||
if (selectedPatternId === "0") return "color";
|
||||
return "pattern";
|
||||
};
|
||||
|
||||
const DesignSettings: FC = () => {
|
||||
const [menuColor, setMenuColor] = useState("#000000");
|
||||
const [appliedColor, setAppliedColor] = useState("#000000");
|
||||
const [selectedPatternId, setSelectedPatternId] = useState(1);
|
||||
const [selectedPatternId, setSelectedPatternId] = useState<string | null>(null);
|
||||
const [backgroundType, setBackgroundType] = useState<BackgroundType>("pattern");
|
||||
const [customImage, setCustomImage] = useState<File | null>(null);
|
||||
const [customImagePreview, setCustomImagePreview] = useState<string | null>(null);
|
||||
const [existingBgUrl, setExistingBgUrl] = useState<string | null>(null);
|
||||
const [imageOpacity, setImageOpacity] = useState(100);
|
||||
const [imageBlur, setImageBlur] = useState(0);
|
||||
const [overlayDarkness, setOverlayDarkness] = useState(0);
|
||||
const patterns = usePatterns(appliedColor);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const { data: backgrounds } = useGetBackgrounds();
|
||||
const { mutate: setBackground, isPending: isSavingBackground } = useSetBackground();
|
||||
const { mutate: updateRestaurant, isPending: isUpdatingRestaurant } = useUpdateRestaurant();
|
||||
const { mutate: singleUpload, isPending: isUploadingImage } = useSingleUpload();
|
||||
const { patterns, isLoading: isPatternsLoading, isError: isPatternsError } = useBackgroundPatterns(appliedColor);
|
||||
const { data: restaurant, refetch } = useGetRestaurant();
|
||||
const backgroundItems = useMemo(() => backgrounds?.data ?? [], [backgrounds]);
|
||||
const isSaving = isSavingBackground || isUploadingImage || isUpdatingRestaurant;
|
||||
|
||||
useEffect(() => {
|
||||
if (!restaurant?.data || isInitialized) return;
|
||||
|
||||
const restaurantData = restaurant.data;
|
||||
const savedBgUrl = restaurantData.bgUrl;
|
||||
|
||||
if (savedBgUrl && backgroundItems.length === 0) return;
|
||||
|
||||
const color = restaurantData.menuColor || "#000000";
|
||||
setMenuColor(color);
|
||||
setAppliedColor(color);
|
||||
|
||||
const savedBgType = restaurantData.bgType;
|
||||
|
||||
if (savedBgType === "color" || (!savedBgUrl && savedBgType !== "custom" && savedBgType !== "image")) {
|
||||
setBackgroundType("pattern");
|
||||
setSelectedPatternId("0");
|
||||
} else if (savedBgUrl) {
|
||||
const matchingPattern = backgroundItems.find((item) => getBackgroundPatternUrl(item) === normalizeBgUrl(savedBgUrl) || getPatternBgUrl(item) === savedBgUrl);
|
||||
|
||||
if (matchingPattern && savedBgType !== "custom" && savedBgType !== "image") {
|
||||
setBackgroundType("pattern");
|
||||
setSelectedPatternId(matchingPattern.id);
|
||||
} else {
|
||||
setBackgroundType("custom");
|
||||
setExistingBgUrl(savedBgUrl);
|
||||
setCustomImagePreview(normalizeBgUrl(savedBgUrl));
|
||||
setImageOpacity(Number(restaurantData.bgOpacity) || 100);
|
||||
setImageBlur(Number(restaurantData.bgBlur) || 0);
|
||||
setOverlayDarkness(Number(restaurantData.bgOverlay) || 0);
|
||||
}
|
||||
} else {
|
||||
setSelectedPatternId("0");
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
}, [restaurant, backgroundItems, isInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setAppliedColor(menuColor), COLOR_APPLY_DELAY_MS);
|
||||
@@ -30,10 +98,7 @@ const DesignSettings: FC = () => {
|
||||
}, [menuColor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customImage) {
|
||||
setCustomImagePreview(null);
|
||||
return;
|
||||
}
|
||||
if (!customImage) return;
|
||||
|
||||
const previewUrl = URL.createObjectURL(customImage);
|
||||
setCustomImagePreview(previewUrl);
|
||||
@@ -55,11 +120,107 @@ const DesignSettings: FC = () => {
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
setCustomImage(null);
|
||||
setExistingBgUrl(null);
|
||||
setCustomImagePreview(null);
|
||||
setImageOpacity(100);
|
||||
setImageBlur(0);
|
||||
setOverlayDarkness(0);
|
||||
};
|
||||
|
||||
const saveCustomBackground = (bgUrl: string) => {
|
||||
const backgroundPayload: SetBackgroundType = {
|
||||
bgUrl,
|
||||
bgType: "custom",
|
||||
bgBlur: String(imageBlur ?? 0),
|
||||
bgOverlay: String(overlayDarkness ?? 0),
|
||||
};
|
||||
|
||||
if (imageOpacity) {
|
||||
backgroundPayload.bgOpacity = String(imageOpacity);
|
||||
}
|
||||
|
||||
setBackground(backgroundPayload, {
|
||||
onSuccess: () => {
|
||||
toast.success("تنظیمات طراحی با موفقیت ذخیره شد");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(extractErrorMessage(error as ErrorType, "خطا در ذخیره تنظیمات طراحی"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleApplyChanges = () => {
|
||||
updateRestaurant(
|
||||
{ menuColor },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (backgroundType === "pattern") {
|
||||
let bgUrl = "";
|
||||
|
||||
if (selectedPatternId !== "0") {
|
||||
const selectedItem = backgroundItems.find((item) => item.id === selectedPatternId);
|
||||
bgUrl = selectedItem ? getPatternBgUrl(selectedItem) : "";
|
||||
|
||||
if (!bgUrl) {
|
||||
toast.error("لطفاً یک پترن انتخاب کنید");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setBackground(
|
||||
{
|
||||
bgUrl,
|
||||
bgType: resolveBgType(backgroundType, selectedPatternId),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("تنظیمات طراحی با موفقیت ذخیره شد");
|
||||
refetch();
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(extractErrorMessage(error as ErrorType, "خطا در ذخیره تنظیمات طراحی"));
|
||||
},
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (customImage) {
|
||||
singleUpload(customImage, {
|
||||
onSuccess: (response) => {
|
||||
const bgUrl = response?.data?.url ?? "";
|
||||
|
||||
if (!bgUrl) {
|
||||
toast.error("خطا در آپلود تصویر");
|
||||
return;
|
||||
}
|
||||
|
||||
saveCustomBackground(bgUrl);
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(extractErrorMessage(error, "خطا در آپلود تصویر"));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingBgUrl) {
|
||||
saveCustomBackground(existingBgUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("لطفاً تصویر پسزمینه را آپلود کنید");
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error(extractErrorMessage(error as ErrorType, "خطا در ذخیره تنظیمات طراحی"));
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const isApplyDisabled = isSaving || (backgroundType === "pattern" ? !selectedPatternId : !customImage && !existingBgUrl);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-4xl p-8">
|
||||
<div className="text-lg font-light">تنظیمات طراحی</div>
|
||||
@@ -76,6 +237,16 @@ const DesignSettings: FC = () => {
|
||||
|
||||
{backgroundType === "pattern" && (
|
||||
<div className="mt-2 flex gap-3 flex-wrap">
|
||||
{isPatternsLoading && Array.from({ length: 6 }).map((_, index) => <div key={index} className="w-16 h-[139px] rounded-lg bg-secondary animate-pulse" />)}
|
||||
{!isPatternsLoading && isPatternsError && <div className="text-sm text-description">خطا در بارگذاری پترنها</div>}
|
||||
{!isPatternsLoading && !isPatternsError && patterns.length === 0 && <div className="text-sm text-description">پترنی یافت نشد</div>}
|
||||
<button
|
||||
onClick={() => setSelectedPatternId("0")}
|
||||
type="button"
|
||||
className={clx("relative w-16 h-[139px] rounded-lg bg-white border overflow-hidden", selectedPatternId === "0" ? "border-black" : "border-border")}
|
||||
>
|
||||
<div className="absolute inset-0" style={{ backgroundColor: appliedColor, opacity: PATTERN_BACKGROUND_OPACITY }} />
|
||||
</button>
|
||||
{patterns.map((pattern) => (
|
||||
<button
|
||||
key={pattern.id}
|
||||
@@ -84,7 +255,7 @@ const DesignSettings: FC = () => {
|
||||
className={clx("relative w-16 h-[139px] rounded-lg bg-white border overflow-hidden", selectedPatternId === pattern.id ? "border-black" : "border-border")}
|
||||
>
|
||||
<div className="absolute inset-0" style={{ backgroundColor: appliedColor, opacity: PATTERN_BACKGROUND_OPACITY }} />
|
||||
<img src={pattern.image} alt={pattern.name} className="relative scale-200 w-full h-full object-cover" />
|
||||
<img src={pattern.image} alt="" className="relative scale-200 w-full h-full object-cover" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -142,6 +313,14 @@ const DesignSettings: FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 flex justify-end">
|
||||
<Button type="button" className="w-fit px-6" isloading={isSaving} disabled={isApplyDisabled} onClick={handleApplyChanges}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<TickCircle size={20} color="#fff" />
|
||||
<span>اعمال تغییرات</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<CachedPatternSource | null> => {
|
||||
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("<svg")) {
|
||||
return { id: item.id, fallbackUrl: url };
|
||||
}
|
||||
|
||||
return { id: item.id, svg: content };
|
||||
} catch {
|
||||
return { id: item.id, fallbackUrl: url };
|
||||
}
|
||||
};
|
||||
|
||||
export type BackgroundPattern = {
|
||||
id: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export const useBackgroundPatterns = (color: string) => {
|
||||
const { data: backgrounds, isLoading, isError } = useGetBackgrounds();
|
||||
const [sources, setSources] = useState<CachedPatternSource[]>([]);
|
||||
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(
|
||||
() =>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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<GetRestaurantResponse> => {
|
||||
const { data } = await axios.get<GetRestaurantResponse>(
|
||||
"/admin/restaurants/my-restaurant"
|
||||
);
|
||||
const { data } = await axios.get<GetRestaurantResponse>("/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<GetBackgroundsResponse>(`/admin/restaurants/background`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const setBackground = async (params: SetBackgroundType) => {
|
||||
const { data } = await axios.patch(`/admin/restaurants/my-restaurant/background`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -40,6 +40,11 @@ export type Restaurant = {
|
||||
logo: string | null;
|
||||
address: string | null;
|
||||
menuColor: string | null;
|
||||
bgUrl?: string | null;
|
||||
bgType?: "pattern" | "custom" | "color" | "image" | null;
|
||||
bgOpacity?: string | null;
|
||||
bgBlur?: string | null;
|
||||
bgOverlay?: string | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
serviceArea: GeoJSONPolygon;
|
||||
@@ -70,3 +75,22 @@ export type Restaurant = {
|
||||
};
|
||||
|
||||
export type GetRestaurantResponse = IResponse<Restaurant>;
|
||||
|
||||
export type SetBackgroundType = {
|
||||
bgUrl: string;
|
||||
bgType: "pattern" | "custom" | "color";
|
||||
bgOpacity?: string;
|
||||
bgBlur?: string;
|
||||
bgOverlay?: string;
|
||||
};
|
||||
|
||||
export type BackgroundItemType = {
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
url?: string;
|
||||
bgUrl?: string;
|
||||
};
|
||||
|
||||
export type GetBackgroundsResponse = IResponse<BackgroundItemType[]>;
|
||||
|
||||
Reference in New Issue
Block a user