mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 17:18:09 +00:00
237 lines
6.7 KiB
TypeScript
237 lines
6.7 KiB
TypeScript
"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 = {
|
|
message: string | null;
|
|
errors: {
|
|
title?: string[];
|
|
slug?: string[];
|
|
content?: string[];
|
|
image?: string[];
|
|
tags?: string[];
|
|
excerpt?: string[];
|
|
_form?: string[];
|
|
};
|
|
previousInput?: {
|
|
title?: string;
|
|
slug?: string;
|
|
content?: string;
|
|
excerpt?: string;
|
|
tags?: string;
|
|
published?: boolean;
|
|
};
|
|
};
|
|
|
|
// 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}`);
|
|
}
|