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

237
actions/posts.ts Normal file
View File

@ -0,0 +1,237 @@
"use server";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { auth } from "@/auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { minioClient, bucketName } from "@/lib/minio"; // Import MinIO client
import { v4 as uuidv4 } from "uuid";
// Max file size (e.g., 5MB)
const MAX_FILE_SIZE = 5 * 1024 * 1024;
// Allowed image types
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
"image/gif",
];
// Update schema: image is now optional File
const PostSchema = z.object({
title: z.string().min(3, "Title must be at least 3 characters"),
slug: z
.string()
.min(3, "Slug must be at least 3 characters")
.regex(
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
"Slug must be lowercase alphanumeric with hyphens"
),
content: z.string().min(10, "Content is too short"),
excerpt: z.string().optional(),
// Validate the image file
image: z
.instanceof(File)
.optional()
.refine(
(file) => !file || file.size <= MAX_FILE_SIZE,
`Max image size is 5MB.`
)
.refine(
(file) => !file || ACCEPTED_IMAGE_TYPES.includes(file.type),
"Only .jpg, .jpeg, .png, .webp and .gif formats are supported."
),
published: z.boolean().optional(),
tags: z.string().optional(), // Handle tags if needed
});
export type CreatePostState = {
errors?: {
title?: string[];
slug?: string[];
content?: string[];
image?: string[]; // Changed from imageUrl
tags?: string[];
_form?: string[];
};
message?: string | null;
// Add a field to hold previous input values
previousInput?: {
title?: string;
slug?: string;
content?: string;
excerpt?: string;
tags?: string;
published?: boolean;
// We don't repopulate the file input for security reasons
};
};
// Helper function to upload to MinIO
async function uploadImageToMinio(file: File): Promise<string | null> {
try {
const fileExtension = file.name.split(".").pop();
const fileName = `${uuidv4()}.${fileExtension}`;
const buffer = Buffer.from(await file.arrayBuffer());
await minioClient.putObject(bucketName, fileName, buffer, buffer.length, {
"Content-Type": file.type,
});
const baseUrl = process.env.MINIO_BASE_URL?.replace(/\/+$/, "");
const publicUrl = `${baseUrl}/${bucketName}/${fileName}`;
return publicUrl;
} catch (error) {
console.error("MinIO Upload Error:", error);
return null;
}
}
export async function createPost(
prevState: CreatePostState,
formData: FormData
): Promise<CreatePostState> {
const session = await auth();
if (!session?.user?.id) {
// Check for user ID specifically
return { errors: { _form: ["Authentication required."] }, message: null };
}
// Store raw form data for potential error return
const rawFormData = {
title: formData.get("title")?.toString(),
slug: formData.get("slug")?.toString(),
content: formData.get("content")?.toString(),
excerpt: formData.get("excerpt")?.toString(),
tags: formData.get("tags")?.toString(),
published: formData.get("published") === "on",
image: formData.get("image"), // Keep image separate for validation
};
const imageFile = rawFormData.image;
const validatedFields = PostSchema.safeParse({
...rawFormData, // Spread raw data (excluding image)
// Pass the file object only if it's a File instance and has size > 0
image:
imageFile instanceof File && imageFile.size > 0 ? imageFile : undefined,
});
if (!validatedFields.success) {
console.error(
"Validation Errors:",
validatedFields.error.flatten().fieldErrors
);
// Return previous input along with errors
return {
errors: validatedFields.error.flatten().fieldErrors,
message: "Failed to create post due to validation errors.",
previousInput: {
// Exclude image file
title: rawFormData.title,
slug: rawFormData.slug,
content: rawFormData.content,
excerpt: rawFormData.excerpt,
tags: rawFormData.tags,
published: rawFormData.published,
},
};
}
const { title, slug, content, excerpt, image, published, tags } =
validatedFields.data;
// --- Handle Image Upload ---
let uploadedImageUrl: string | null = null;
if (image) {
uploadedImageUrl = await uploadImageToMinio(image);
if (!uploadedImageUrl) {
// Return previous input on image upload failure
return {
errors: { image: ["Failed to upload image."] },
message: null,
previousInput: {
title: rawFormData.title,
slug: rawFormData.slug,
content: rawFormData.content,
excerpt: rawFormData.excerpt,
tags: rawFormData.tags,
published: rawFormData.published,
},
};
}
}
// --- End Image Upload ---
// Check if slug is unique
const existingPost = await prisma.post.findUnique({ where: { slug } });
if (existingPost) {
// Return previous input on slug error
return {
errors: { slug: ["Slug already exists."] },
message: null,
previousInput: {
title: rawFormData.title,
slug: rawFormData.slug, // Keep the problematic slug to show the user
content: rawFormData.content,
excerpt: rawFormData.excerpt,
tags: rawFormData.tags,
published: rawFormData.published,
},
};
}
// --- Handle Tags ---
// Example: Split comma-separated tags and connect/create them
const tagConnections = tags
? await Promise.all(
tags
.split(",")
.map((tag) => tag.trim())
.filter(Boolean)
.map((tagName) =>
prisma.tag.upsert({
where: { name: tagName },
update: {},
create: { name: tagName },
})
)
)
: [];
// --- End Handle Tags ---
try {
await prisma.post.create({
data: {
title,
slug,
content,
excerpt: excerpt || content.substring(0, 150) + "...",
imageUrl: uploadedImageUrl, // Store the MinIO URL
published: published ?? false,
authorId: session.user.id,
tags: {
// Connect tags
connect: tagConnections.map((tag) => ({ id: tag.id })),
},
},
});
} catch (error) {
console.error("Database Error:", error);
// Return previous input on database error
return {
errors: { _form: ["Database Error: Failed to create post."] },
message: null,
previousInput: {
title: rawFormData.title,
slug: rawFormData.slug,
content: rawFormData.content,
excerpt: rawFormData.excerpt,
tags: rawFormData.tags,
published: rawFormData.published,
},
};
}
revalidatePath("/tech-talk");
redirect(`/tech-talk/${slug}`);
}