feat: Add dashboard for managing portfolio content (blog, experience, projects) and restructure public portfolio routes.

This commit is contained in:
Moh Dzulfikri Maulana
2026-03-09 05:37:58 +07:00
parent dca848666e
commit e25416f3db
31 changed files with 2591 additions and 35 deletions

86
src/lib/actions/blog.ts Normal file
View File

@@ -0,0 +1,86 @@
"use server";
import { prisma } from "@/lib/prisma";
import { BlogStatus } from "@prisma/client";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const BlogSchema = z.object({
title: z.string().min(1, "Title is required"),
slug: z.string().min(1, "Slug is required"),
content: z.string().min(1, "Content is required"),
excerpt: z.string().optional(),
status: z.nativeEnum(BlogStatus),
publishedAt: z.coerce.date().nullable().optional(),
tags: z.array(z.string()).optional(),
});
export async function createBlog(data: z.infer<typeof BlogSchema>) {
const validated = BlogSchema.parse(data);
const blog = await prisma.blog.create({
data: {
title: validated.title,
slug: validated.slug,
content: validated.content,
excerpt: validated.excerpt,
status: validated.status,
publishedAt: validated.status === BlogStatus.PUBLISHED ? new Date() : null,
tags: {
connectOrCreate: validated.tags?.map((tag) => ({
where: { name: tag },
create: { name: tag },
})),
},
},
});
revalidatePath("/dashboard/blog");
revalidatePath("/blog");
return blog;
}
export async function updateBlog(id: string, data: z.infer<typeof BlogSchema>) {
const validated = BlogSchema.parse(data);
const blog = await prisma.blog.update({
where: { id },
data: {
title: validated.title,
slug: validated.slug,
content: validated.content,
excerpt: validated.excerpt,
status: validated.status,
publishedAt: validated.status === BlogStatus.PUBLISHED ? new Date() : null,
tags: {
set: [], // Clear existing tags
connectOrCreate: validated.tags?.map((tag) => ({
where: { name: tag },
create: { name: tag },
})),
},
},
});
revalidatePath("/dashboard/blog");
revalidatePath("/blog");
revalidatePath(`/blog/${blog.slug}`);
return blog;
}
export async function deleteBlog(id: string) {
const blog = await prisma.blog.delete({
where: { id },
});
revalidatePath("/dashboard/blog");
revalidatePath("/blog");
return blog;
}
export async function getBlogs() {
return await prisma.blog.findMany({
orderBy: { createdAt: "desc" },
include: { tags: true },
});
}

View File

@@ -0,0 +1,58 @@
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const ExperienceSchema = z.object({
company: z.string().min(1, "Company is required"),
role: z.string().min(1, "Role is required"),
startDate: z.coerce.date(),
endDate: z.coerce.date().nullable().optional(),
current: z.boolean().default(false),
description: z.string().min(1, "Description is required"),
highlights: z.array(z.string()).optional().default([]),
techStack: z.array(z.string()).optional().default([]),
order: z.coerce.number().default(0),
});
export async function createExperience(data: z.infer<typeof ExperienceSchema>) {
const validated = ExperienceSchema.parse(data);
const experience = await prisma.experience.create({
data: validated,
});
revalidatePath("/dashboard/experience");
revalidatePath("/experience");
return experience;
}
export async function updateExperience(id: string, data: z.infer<typeof ExperienceSchema>) {
const validated = ExperienceSchema.parse(data);
const experience = await prisma.experience.update({
where: { id },
data: validated,
});
revalidatePath("/dashboard/experience");
revalidatePath("/experience");
return experience;
}
export async function deleteExperience(id: string) {
const experience = await prisma.experience.delete({
where: { id },
});
revalidatePath("/dashboard/experience");
revalidatePath("/experience");
return experience;
}
export async function getExperiences() {
return await prisma.experience.findMany({
orderBy: { startDate: "desc" },
});
}

View File

@@ -0,0 +1,57 @@
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const ProjectSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
imageUrl: z.string().optional().nullable(),
liveUrl: z.string().optional().nullable(),
githubUrl: z.string().optional().nullable(),
techStack: z.array(z.string()).min(1, "At least one tech stack is required"),
featured: z.boolean().default(false),
order: z.coerce.number().default(0),
});
export async function createProject(data: z.infer<typeof ProjectSchema>) {
const validated = ProjectSchema.parse(data);
const project = await prisma.project.create({
data: validated,
});
revalidatePath("/dashboard/projects");
revalidatePath("/projects");
return project;
}
export async function updateProject(id: string, data: z.infer<typeof ProjectSchema>) {
const validated = ProjectSchema.parse(data);
const project = await prisma.project.update({
where: { id },
data: validated,
});
revalidatePath("/dashboard/projects");
revalidatePath("/projects");
return project;
}
export async function deleteProject(id: string) {
const project = await prisma.project.delete({
where: { id },
});
revalidatePath("/dashboard/projects");
revalidatePath("/projects");
return project;
}
export async function getProjects() {
return await prisma.project.findMany({
orderBy: [{ order: "asc" }, { createdAt: "desc" }],
});
}