316 lines
13 KiB
TypeScript
316 lines
13 KiB
TypeScript
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;
|