feat: implement full CRUD functionality for projects with image upload support and admin dashboard management
This commit is contained in:
@@ -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 →</div>
|
||||
</Link>
|
||||
|
||||
{/* Card: Skills */}
|
||||
<div className="p-6 rounded-2xl bg-card border border-border/50 shadow-sm hover:shadow-md transition-shadow">
|
||||
|
||||
33
src/app/[locale]/admin/dashboard/projects/[id]/edit/page.tsx
Normal file
33
src/app/[locale]/admin/dashboard/projects/[id]/edit/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
src/app/[locale]/admin/dashboard/projects/create/page.tsx
Normal file
19
src/app/[locale]/admin/dashboard/projects/create/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
115
src/app/[locale]/admin/dashboard/projects/page.tsx
Normal file
115
src/app/[locale]/admin/dashboard/projects/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user