Complete guide to the projects (portfolio) admin panel, mirroring the product admin structure.
Projects rely on next-intl for translations. The title, description, and badge fields are translation keys.
src/app/
├── types/
│ └── project.ts # TypeScript types
├── server/projects/
│ ├── get-all-projects.ts # Active projects (public)
│ ├── get-all-projects-admin.ts # All projects (admin)
│ ├── get-project-by-id.ts # Fetch by Supabase ID
│ └── get-project-by-slug.ts # Fetch by slug
├── actions/projects/
│ ├── create-project.ts # Create project
│ ├── update-project.ts # Update project
│ ├── delete-project.ts # Soft/Hard delete
│ └── toggle-project-active.ts # Toggle active/inactive
├── [locale]/admin/projects/
│ ├── page.tsx # Project list
│ ├── new/page.tsx # Create new
│ └── [id]/page.tsx # Edit existing
└── components/admin/
├── project-form.tsx # Form
└── project-actions.tsx # Action buttonsinterface Project {
slug: string // Unique, URL-friendly ID
title: string // Translation key
description: string // Translation key
badge?: string // Translation key (opcional)
category: 'mobile' | 'front-end' | 'back-end' | 'full-stack' | 'ai'
tech: string[] // Technologies shown on the card
stack: string[] // Used for filters
link: string // External URL (GitHub, demo, etc.)
image?: string // Image URL
imagePath?: string // Path inside Supabase Storage
isActive: boolean // Visible on the public page
isFeatured: boolean // Featured project
order: number // Display order
createdAt: string
updatedAt: string
_supabaseId?: string // Internal ID (admin only)
}Projects use translation keys. For every project add translations in:
src/i18n/messages/
├── en.json
├── pt.json
└── es.json{
"Projects": {
"items": {
"meu-projeto": {
"title": "My Amazing Project",
"description": "A revolutionary web application...",
"badge": "Featured"
}
}
}
}title, description, and badge must match the project slug so the system can resolve the translations.
http://localhost:3000/en/admin/projectsimport { getAllProjectsAdmin } from '@/app/server/projects/get-all-projects-admin'
const projects = await getAllProjectsAdmin()import { createProject } from '@/app/actions/projects/create-project'
const result = await createProject({
slug: 'my-awesome-project',
title: 'my-awesome-project', // Translation key
description: 'my-awesome-project', // Translation key
badge: 'my-awesome-project', // Translation key (opcional)
category: 'front-end',
tech: ['React', 'TypeScript', 'Next.js'],
stack: ['React', 'TypeScript'],
link: 'https://github.com/user/my-awesome-project',
isActive: true,
isFeatured: false,
order: 0,
})en.json:
{
"Projects": {
"items": {
"my-awesome-project": {
"title": "My Awesome Project",
"description": "A revolutionary web application built with modern tech stack...",
"badge": "Featured"
}
}
}
}pt.json:
{
"Projects": {
"items": {
"my-awesome-project": {
"title": "My Amazing Project",
"description": "A revolutionary web app built with a modern stack...",
"badge": "Featured"
}
}
}
}es.json:
{
"Projects": {
"items": {
"my-awesome-project": {
"title": "My Amazing Project (ES)",
"description": "A revolutionary web app built with a modern stack (ES)...",
"badge": "Featured"
}
}
}
}Visit /projetos to see the project listed.
import { updateProject } from '@/app/actions/projects/update-project'
const result = await updateProject(supabaseId, {
order: 5,
isFeatured: true,
})// Soft delete (isActive = false)
import { deleteProject } from '@/app/actions/projects/delete-project'
const result = await deleteProject(supabaseId)
// Hard delete (remove from DB)
import { hardDeleteProject } from '@/app/actions/projects/delete-project'
const result = await hardDeleteProject(supabaseId)import { toggleProjectActive } from '@/app/actions/projects/toggle-project-active'
const result = await toggleProjectActive(supabaseId)import ProjectForm from '@/app/components/admin/project-form'
// Create
<ProjectForm mode="create" />
// Edit
<ProjectForm mode="edit" project={project} />import ProjectActions from '@/app/components/admin/project-actions'
<ProjectActions
projectId={project._supabaseId}
projectTitle={project.title}
isActive={project.isActive}
/>The /projetos page exposes two filter types:
stack field| Aspecto | Projects | Products |
|---|---|---|
| Focus | Portfolio/showcase | E-commerce/sales |
| Pricing | No | Yes |
| Author | No | Yes |
| Features | Tech stack | Feature list |
| Download | External link | Download URL |
| Translations | Yes (title, description, badge) | No |
| Categories | mobile/front-end/etc | landing-page/dashboard/etc |
To reorder projects in the list:
const projects = [
{ id: 'proj1', order: 0 },
{ id: 'proj2', order: 1 },
{ id: 'proj3', order: 2 },
]
for (const proj of projects) {
await updateProject(proj.id, { order: proj.order })
}await updateProject(projectId, { isFeatured: true })isActive: truerouter.refresh() after creatingtitle/description/badge matches the slugstack field is populated correctly