Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions app/(stories)/item/[id]/reply-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,47 @@ 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 '@/lib/use-local-store';

export function ReplyForm({ storyId }: { storyId: string }) {
const [state, formAction] = useFormState(replyAction, {});
const [storedComment, setStoredComment] = useStoreState(storyId, "");

return (
<form action={formAction}>
<ReplyFormFields storyId={storyId} {...state} />
<form
action={(payload) => {
formAction(payload);
setStoredComment("");
}}>
<ReplyFormFields
storyId={storyId}
storedComment={storedComment}
setStoredComment={setStoredComment}
{...state}
/>
</form>
);
}

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 (
<div key={commentId} className="flex flex-col gap-2">
Expand All @@ -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) &&
Expand All @@ -59,7 +85,7 @@ function ReplyFormFields({
{pending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Submit
</Button>
{error &&
{error ? (
"message" in error &&
(error.code === "AUTH_ERROR" ? (
<span className="text-red-500 text-sm">
Expand All @@ -71,7 +97,10 @@ function ReplyFormFields({
</span>
) : (
<span className="text-red-500 text-sm">{error.message}</span>
))}
))
) : isDraftSaved && !pending ? (
<span className="text-[#666] text-sm">Saved to draft.</span>
) : null}
</div>
</div>
);
Expand Down
45 changes: 45 additions & 0 deletions lib/use-local-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react';

function useStoreState<T>(
_key: string,
_initialValue: T | (() => T)
): [T, Dispatch<SetStateAction<T>>];
function useStoreState<T = undefined>(
_key: string
): [T | undefined, Dispatch<SetStateAction<T | undefined>>];

function useStoreState<T = undefined>(
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;