diff --git a/.eslintrc.json b/.eslintrc.json index db32bb9..fdca050 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "import/no-relative-parent-imports": [ "error", { - "ignore": ["@/app", "@/components", "@/lib"] + "ignore": ["@/app", "@/components", "@/lib", "@/hooks"] } ] } diff --git a/app/(stories)/item/[id]/reply-form.tsx b/app/(stories)/item/[id]/reply-form.tsx index cc443be..b43be02 100644 --- a/app/(stories)/item/[id]/reply-form.tsx +++ b/app/(stories)/item/[id]/reply-form.tsx @@ -6,13 +6,25 @@ import { replyAction, type ReplyActionData } from "./actions"; import { Loader2 } from "lucide-react"; import { useFormStatus, useFormState } from "react-dom"; import Link from "next/link"; +import { useEffect, useState } from 'react'; +import useStoreState from '@/hooks/use-local-store'; export function ReplyForm({ storyId }: { storyId: string }) { const [state, formAction] = useFormState(replyAction, {}); + const [storedComment, setStoredComment] = useStoreState(storyId, ""); return ( -
- + { + formAction(payload); + setStoredComment(""); + }}> + ); } @@ -20,11 +32,21 @@ export function ReplyForm({ storyId }: { storyId: string }) { function ReplyFormFields({ error, commentId, + setStoredComment, + storedComment, storyId, }: ReplyActionData & { + setStoredComment: (value: string) => void; + storedComment: string; storyId: string; }) { const { pending } = useFormStatus(); + const [isDraftSaved, setIsDraftSaved] = useState(false); + + // Change state only after mount to prevent hydration errors + useEffect(() => { + setIsDraftSaved(!!storedComment); + }, [storedComment]); return (
@@ -36,6 +58,10 @@ function ReplyFormFields({ className="w-full text-base bg-white" placeholder="Write a reply..." rows={4} + value={storedComment} + onChange={(e) => { + setStoredComment(e.target.value); + }} onKeyDown={(e) => { if ( (e.ctrlKey || e.metaKey) && @@ -59,7 +85,7 @@ function ReplyFormFields({ {pending ? : null} Submit - {error && + {error ? ( "message" in error && (error.code === "AUTH_ERROR" ? ( @@ -71,7 +97,10 @@ function ReplyFormFields({ ) : ( {error.message} - ))} + )) + ) : isDraftSaved && !pending ? ( + Saved to draft. + ) : null}
); diff --git a/hooks/use-local-store.ts b/hooks/use-local-store.ts new file mode 100644 index 0000000..5faedd7 --- /dev/null +++ b/hooks/use-local-store.ts @@ -0,0 +1,45 @@ +import { useState, useEffect, Dispatch, SetStateAction } from 'react'; + +function useStoreState( + _key: string, + _initialValue: T | (() => T) +): [T, Dispatch>]; +function useStoreState( + _key: string +): [T | undefined, Dispatch>]; + +function useStoreState( + key: string, + initialValue?: T | (() => T) +) { + const [value, setValue] = useState(() => { + if (typeof window !== 'undefined' && window.localStorage) { + try { + const storedValue = localStorage.getItem(key); + return storedValue ? (JSON.parse(storedValue) as T) : initialValue; + } catch (error) {} + } + return initialValue; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); + + useEffect(() => { + const handleStorageChange = (event: StorageEvent) => { + if (event.key === key) + setValue(event.newValue ? JSON.parse(event.newValue) : initialValue); + }; + + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, [key, initialValue]); + + return [value, setValue] as const; +} + +export default useStoreState; \ No newline at end of file