mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 18:58:10 +00:00
create post completed
This commit is contained in:
237
actions/posts.ts
Normal file
237
actions/posts.ts
Normal 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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user