From d3d63a922c43359e66a7bc72c2a8af034a5711b2 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 13 Oct 2025 10:48:12 +0330 Subject: [PATCH] unsaveChangeWarning hooks --- src/hooks/useUnsavedChangesWarning.ts | 74 +++++++++++++++++++++++++++ src/pages/blog/CreateBlog.tsx | 5 ++ src/pages/blog/Update.tsx | 6 +++ 3 files changed, 85 insertions(+) create mode 100644 src/hooks/useUnsavedChangesWarning.ts diff --git a/src/hooks/useUnsavedChangesWarning.ts b/src/hooks/useUnsavedChangesWarning.ts new file mode 100644 index 0000000..28043bc --- /dev/null +++ b/src/hooks/useUnsavedChangesWarning.ts @@ -0,0 +1,74 @@ +import { useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; + +/** + * هوک برای هشدار به کاربر هنگام خروج از صفحه با تغییرات ذخیره نشده + * این هوک از رفرش، بستن تب، و navigation داخلی (دکمه برگشت، لینک‌ها) جلوگیری می‌کند + * @param isDirty - آیا فرم تغییر کرده است؟ + * @param message - پیام هشدار (اختیاری) + */ +export const useUnsavedChangesWarning = ( + isDirty: boolean, + message: string = "تغییرات شما ذخیره نشده‌اند. آیا مطمئن هستید که می‌خواهید این صفحه را ترک کنید؟" +) => { + const location = useLocation(); + const currentPath = useRef(location.pathname); + const isNavigatingRef = useRef(false); + + // برای جلوگیری از بستن/رفرش مرورگر + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isDirty) { + e.preventDefault(); + e.returnValue = ""; + return ""; + } + }; + + if (isDirty) { + window.addEventListener("beforeunload", handleBeforeUnload); + } + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [isDirty]); + + // برای جلوگیری از navigation داخلی (دکمه برگشت مرورگر) + useEffect(() => { + if (!isDirty) { + currentPath.current = location.pathname; + return; + } + + const handlePopState = () => { + if (isDirty && !isNavigatingRef.current) { + const confirmLeave = window.confirm(message); + + if (!confirmLeave) { + // اگر کاربر لغو کرد، به صفحه قبلی برگرد + window.history.pushState(null, "", currentPath.current); + } else { + // اگر کاربر تایید کرد، اجازه navigation بده + currentPath.current = location.pathname; + } + } + }; + + // یک state جدید به history اضافه کن تا بتونیم popstate رو catch کنیم + window.history.pushState(null, "", location.pathname); + + window.addEventListener("popstate", handlePopState); + + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, [isDirty, message, location.pathname]); + + // Update current path when location changes + useEffect(() => { + if (!isDirty) { + currentPath.current = location.pathname; + } + }, [location.pathname, isDirty]); +}; diff --git a/src/pages/blog/CreateBlog.tsx b/src/pages/blog/CreateBlog.tsx index 43f1545..e44d6ba 100644 --- a/src/pages/blog/CreateBlog.tsx +++ b/src/pages/blog/CreateBlog.tsx @@ -16,11 +16,13 @@ import { toast } from 'react-toastify' import { ErrorType } from '../../helpers/types' import { Pages } from '../../config/Pages' import { useNavigate } from 'react-router-dom' +import { useUnsavedChangesWarning } from '../../hooks/useUnsavedChangesWarning' const CreateBlog: FC = () => { const { t } = useTranslation('global') const [file, setFile] = useState() const [audioFile, setAudioFile] = useState() + const [isSaved, setIsSaved] = useState(false) const singleSlug = useSingleUpload() const audioUpload = useSingleUpload() @@ -85,6 +87,7 @@ const CreateBlog: FC = () => { createBlog.mutate(values, { onSuccess() { + setIsSaved(true) toast.success(t('success')) navigate(Pages.blog.list) }, @@ -95,6 +98,8 @@ const CreateBlog: FC = () => { }, }) + // استفاده از hook برای هشدار تغییرات ذخیره نشده + useUnsavedChangesWarning(formik.dirty && !isSaved) return (
diff --git a/src/pages/blog/Update.tsx b/src/pages/blog/Update.tsx index 5f8a781..42da6d4 100644 --- a/src/pages/blog/Update.tsx +++ b/src/pages/blog/Update.tsx @@ -16,12 +16,14 @@ import { toast } from 'react-toastify' import { ErrorType } from '../../helpers/types' import { Pages } from '../../config/Pages' import { useNavigate, useParams } from 'react-router-dom' +import { useUnsavedChangesWarning } from '../../hooks/useUnsavedChangesWarning' const UpdateBlog: FC = () => { const { id } = useParams() const { t } = useTranslation('global') const [file, setFile] = useState() const [audioFile, setAudioFile] = useState() + const [isSaved, setIsSaved] = useState(false) const singleSlug = useSingleUpload() const audioUpload = useSingleUpload() const getBlogDetail = useGetBlogDetail(id || '') @@ -84,6 +86,7 @@ const UpdateBlog: FC = () => { updateBlog.mutate({ id: id || '', params: values }, { onSuccess() { + setIsSaved(true) toast.success(t('success')) navigate(Pages.blog.list) }, @@ -102,6 +105,9 @@ const UpdateBlog: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getBlogDetail.data]) + // استفاده از hook برای هشدار تغییرات ذخیره نشده + useUnsavedChangesWarning(formik.dirty && !isSaved) + return (