From 7242579c1739f8bd5c48ac56a1cd5a7a9078bae3 Mon Sep 17 00:00:00 2001 From: Alex Muszynski Date: Tue, 24 Jun 2025 20:38:24 -0400 Subject: [PATCH] feat: ability to update blog --- frontend/Dockerfile | 8 +-- frontend/nginx.conf | 28 +++++++++ frontend/src/App.tsx | 3 +- frontend/src/utils/BlogViewer.tsx | 18 ++++-- frontend/src/utils/EditBlog.tsx | 95 +++++++++++++++++++++++++++++++ frontend/src/utils/types.ts | 3 +- 6 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 frontend/nginx.conf create mode 100644 frontend/src/utils/EditBlog.tsx diff --git a/frontend/Dockerfile b/frontend/Dockerfile index edc8ccf..4321151 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,18 +1,14 @@ # Build stage FROM node:20 AS build - WORKDIR /app - -COPY package.json package-lock.json ./ +COPY package*.json ./ RUN npm install - COPY . . - RUN npm run build # Production stage FROM nginx:alpine - +COPY nginx.conf /etc/nginx/nginx.conf COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..3f8a88c --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,28 @@ +worker_processes 1; + +events { worker_connections 1024; } + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + # try to serve file directly, otherwise fallback to index.html + try_files $uri $uri/ /index.html; + } + + # optional: block .git, .env, etc + location ~ /\.(?!well-known).* { + deny all; + } + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 929020b..fb11991 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,7 @@ import { useForm } from "react-hook-form"; import { BlogViewer } from "./utils/BlogViewer"; import { RequireAdmin } from "./utils/RouteGuard"; import { AdminPage } from "./utils/AdminPage"; +import { EditBlog } from "./utils/EditBlog"; import Unauthorized from "./utils/UnauthorizedPage"; import { FaGithub, @@ -141,7 +142,6 @@ function CreateBlog() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Stubbed API call try { const body = { title: title, @@ -277,6 +277,7 @@ function App() { } /> } /> } /> + } /> } /> } /> (); const [blog, setBlog] = useState(null); + const navigate = useNavigate(); + const me = localStorage.getItem("user_id"); useEffect(() => { if (!slug) return; @@ -20,12 +22,20 @@ export function BlogViewer() { .catch((err) => console.error(err)); }, [slug]); - if (!blog) { - return

Loading...

; - } + if (!blog) return

Loading…

; + + const isAuthor = me === String(blog.author_id); return (
+ {isAuthor && ( + + )}

{blog.title.toUpperCase()}

By User {blog.author_id}

diff --git a/frontend/src/utils/EditBlog.tsx b/frontend/src/utils/EditBlog.tsx new file mode 100644 index 0000000..8850bee --- /dev/null +++ b/frontend/src/utils/EditBlog.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState, type FormEvent } from "react"; + +import { useParams, useNavigate } from "react-router-dom"; +import { API_URL } from "./constants"; +import type { Blog } from "./types"; +import { countWords } from "./countWords"; + +export function EditBlog() { + const { slug } = useParams<{ slug: string }>(); + const [blog, setBlog] = useState(undefined); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [body, setBody] = useState(""); + const navigate = useNavigate(); + + useEffect(() => { + if (!slug) return; + fetch(`${API_URL}/blogs/${slug}`) + .then((res) => res.json()) + .then((data: Blog) => { + setBlog(data); + setTitle(data.title); + setDescription(data.description); + setBody(data.body); + }) + .catch(console.error); + }, [slug]); + + if (!blog) return

Loading…

; + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + + const updatedBlog = { + title, + description, + body, + updated_at: new Date().toISOString(), + word_count: countWords(body), + version: blog ? blog.version : 1, + }; + + const res = await fetch(`${API_URL}/blogs/${slug}`, { + method: "PUT", // or "PATCH" if your API prefers + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updatedBlog), + }); + + if (res.ok) { + navigate(`/blog/${slug}`); // go back to the viewer + } else { + console.error("Update failed:", await res.text()); + alert("Could not update the post. See console for details."); + } + } + + return ( +
+

Edit Blog Post

+
+
+ + setTitle(e.target.value)} + className="w-full border px-2 py-1 rounded" + /> +
+
+ +