feat: implement full CRUD functionality for projects with image upload support and admin dashboard management

This commit is contained in:
Yolando
2026-03-28 21:11:36 +07:00
parent 0549f12a97
commit 01ecca4b28
17 changed files with 2399 additions and 6 deletions

View File

@@ -1,7 +1,8 @@
import { verifySession, clearSession } from "@/core/security/session";
import { redirect } from "@/i18n/routing";
import { Link, redirect } from "@/i18n/routing";
import { getLocale } from "next-intl/server";
import { LogOut } from "lucide-react";
import { LogOut, ArrowRight } from "lucide-react";
import { prisma } from "@/core/db/prisma";
export default async function DashboardPage() {
const session = await verifySession();
@@ -41,11 +42,14 @@ export default async function DashboardPage() {
<main className="max-w-7xl mx-auto px-6 py-12">
<div className="grid gap-6 md:grid-cols-3">
{/* Card: Projects */}
<div className="p-6 rounded-2xl bg-card border border-border/50 shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-lg font-bold mb-2">Projects</h2>
<Link href="/admin/dashboard/projects" className="p-6 rounded-2xl bg-card border border-border/50 shadow-sm hover:shadow-md hover:border-accent/40 hover:-translate-y-1 transition-all group block">
<div className="flex justify-between items-start mb-2">
<h2 className="text-lg font-bold">Projects</h2>
<ArrowRight size={18} className="text-muted-foreground group-hover:text-accent transition-colors" />
</div>
<p className="text-sm text-muted-foreground mb-4">Manage portfolio projects and case studies.</p>
<div className="text-3xl font-mono font-bold text-accent">--</div>
</div>
<div className="text-3xl font-mono font-bold text-accent">Go &rarr;</div>
</Link>
{/* Card: Skills */}
<div className="p-6 rounded-2xl bg-card border border-border/50 shadow-sm hover:shadow-md transition-shadow">

View File

@@ -0,0 +1,33 @@
import { ProjectForm } from "@/features/projects/project-form";
import { verifySession } from "@/core/security/session";
import { redirect } from "@/i18n/routing";
import { getLocale } from "next-intl/server";
import { prisma } from "@/core/db/prisma";
import { notFound } from "next/navigation";
export default async function EditProjectPage({
params
}: {
params: Promise<{ id: string }>
}) {
const session = await verifySession();
const locale = await getLocale();
if (!session) {
redirect({ href: "/admin/login", locale });
}
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id }
});
if (!project) return notFound();
return (
<div className="min-h-screen bg-muted/10 px-6 py-12">
<ProjectForm initialData={project} projectId={project.id} />
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { ProjectForm } from "@/features/projects/project-form";
import { verifySession } from "@/core/security/session";
import { redirect } from "@/i18n/routing";
import { getLocale } from "next-intl/server";
export default async function CreateProjectPage() {
const session = await verifySession();
const locale = await getLocale();
if (!session) {
redirect({ href: "/admin/login", locale });
}
return (
<div className="min-h-screen bg-muted/10 px-6 py-12">
<ProjectForm />
</div>
);
}

View File

@@ -0,0 +1,115 @@
import { prisma } from "@/core/db/prisma";
import { verifySession } from "@/core/security/session";
import { Link, redirect } from "@/i18n/routing";
import { getLocale } from "next-intl/server";
import { Plus, Pencil, ArrowLeft } from "lucide-react";
import { DeleteProjectButton } from "@/features/projects/delete-button";
import { TogglePublishButton } from "@/features/projects/toggle-button";
export default async function AdminProjectsPage() {
const session = await verifySession();
const locale = await getLocale();
if (!session) {
redirect({ href: "/admin/login", locale });
}
const projects = await prisma.project.findMany({
orderBy: { createdAt: 'desc' }
});
return (
<div className="min-h-screen bg-muted/10 p-6 lg:p-12">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<Link
href="/admin/dashboard"
className="p-2 rounded-full hover:bg-muted text-muted-foreground transition-colors"
>
<ArrowLeft size={20} />
</Link>
<div>
<h1 className="text-3xl font-bold tracking-tight">Manage Projects</h1>
<p className="text-sm text-muted-foreground mt-1">Add, update, or remove portfolio case studies.</p>
</div>
</div>
<Link
href="/admin/dashboard/projects/create"
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-foreground text-background font-semibold text-sm shadow-md hover:scale-105 transition-all"
>
<Plus size={16} />
New Project
</Link>
</div>
<div className="bg-card border border-border/50 rounded-2xl shadow-sm overflow-hidden">
{projects.length === 0 ? (
<div className="p-12 text-center flex flex-col items-center">
<div className="text-muted-foreground mb-4 opacity-50">
<Plus size={48} />
</div>
<h3 className="text-lg font-bold">No projects yet</h3>
<p className="text-sm text-muted-foreground mb-6">Get started by creating your first portfolio case study.</p>
<Link
href="/admin/dashboard/projects/create"
className="px-6 py-2 rounded-xl border border-border hover:bg-muted/50 transition-colors font-medium text-sm"
>
Create Project
</Link>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-border/50 bg-muted/20 text-muted-foreground text-sm uppercase tracking-wider">
<th className="p-4 font-semibold">Title</th>
<th className="p-4 font-semibold">Category</th>
<th className="p-4 font-semibold">Status</th>
<th className="p-4 font-semibold">Date Added</th>
<th className="p-4 font-semibold text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{projects.map((project: any) => (
<tr key={project.id} className="hover:bg-muted/10 transition-colors">
<td className="p-4 font-medium flex items-center gap-3">
{project.imageUrl && (
<div className="w-10 h-10 rounded overflow-hidden aspect-square flex-shrink-0 bg-muted">
<img src={project.imageUrl} alt={project.title} className="w-full h-full object-cover" />
</div>
)}
<div>
<div className="font-semibold">{project.title}</div>
<div className="font-mono text-xs text-muted-foreground truncate max-w-[200px]">{project.slug}</div>
</div>
</td>
<td className="p-4 text-sm text-muted-foreground">{project.category}</td>
<td className="p-4">
<TogglePublishButton id={project.id} isPublished={project.isPublished} />
</td>
<td className="p-4 text-sm text-muted-foreground font-mono">
{new Date(project.createdAt).toLocaleDateString()}
</td>
<td className="p-4 text-right">
<div className="flex justify-end gap-2">
<Link
href={`/admin/dashboard/projects/${project.id}/edit`}
className="p-2 text-muted-foreground hover:text-accent hover:bg-accent/10 rounded-lg transition-colors"
>
<Pencil size={18} />
</Link>
<DeleteProjectButton id={project.id} />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
}