Files
dmenu-admin/src/pages/settings/components/DesignSettings.tsx
T
hamid zarghami 18dcc5b2ba set empty
2026-06-22 11:15:25 +03:30

316 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { 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 DesignSettings: FC = () => {
const [menuColor, setMenuColor] = useState("#000000");
const [appliedColor, setAppliedColor] = useState("#000000");
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 [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);
if (savedBgUrl) {
const matchingPattern = backgroundItems.find((item) => getBackgroundPatternUrl(item) === normalizeBgUrl(savedBgUrl) || getPatternBgUrl(item) === savedBgUrl);
if (matchingPattern) {
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);
return () => clearTimeout(timer);
}, [menuColor]);
useEffect(() => {
if (!customImage) return;
const previewUrl = URL.createObjectURL(customImage);
setCustomImagePreview(previewUrl);
return () => URL.revokeObjectURL(previewUrl);
}, [customImage]);
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles[0]) {
setCustomImage(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: { "image/*": [] },
multiple: false,
});
const handleRemoveImage = () => {
setCustomImage(null);
setExistingBgUrl(null);
setCustomImagePreview(null);
setImageOpacity(100);
setImageBlur(0);
setOverlayDarkness(0);
};
const saveCustomBackground = (bgUrl: string) => {
const backgroundPayload: SetBackgroundType = {
bgUrl,
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 },
{
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>
<div className="flex mt-5">
<div className="max-w-[506px] w-full">
<ColorPicker label="رنگ منو" value={menuColor} onChange={setMenuColor} />
<div className="mt-7">
<div className="flex gap-2 items-center">
<Radio isActive={backgroundType === "pattern"} value="pattern" onChange={(value) => setBackgroundType(value as BackgroundType)} />
<div className="text-sm">انتخاب پس زمینه</div>
</div>
{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}
type="button"
onClick={() => setSelectedPatternId(pattern.id)}
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="" className="relative scale-200 w-full h-full object-cover" />
</button>
))}
</div>
)}
</div>
<div className="mt-7">
<div className="flex gap-2 items-center">
<Radio isActive={backgroundType === "custom"} value="custom" onChange={(value) => setBackgroundType(value as BackgroundType)} />
<div className="text-sm">تصویر پس زمینه دلخواه</div>
</div>
{backgroundType === "custom" && (
<div className="mt-2">
{!customImagePreview ? (
<div {...getRootProps()} className={clx(UPLOAD_BOX_CLASS, "flex flex-col items-center justify-center gap-3 cursor-pointer bg-white")}>
<input {...getInputProps()} />
<Gallery className="size-8" color="#8C90A3" />
<div className="text-description text-xs text-center leading-5 px-4">تصویر مورد نظر را آپلود کنید</div>
<div className="flex items-center gap-1">
<DocumentUpload className="size-4" color="black" />
<div className="text-xs">آپلود</div>
</div>
</div>
) : (
<div className="flex gap-8 items-start">
<div className={clx(UPLOAD_BOX_CLASS, "relative shrink-0 overflow-hidden")}>
<img
src={customImagePreview}
alt="پس‌زمینه دلخواه"
className="w-full h-full object-cover"
style={{
opacity: imageOpacity / 100,
filter: `blur(${imageBlur * 0.2}px)`,
}}
/>
{overlayDarkness > 0 && <div className="absolute inset-0 bg-black pointer-events-none" style={{ opacity: overlayDarkness / 100 }} />}
<button
type="button"
onClick={handleRemoveImage}
className="absolute top-1.5 right-1.5 size-5 rounded-full bg-white shadow-sm flex items-center justify-center cursor-pointer z-10"
>
<Trash size={12} color="#EF4444" variant="Bold" />
</button>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-normal mb-4">تنظیمات</div>
<DesignSlider label="شفافیت تصویر" value={imageOpacity} onChange={setImageOpacity} />
<DesignSlider label="محوشدگی تصویر" value={imageBlur} onChange={setImageBlur} />
<DesignSlider label="تیرگی روکش پس زمینه" value={overlayDarkness} onChange={setOverlayDarkness} />
</div>
</div>
)}
</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>
);
};
export default DesignSettings;