diff --git a/.eslintrc.json b/.eslintrc.json index acf6d3a..3f21361 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,7 +19,9 @@ "it": true, "author": true, "expect": true, - "jest": true + "jest": true, + "beforeEach": true, + "afterEach": true }, "rules": { "no-unused-vars": "off", diff --git a/package-lock.json b/package-lock.json index c3c122c..52908b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "next": "14.2.3", "next-themes": "^0.3.0", "react": "^18", - "react-dom": "^18", + "react-dom": "^18.3.1", "react-hook-form": "^7.51.4", "react-intersection-observer": "^9.10.2", "tailwind-merge": "^2.3.0", @@ -2994,6 +2994,21 @@ "glob": "10.3.10" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-darwin-x64": { "version": "14.2.3", "cpu": [ @@ -3008,6 +3023,111 @@ "node": ">= 10" } }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -15991,7 +16111,8 @@ }, "node_modules/react-dom": { "version": "18.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -19327,126 +19448,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", - "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", - "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", - "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", - "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", - "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", - "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", - "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", - "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index 7da4684..a53f2e0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "next": "14.2.3", "next-themes": "^0.3.0", "react": "^18", - "react-dom": "^18", + "react-dom": "^18.3.1", "react-hook-form": "^7.51.4", "react-intersection-observer": "^9.10.2", "tailwind-merge": "^2.3.0", diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c687f2b..3fb6433 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,5 +1,4 @@ import { EditPost } from "@/components/app/edit-post"; -import { fetchPost } from "@/lib/data"; import { PostsTable } from "@/components/app/posts-table"; import { Suspense } from "react"; import { PostsTableSkeleton } from "@/components/app/posts-table/skeleton"; @@ -16,13 +15,10 @@ interface AdminPageProps { // https://nextjs.org/docs/app/api-reference/file-conventions/page // Need to check the next package if there are types for page components. const AdminPage: React.FC = async ({ searchParams }) => { - const postId = Number(searchParams?.id); - const editingPost = isNaN(postId) ? undefined : await fetchPost(postId); const currentPage = Number(searchParams?.page) || 1; return (
- {/* post = undefined handles new post */} - + }> diff --git a/src/components/app/edit-post/edit-post.tsx b/src/components/app/edit-post/edit-post.tsx index e0b3528..756c1c1 100644 --- a/src/components/app/edit-post/edit-post.tsx +++ b/src/components/app/edit-post/edit-post.tsx @@ -1,6 +1,5 @@ "use client"; -import { createOrUpdatePost } from "@/lib/actions"; -import { type BlogPost } from "@/lib/definitions"; +import { useCallback, useEffect, useReducer, useTransition } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -12,70 +11,153 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; +import { BlogPost } from "@/lib/definitions"; import { LoaderCircleIcon } from "lucide-react"; -import { useRouter, usePathname, useSearchParams } from "next/navigation"; -import { useTransition } from "react"; +import { useSearchParams } from "next/navigation"; +import { fetchPost } from "@/lib/data"; +import { createOrUpdatePost } from "@/lib/actions"; +import { useEditPostOverlay } from "./useEditPostOverlay"; -interface EditPostProps { - post?: BlogPost; +interface State { + post: BlogPost | null; + errorMessages: { title?: string; description?: string }; } -export const EditPost: React.FC = ({ post }) => { - const searchParams = useSearchParams(); - const pathname = usePathname(); - const { replace } = useRouter(); - const overlay = searchParams.get("overlay")?.toString(); - const canShowModal = overlay === "new-post" || overlay === "edit-post"; +type Action = + | { type: "SET_POST"; payload: BlogPost | null } + | { + type: "SET_ERROR_MESSAGES"; + payload: { title?: string; description?: string }; + } + | { type: "RESET_ERROR_MESSAGES" }; + +const initialState: State = { + post: null, + errorMessages: { title: "", description: "" }, +}; +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "SET_POST": + return { ...state, post: action.payload }; + case "SET_ERROR_MESSAGES": + return { ...state, errorMessages: action.payload }; + case "RESET_ERROR_MESSAGES": + return { ...state, errorMessages: { title: "", description: "" } }; + default: + return state; + } +}; + +export const EditPost: React.FC = () => { const [isPending, startTransition] = useTransition(); + const [state, dispatch] = useReducer(reducer, initialState); + const { isOpen, openOverlay, closeOverlay } = useEditPostOverlay(); - const onSubmit = (formData: FormData) => { - startTransition(() => void createOrUpdatePost(formData)); - }; + const searchParams = useSearchParams(); + const idParam = searchParams.get("id"); + const postId = Number(idParam) ? Number(idParam) : undefined; - return ( - { - const params = new URLSearchParams(); - if (open) { - params.set("overlay", "new-post"); + // Fetch post data before start editing. + useEffect(() => { + if (!postId) { + dispatch({ type: "SET_POST", payload: null }); + return; + } + startTransition(async () => { + const post = await fetchPost(postId); + dispatch({ type: "SET_POST", payload: post }); + }); + }, [postId]); + + useEffect(() => { + if (!isOpen) { + dispatch({ type: "RESET_ERROR_MESSAGES" }); + } + }, [isOpen]); + + const onSubmit = useCallback( + (formData: FormData) => { + startTransition(async () => { + const result = await createOrUpdatePost(formData, postId); + if (result?.errors) { + dispatch({ + type: "SET_ERROR_MESSAGES", + payload: { + title: result.errors.title?.toString(), + description: result.errors.description?.toString(), + }, + }); } else { - params.delete("overlay"); + closeOverlay(); } - replace(`${pathname}?${params.toString()}`); - }} + }); + }, + [closeOverlay, postId] + ); + + const handleInputChange = useCallback(() => { + // Reset error messages when the user starts typing + dispatch({ type: "RESET_ERROR_MESSAGES" }); + }, []); + + return ( + (open ? openOverlay() : closeOverlay())} > - {Boolean(post) ? "Edit Blog Post" : "Add New Blog Post"} + {postId ? "Edit Blog Post" : "Add New Blog Post"} -
+ { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + onSubmit(formData); + }} + >
+ {state.errorMessages.title && ( +

+ {state.errorMessages.title} +

+ )}