create post completed

This commit is contained in:
libertyoms
2025-04-21 20:59:37 +02:00
parent 629da1b7e7
commit a8c6b5297b
21 changed files with 1973 additions and 37 deletions

View File

@ -0,0 +1,243 @@
"use client";
// Import useActionState from react instead of useFormState from react-dom
import React, { useActionState } from "react";
import { useFormStatus } from "react-dom"; // Keep this for useFormStatus
import { createPost, CreatePostState } from "@/actions/posts";
import Button from "@/components/ui/Button";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" variant="primary" disabled={pending}>
{pending ? "Creating..." : "Create Post"}
</Button>
);
}
export default function CreatePostForm() {
const initialState: CreatePostState = { message: null, errors: {} };
// Use useActionState from React
const [state, dispatch] = useActionState(createPost, initialState);
// Helper to get default value or empty string
const getPreviousInput = (
key: keyof NonNullable<CreatePostState["previousInput"]>
) => state.previousInput?.[key] ?? "";
return (
// Remove encType="multipart/form-data"
<form action={dispatch} className="space-y-6">
{/* Title */}
<div>
<label
htmlFor="title"
className="block text-sm font-medium text-foreground mb-1"
>
Title
</label>
<input
type="text"
id="title"
name="title"
required
// Use previous input as default value
defaultValue={getPreviousInput("title")}
className="block w-full px-3 py-2 border border-border rounded-md shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
aria-describedby="title-error"
/>
<div id="title-error" aria-live="polite" aria-atomic="true">
{state.errors?.title &&
state.errors.title.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
</div>
</div>
{/* Slug */}
<div>
<label
htmlFor="slug"
className="block text-sm font-medium text-foreground mb-1"
>
Slug (URL Path)
</label>
<input
type="text"
id="slug"
name="slug"
required
pattern="[a-z0-9]+(?:-[a-z0-9]+)*"
title="Lowercase letters, numbers, and hyphens only (e.g., my-cool-post)"
// Use previous input as default value
defaultValue={getPreviousInput("slug")}
className="block w-full px-3 py-2 border border-border rounded-md shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
aria-describedby="slug-error"
/>
<p className="mt-1 text-xs text-muted-foreground">
Lowercase letters, numbers, and hyphens only (e.g., my-cool-post).
</p>
<div id="slug-error" aria-live="polite" aria-atomic="true">
{state.errors?.slug &&
state.errors.slug.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
</div>
</div>
{/* Content (Textarea - consider a Markdown editor later) */}
<div>
<label
htmlFor="content"
className="block text-sm font-medium text-foreground mb-1"
>
Content (Markdown supported)
</label>
<textarea
id="content"
name="content"
rows={10}
required
// Use previous input as default value
defaultValue={getPreviousInput("content")}
className="block w-full px-3 py-2 border border-border rounded-md shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
aria-describedby="content-error"
></textarea>
<div id="content-error" aria-live="polite" aria-atomic="true">
{state.errors?.content &&
state.errors.content.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
</div>
</div>
{/* Excerpt (Optional) */}
<div>
<label
htmlFor="excerpt"
className="block text-sm font-medium text-foreground mb-1"
>
Excerpt (Optional short summary)
</label>
<textarea
id="excerpt"
name="excerpt"
rows={3}
// Use previous input as default value
defaultValue={getPreviousInput("excerpt")}
className="block w-full px-3 py-2 border border-border rounded-md shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
aria-describedby="excerpt-error"
></textarea>
<div id="excerpt-error" aria-live="polite" aria-atomic="true">
{state.errors?.excerpt &&
state.errors.excerpt.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
</div>
</div>
{/* Image Upload (File Input) */}
<div>
<label
htmlFor="image"
className="block text-sm font-medium text-foreground mb-1"
>
Featured Image (Optional, Max 5MB)
</label>
<input
type="file" // Changed type to file
id="image"
name="image"
className="block w-full text-sm text-muted-foreground file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-secondary file:text-secondary-foreground hover:file:bg-secondary/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
aria-describedby="image-error"
accept="image/jpeg, image/png, image/webp, image/gif" // Specify accepted types
/>
<div id="image-error" aria-live="polite" aria-atomic="true">
{state.errors?.image &&
state.errors.image.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
</div>
</div>
{/* Tags Input (Optional) */}
<div>
<label
htmlFor="tags"
className="block text-sm font-medium text-foreground mb-1"
>
Tags (Optional, comma-separated)
</label>
<input
type="text"
id="tags"
name="tags"
// Use previous input as default value
defaultValue={getPreviousInput("tags")}
className="block w-full px-3 py-2 border border-border rounded-md shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
aria-describedby="tags-error"
placeholder="e.g., cloud, ai, development"
/>
<div id="tags-error" aria-live="polite" aria-atomic="true">
{state.errors?.tags &&
state.errors.tags.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
</div>
</div>
{/* Published Checkbox */}
<div className="flex items-center">
<input
id="published"
name="published"
type="checkbox"
// Use previous input as default checked state
defaultChecked={state.previousInput?.published ?? false}
className="h-4 w-4 text-primary border-border rounded focus:ring-primary"
/>
<label
htmlFor="published"
className="ml-2 block text-sm text-foreground"
>
Publish immediately
</label>
</div>
{/* General Form Error */}
<div id="form-error" aria-live="polite" aria-atomic="true">
{state.errors?._form &&
state.errors._form.map((error: string) => (
<p className="mt-1 text-sm text-destructive" key={error}>
{error}
</p>
))}
{state.message && !state.errors?._form && (
// Display general message if no specific form error
<p
className={`mt-1 text-sm ${
state.errors ? "text-destructive" : "text-green-600"
}`}
>
{state.message}
</p>
)}
</div>
{/* Submit Button */}
<SubmitButton />
</form>
);
}