From 239468738f3f075ba3de268103fdd4d41ceff8a6 Mon Sep 17 00:00:00 2001 From: barrera77 Date: Fri, 15 Aug 2025 05:31:27 -0600 Subject: [PATCH 1/2] Added logic for creating posts --- src/api/axiosConfig.js | 2 +- src/api/postsApi.js | 10 ++ src/hooks/useCreatePost.js | 49 +++++++++ src/pages/posts/index.jsx | 14 ++- src/pages/posts/newPost.jsx | 206 ++++++++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useCreatePost.js create mode 100644 src/pages/posts/newPost.jsx diff --git a/src/api/axiosConfig.js b/src/api/axiosConfig.js index bd45cb2..ea5c132 100644 --- a/src/api/axiosConfig.js +++ b/src/api/axiosConfig.js @@ -1,7 +1,7 @@ import axios from "axios" import { isHyperlink } from '@/lib/isHyperlink' -const BASE_URL = process.env.DOTNET_SERVER_URL +const BASE_URL = process.env.NEXT_PUBLIC_DOTNET_SERVER_URL const AXIOS_BASE = axios.create({ baseURL: BASE_URL, diff --git a/src/api/postsApi.js b/src/api/postsApi.js index 54f8964..655c023 100644 --- a/src/api/postsApi.js +++ b/src/api/postsApi.js @@ -21,3 +21,13 @@ export const getPost = (postSlug) => { return {} } } + +export const createPost = async (postData) => { + try { + const res = await API.post('/posts/', postData) + return res.data + } catch (e) { + console.error('Failed to create post:', e) + throw e + } +} diff --git a/src/hooks/useCreatePost.js b/src/hooks/useCreatePost.js new file mode 100644 index 0000000..ac6a6c8 --- /dev/null +++ b/src/hooks/useCreatePost.js @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; + +export function useCreatePost() { + const router = useRouter(); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(''); + + const createPost = async (payload) => { + setError(''); + if (!payload.title || !payload.body || !payload.id || !payload.slug || !payload.createdDate) { + setError('All fields are required.'); + return null; + } + + setSubmitting(true); + + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_DOTNET_SERVER_URL}/posts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (res.status === 201) { + // Post created successfully, just redirect to the list page + router.push('/posts'); + return null; + } + + if (res.status === 400) { + const data = await res.json(); + setError(Array.isArray(data?.errors) ? data.errors.join(', ') : 'Validation failed.'); + return null; + } + + setError(`Unexpected error: ${res.status}`); + return null; + } catch (err) { + console.error(err); + setError('Network error. Is the API running?'); + return null; + } finally { + setSubmitting(false); + } + }; + + return { createPost, submitting, error }; +} diff --git a/src/pages/posts/index.jsx b/src/pages/posts/index.jsx index c0daf46..cdaa63a 100644 --- a/src/pages/posts/index.jsx +++ b/src/pages/posts/index.jsx @@ -4,10 +4,12 @@ import { Card } from '@/components/Card' import { SimpleLayout } from '@/components/SimpleLayout' import { formatDate } from '@/lib/formatDate' import { getPosts, getPost } from "@/api/postsApi" +import Link from 'next/link' +import { useUser } from '@auth0/nextjs-auth0/client' function Post({ post }) { const date = new Date(post.createdDate) - + return (
@@ -50,8 +52,16 @@ export default function PostsIndex({ posts }) { title="Writing on software design, company building, and the aerospace industry." intro="All of my long-form thoughts on programming, leadership, product design, and more, collected in chronological order." > + + Create New Post + +
-
+
+ {posts.map((post) => ( ))} diff --git a/src/pages/posts/newPost.jsx b/src/pages/posts/newPost.jsx new file mode 100644 index 0000000..c35e4f1 --- /dev/null +++ b/src/pages/posts/newPost.jsx @@ -0,0 +1,206 @@ +// pages/posts/new.jsx +import Head from 'next/head' +import { useRouter } from 'next/router' +import { useUser } from '@auth0/nextjs-auth0/client' +import { useEffect, useMemo, useState } from 'react' + +import { SimpleLayout } from '@/components/SimpleLayout' +import { Card } from '@/components/Card' +import { createPost } from '@/api/postsApi' +import { useCreatePost } from '@/hooks/useCreatePost'; + +// util to make a slug from a title +function toSlug(s) { + return (s || '') + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') +} + +// convert Date -> "YYYY-MM-DDTHH:mm" for datetime-local input +function toLocalDatetimeInputValue(date) { + const pad = (n) => String(n).padStart(2, '0') + const y = date.getFullYear() + const m = pad(date.getMonth() + 1) + const d = pad(date.getDate()) + const hh = pad(date.getHours()) + const mm = pad(date.getMinutes()) + return `${y}-${m}-${d}T${hh}:${mm}` +} + +export default function NewPost() { + const router = useRouter() + const { user, isLoading } = useUser() + + const [id, setId] = useState('') + const [title, setTitle] = useState('') + const [slug, setSlug] = useState('') + const [body, setBody] = useState('') + const [createDate, setCreateDate] = useState(toLocalDatetimeInputValue(new Date())) + + useEffect(() => { + try { + const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) + ? crypto.randomUUID() + : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + setId(uuid) + } catch { + setId(`${Date.now()}-${Math.random().toString(16).slice(2)}`) + } + }, []) + + const [slugTouched, setSlugTouched] = useState(false) + useEffect(() => { + if (!slugTouched) setSlug(toSlug(title)) + }, [title, slugTouched]) + + const isValid = useMemo(() => { + return title.trim() && (slug || toSlug(title)) && body.trim() && id && createDate + }, [title, slug, body, id, createDate]) + + function toIsoFromLocal(datetimeLocal) { + const d = new Date(datetimeLocal) + return d.toISOString() + } +const { createPost, submitting, error } = useCreatePost(); +const onSubmit = async (e) => { + e.preventDefault(); + + const payload = { + id, + title: title.trim(), + slug: slug.trim() || toSlug(title), + body: body.trim(), + createdDate: toIsoFromLocal(createDate), + }; + + await createPost(payload); +}; + + if (isLoading) { + return ( + +

Loading…

+
+ ) + } + + return ( + <> + + New Post + + + + +
+ + Create Post +
+ {/* ID (UUID) */} +
+ + setId(e.target.value)} + className="mt-2 w-full rounded-md border border-zinc-300 px-3 py-2" + required + /> +

+ Auto-generated. You can replace with your own UUID if needed. +

+
+ + {/* Title */} +
+ + setTitle(e.target.value)} + className="mt-2 w-full rounded-md border border-zinc-300 px-3 py-2" + placeholder="Understanding C# class" + required + /> +
+ + {/* Slug */} +
+ + { setSlug(e.target.value); setSlugTouched(true) }} + onBlur={() => setSlug(toSlug(slug))} + className="mt-2 w-full rounded-md border border-zinc-300 px-3 py-2" + placeholder="understanding-csharp-class" + required + /> +

+ Auto-derives from the title; you can edit it. +

+
+ + {/* Body */} +
+ +