From 9f3510df8f9274bf9798365c5077281a0374ac28 Mon Sep 17 00:00:00 2001 From: Yolando Date: Fri, 3 Apr 2026 17:54:02 +0700 Subject: [PATCH] feat: implement skill management system with database schema, server actions, and dynamic tech stack section --- messages/en.json | 6 +-- messages/id.json | 6 +-- .../migration.sql | 2 + prisma/schema.prisma | 1 + .../[locale]/admin/dashboard/skills/page.tsx | 21 +++++--- src/features/skills/actions.ts | 5 ++ src/features/skills/animated-tech-item.tsx | 53 +++++++++++++++++++ src/features/skills/skill-form.tsx | 14 +++++ src/features/skills/tech-stack-section.tsx | 30 +++-------- 9 files changed, 102 insertions(+), 36 deletions(-) create mode 100644 prisma/migrations/20260403104002_add_skill_sort_order/migration.sql create mode 100644 src/features/skills/animated-tech-item.tsx diff --git a/messages/en.json b/messages/en.json index 7f71d16..fd2b0ac 100644 --- a/messages/en.json +++ b/messages/en.json @@ -60,10 +60,10 @@ } }, "TechStack": { - "badge": "Tech Arsenal", - "title": "Technology Stack", + "badge": "Technologies", + "title": "Skills & Technologies", "seeMore": "See more", - "subtitle": "A comprehensive toolkit forged through years of enterprise development, from backend infrastructure to mobile interfaces.", + "subtitle": "The core technologies and tools I use to build robust and scalable enterprise applications.", "categories": { "backend": { "title": "Enterprise Backend", diff --git a/messages/id.json b/messages/id.json index 9a594b9..05d1722 100644 --- a/messages/id.json +++ b/messages/id.json @@ -60,10 +60,10 @@ } }, "TechStack": { - "badge": "Arsenal Teknologi", - "title": "Tech Stack", + "badge": "Teknologi", + "title": "Keahlian & Teknologi", "seeMore": "Lihat semua", - "subtitle": "Perangkat komprehensif yang ditempa dari pengalaman pengembangan enterprise, mulai dari infrastruktur backend hingga antarmuka mobile.", + "subtitle": "Kumpulan teknologi dan perangkat utama yang saya gunakan sehari-hari untuk mengembangkan aplikasi berskala enterprise.", "categories": { "backend": { "title": "Enterprise Backend", diff --git a/prisma/migrations/20260403104002_add_skill_sort_order/migration.sql b/prisma/migrations/20260403104002_add_skill_sort_order/migration.sql new file mode 100644 index 0000000..8df7701 --- /dev/null +++ b/prisma/migrations/20260403104002_add_skill_sort_order/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "skills" ADD COLUMN "sort_order" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e9bc09a..317b33b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,6 +38,7 @@ model Skill { name String iconName String? @map("icon_name") category String + sortOrder Int @default(0) @map("sort_order") projects ProjectSkill[] diff --git a/src/app/[locale]/admin/dashboard/skills/page.tsx b/src/app/[locale]/admin/dashboard/skills/page.tsx index f0f094c..9cabe64 100644 --- a/src/app/[locale]/admin/dashboard/skills/page.tsx +++ b/src/app/[locale]/admin/dashboard/skills/page.tsx @@ -29,7 +29,7 @@ export default async function AdminSkillsPage() { } const skills = await prisma.skill.findMany({ - orderBy: [{ category: "asc" }, { name: "asc" }], + orderBy: [{ category: "asc" }, { sortOrder: "asc" }, { name: "asc" }], }); return ( @@ -84,13 +84,18 @@ export default async function AdminSkillsPage() {

{skill.name}

- - {CATEGORY_LABELS[skill.category] ?? skill.category} - +
+ + {CATEGORY_LABELS[skill.category] ?? skill.category} + + + #{skill.sortOrder} + +
diff --git a/src/features/skills/actions.ts b/src/features/skills/actions.ts index 88d0ed9..dab256a 100644 --- a/src/features/skills/actions.ts +++ b/src/features/skills/actions.ts @@ -12,6 +12,7 @@ const skillSchema = z.object({ { message: "Invalid category" } ), iconName: z.string().optional(), + sortOrder: z.coerce.number().int().min(0).default(0), }); export async function createSkillAction(prevState: any, formData: FormData) { @@ -22,6 +23,7 @@ export async function createSkillAction(prevState: any, formData: FormData) { name: formData.get("name") as string, category: formData.get("category") as string, iconName: formData.get("iconName") as string, + sortOrder: formData.get("sortOrder") as string, }; const validation = skillSchema.safeParse(data); @@ -35,6 +37,7 @@ export async function createSkillAction(prevState: any, formData: FormData) { name: validation.data.name, category: validation.data.category, iconName: validation.data.iconName || null, + sortOrder: validation.data.sortOrder, }, }); revalidatePath("/admin/dashboard/skills"); @@ -58,6 +61,7 @@ export async function updateSkillAction( name: formData.get("name") as string, category: formData.get("category") as string, iconName: formData.get("iconName") as string, + sortOrder: formData.get("sortOrder") as string, }; const validation = skillSchema.safeParse(data); @@ -72,6 +76,7 @@ export async function updateSkillAction( name: validation.data.name, category: validation.data.category, iconName: validation.data.iconName || null, + sortOrder: validation.data.sortOrder, }, }); revalidatePath("/admin/dashboard/skills"); diff --git a/src/features/skills/animated-tech-item.tsx b/src/features/skills/animated-tech-item.tsx new file mode 100644 index 0000000..4ad6216 --- /dev/null +++ b/src/features/skills/animated-tech-item.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Code2 } from "lucide-react"; + +interface AnimatedTechItemProps { + name: string; + iconName: string | null; + index: number; +} + +export function AnimatedTechItem({ name, iconName, index }: AnimatedTechItemProps) { + return ( + + {iconName ? ( + + ) : ( + + )} + + {name} + + + ); +} diff --git a/src/features/skills/skill-form.tsx b/src/features/skills/skill-form.tsx index 4ad65e6..197bd12 100644 --- a/src/features/skills/skill-form.tsx +++ b/src/features/skills/skill-form.tsx @@ -144,6 +144,20 @@ export function SkillForm({ initialData, skillId }: { initialData?: any; skillId
+
+ + +
+
- {items.map((skill) => ( -
( + - {skill.iconName ? ( - {skill.name} - ) : ( - - )} - - {skill.name} - -
+ name={skill.name} + iconName={skill.iconName} + index={skillIndex} + /> ))}