diff --git a/bun.lock b/bun.lock index 3dd9863..6df0565 100644 --- a/bun.lock +++ b/bun.lock @@ -68,6 +68,7 @@ "react-dom": "^19.0.0", "react-dropzone": "^14.3.8", "react-hook-form": "^7.55.0", + "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", "remeda": "^2.21.2", @@ -755,6 +756,8 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], @@ -1043,6 +1046,8 @@ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-medium-image-zoom": ["react-medium-image-zoom@5.2.14", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-nfTVYcAUnBzXQpPDcZL+cG/e6UceYUIG+zDcnemL7jtAqbJjVVkA85RgneGtJeni12dTyiRPZVM6Szkmwd/o8w=="], "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], diff --git a/package.json b/package.json index 07fb90b..bac583d 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "react-dom": "^19.0.0", "react-dropzone": "^14.3.8", "react-hook-form": "^7.55.0", + "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", "remeda": "^2.21.2", diff --git a/src/app/(home)/admin/blog/edit/[id]/page.tsx b/src/app/(home)/admin/blog/edit/[id]/page.tsx new file mode 100644 index 0000000..4d7fcb6 --- /dev/null +++ b/src/app/(home)/admin/blog/edit/[id]/page.tsx @@ -0,0 +1,220 @@ +'use client' + +import { MarkdownEditor } from '@/components/markdown-editor' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' +import { Textarea } from '@/components/ui/textarea' +import { api } from '@/trpc/react' +import { useParams } from 'next/navigation' +import { useRouter } from 'next/navigation' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' + +export default function EditBlogPostPage() { + const params = useParams<{ + id: string + }>() + const id = Number.parseInt(params.id, 10) + const router = useRouter() + + const [title, setTitle] = useState('') + const [content, setContent] = useState('') + const [excerpt, setExcerpt] = useState('') + const [published, setPublished] = useState(false) + const [updateSlug, setUpdateSlug] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + // Fetch post data + const { data: posts, isLoading: isFetching } = api.blog.getAll.useQuery() + + useEffect(() => { + if (posts) { + const currentPost = posts.find((p) => p.id === id) + if (currentPost) { + setTitle(currentPost.title) + setContent(currentPost.content) + setExcerpt(currentPost.excerpt || '') + setPublished(currentPost.published) + setIsLoading(false) + } else { + toast.error('Blog post not found') + router.push('/admin/blog') + } + } + }, [posts, id, router]) + + // Update post mutation + const updatePost = api.blog.update.useMutation({ + onSuccess: () => { + toast.success('Blog post updated successfully') + router.push('/admin/blog') + router.refresh() + }, + onError: (error) => { + toast.error(`Error updating blog post: ${error.message}`) + setIsSubmitting(false) + }, + }) + + // Delete post mutation + const deletePost = api.blog.delete.useMutation({ + onSuccess: () => { + toast.success('Blog post deleted successfully') + router.push('/admin/blog') + router.refresh() + }, + onError: (error) => { + toast.error(`Error deleting blog post: ${error.message}`) + }, + }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!title.trim()) { + toast.error('Title is required') + return + } + + if (!content.trim()) { + toast.error('Content is required') + return + } + + setIsSubmitting(true) + + updatePost.mutate({ + id, + title, + content, + excerpt: excerpt || undefined, + published, + updateSlug, + }) + } + + const handleDelete = () => { + deletePost.mutate({ id }) + } + + if (isLoading || isFetching) { + return ( +
+

Edit Blog Post

+
+

Loading...

+
+
+ ) + } + + return ( +
+
+

Edit Blog Post

+ + + + + + + + Are you sure? + + This action cannot be undone. This will permanently delete the + blog post. + + + + Cancel + + Delete + + + + +
+ +
+
+ + setTitle(e.target.value)} + placeholder='Enter post title' + required + /> +
+ +
+ +