Tutorial Membuat Portfolio Developer dengan Next.js dan Shadcn UI - Nayaka Yoga Pradipta

Tutorial Membuat Portfolio Developer dengan Next.js dan Shadcn UI

Senin, 22 Des 2025

Memiliki portfolio yang profesional adalah keharusan bagi setiap developer. Dalam tutorial ini, saya akan memandu kamu membuat portfolio modern menggunakan Next.js dan Shadcn UI—library komponen yang customizable, accessible, dan tanpa runtime overhead.

Kenapa Shadcn UI?

Shadcn UI berbeda dari component library lainnya:

  • Customizable - Kamu memiliki kode komponennya, bukan dependency
  • Accessible - Dibangun di atas Radix UI primitives
  • No Runtime - Tidak ada bundle size tambahan dari library
  • Beautiful by Default - Design system yang sudah teruji

Apa yang Akan Kita Bangun?

Portfolio dengan fitur:

  • Hero section dengan animasi
  • Projects/Portfolio showcase
  • About section
  • Contact form
  • Dark mode toggle
  • Responsive design

Prasyarat

  • Node.js versi 18 atau lebih baru
  • Text editor (VS Code recommended)
  • Pengetahuan dasar React dan TypeScript

Step 1: Setup Project Next.js

Buka terminal dan jalankan command berikut:

npx create-next-app@latest my-portfolio
cd my-portfolio

Saat ditanya, pilih opsi berikut:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes
  • Import alias: @/*

Step 2: Install dan Konfigurasi Shadcn UI

Jalankan command untuk inisialisasi Shadcn UI:

npx shadcn@latest init

Pilih konfigurasi berikut:

  • Style: Default
  • Base color: Slate
  • CSS variables: Yes

Sekarang install komponen yang kita butuhkan:

npx shadcn@latest add button card input textarea badge navigation-menu

Step 3: Struktur Folder Portfolio

Buat struktur folder berikut untuk mengorganisir project:

src/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── globals.css
├── components/
│   ├── layout/
│   │   ├── header.tsx
│   │   └── footer.tsx
│   ├── sections/
│   │   ├── hero.tsx
│   │   ├── projects.tsx
│   │   ├── about.tsx
│   │   └── contact.tsx
│   ├── ui/
│   │   └── (shadcn components)
│   └── theme-provider.tsx
├── lib/
│   └── utils.ts
└── data/
    └── projects.ts

Buat folder yang diperlukan:

mkdir -p src/components/layout src/components/sections src/data

Step 4: Setup Theme Provider untuk Dark Mode

Install next-themes untuk dark mode:

npm install next-themes

Buat file src/components/theme-provider.tsx:

"use client"

import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes"

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Update src/app/layout.tsx:

import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { Header } from "@/components/layout/header"
import { Footer } from "@/components/layout/footer"

const inter = Inter({ subsets: ["latin"] })

export const metadata: Metadata = {
  title: "John Doe - Full Stack Developer",
  description: "Portfolio of John Doe, a passionate full stack developer",
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="id" suppressHydrationWarning>
      <body className={inter.className}>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <Header />
          <main>{children}</main>
          <Footer />
        </ThemeProvider>
      </body>
    </html>
  )
}

Step 5: Membuat Header dengan Theme Toggle

Install komponen dropdown menu dan icon:

npx shadcn@latest add dropdown-menu
npm install lucide-react

Buat file src/components/layout/header.tsx:

"use client"

import Link from "next/link"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Moon, Sun, Menu } from "lucide-react"
import { useTheme } from "next-themes"

export function Header() {
  const { setTheme } = useTheme()

  return (
    <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
      <div className="container flex h-16 items-center justify-between">
        <Link href="/" className="font-bold text-xl">
          JohnDoe
        </Link>

        <nav className="hidden md:flex items-center gap-6">
          <Link href="#projects" className="text-sm font-medium hover:text-primary">
            Projects
          </Link>
          <Link href="#about" className="text-sm font-medium hover:text-primary">
            About
          </Link>
          <Link href="#contact" className="text-sm font-medium hover:text-primary">
            Contact
          </Link>
        </nav>

        <div className="flex items-center gap-2">
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button variant="ghost" size="icon">
                <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
                <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
                <span className="sr-only">Toggle theme</span>
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              <DropdownMenuItem onClick={() => setTheme("light")}>
                Light
              </DropdownMenuItem>
              <DropdownMenuItem onClick={() => setTheme("dark")}>
                Dark
              </DropdownMenuItem>
              <DropdownMenuItem onClick={() => setTheme("system")}>
                System
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
      </div>
    </header>
  )
}

Step 6: Membuat Hero Section

Portfolio Hero Section

Buat file src/components/sections/hero.tsx:

import { Button } from "@/components/ui/button"
import { ArrowRight, Github, Linkedin, Twitter } from "lucide-react"
import Link from "next/link"

export function Hero() {
  return (
    <section className="container py-24 md:py-32">
      <div className="flex flex-col items-center text-center gap-8">
        <div className="space-y-4">
          <h1 className="text-4xl md:text-6xl font-bold tracking-tight">
            Hi, I'm <span className="text-primary">John Doe</span>
          </h1>
          <p className="text-xl md:text-2xl text-muted-foreground max-w-2xl">
            Full Stack Developer yang passionate dalam membangun 
            aplikasi web modern dan user-friendly.
          </p>
        </div>

        <div className="flex gap-4">
          <Button asChild size="lg">
            <Link href="#projects">
              Lihat Projects <ArrowRight className="ml-2 h-4 w-4" />
            </Link>
          </Button>
          <Button variant="outline" size="lg" asChild>
            <Link href="#contact">Hubungi Saya</Link>
          </Button>
        </div>

        <div className="flex gap-4 mt-4">
          <Button variant="ghost" size="icon" asChild>
            <Link href="https://github.com" target="_blank">
              <Github className="h-5 w-5" />
            </Link>
          </Button>
          <Button variant="ghost" size="icon" asChild>
            <Link href="https://linkedin.com" target="_blank">
              <Linkedin className="h-5 w-5" />
            </Link>
          </Button>
          <Button variant="ghost" size="icon" asChild>
            <Link href="https://twitter.com" target="_blank">
              <Twitter className="h-5 w-5" />
            </Link>
          </Button>
        </div>
      </div>
    </section>
  )
}

Step 7: Membuat Projects Section

Portfolio Projects Grid

Pertama, buat data projects di src/data/projects.ts:

export const projects = [
  {
    id: 1,
    title: "E-Commerce Platform",
    description: "Full-stack e-commerce dengan Next.js, Prisma, dan Stripe payment integration.",
    image: "/images/projects/ecommerce.jpg",
    tags: ["Next.js", "Prisma", "Stripe", "PostgreSQL"],
    github: "https://github.com",
    demo: "https://demo.com",
  },
  {
    id: 2,
    title: "Task Management App",
    description: "Aplikasi manajemen tugas real-time dengan drag and drop functionality.",
    image: "/images/projects/taskapp.jpg",
    tags: ["React", "Node.js", "Socket.io", "MongoDB"],
    github: "https://github.com",
    demo: "https://demo.com",
  },
  {
    id: 3,
    title: "AI Chat Application",
    description: "Chat application dengan AI assistant menggunakan OpenAI API.",
    image: "/images/projects/aichat.jpg",
    tags: ["Next.js", "OpenAI", "Vercel AI SDK", "Tailwind"],
    github: "https://github.com",
    demo: "https://demo.com",
  },
]

Buat file src/components/sections/projects.tsx:

import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Github, ExternalLink } from "lucide-react"
import { projects } from "@/data/projects"
import Image from "next/image"
import Link from "next/link"

export function Projects() {
  return (
    <section id="projects" className="container py-24">
      <div className="space-y-4 text-center mb-12">
        <h2 className="text-3xl md:text-4xl font-bold">Projects</h2>
        <p className="text-muted-foreground max-w-2xl mx-auto">
          Beberapa project yang telah saya kerjakan
        </p>
      </div>

      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        {projects.map((project) => (
          <Card key={project.id} className="overflow-hidden group">
            <div className="relative h-48 overflow-hidden">
              <Image
                src={project.image}
                alt={project.title}
                fill
                className="object-cover transition-transform group-hover:scale-105"
              />
            </div>
            <CardHeader>
              <CardTitle>{project.title}</CardTitle>
              <CardDescription>{project.description}</CardDescription>
            </CardHeader>
            <CardContent>
              <div className="flex flex-wrap gap-2">
                {project.tags.map((tag) => (
                  <Badge key={tag} variant="secondary">
                    {tag}
                  </Badge>
                ))}
              </div>
            </CardContent>
            <CardFooter className="gap-2">
              <Button variant="outline" size="sm" asChild>
                <Link href={project.github} target="_blank">
                  <Github className="h-4 w-4 mr-2" /> Code
                </Link>
              </Button>
              <Button size="sm" asChild>
                <Link href={project.demo} target="_blank">
                  <ExternalLink className="h-4 w-4 mr-2" /> Demo
                </Link>
              </Button>
            </CardFooter>
          </Card>
        ))}
      </div>
    </section>
  )
}

Step 8: Membuat About Section

Buat file src/components/sections/about.tsx:

import { Badge } from "@/components/ui/badge"
import Image from "next/image"

const skills = [
  "JavaScript", "TypeScript", "React", "Next.js",
  "Node.js", "PostgreSQL", "MongoDB", "Prisma",
  "Tailwind CSS", "Git", "Docker", "AWS"
]

export function About() {
  return (
    <section id="about" className="bg-muted/50 py-24">
      <div className="container">
        <div className="grid md:grid-cols-2 gap-12 items-center">
          <div className="relative aspect-square max-w-md mx-auto">
            <Image
              src="/images/profile.jpg"
              alt="John Doe"
              fill
              className="object-cover rounded-2xl"
            />
          </div>

          <div className="space-y-6">
            <h2 className="text-3xl md:text-4xl font-bold">About Me</h2>
            <div className="space-y-4 text-muted-foreground">
              <p>
                Halo! Saya John, seorang Full Stack Developer dengan 5+ tahun 
                pengalaman membangun aplikasi web. Saya passionate dalam 
                menciptakan solusi digital yang tidak hanya fungsional 
                tetapi juga memberikan pengalaman user yang excellent.
              </p>
              <p>
                Saat ini saya fokus pada teknologi modern seperti Next.js, 
                TypeScript, dan cloud services. Di waktu luang, saya senang 
                berkontribusi ke open source dan menulis artikel teknis.
              </p>
            </div>

            <div className="space-y-4">
              <h3 className="text-xl font-semibold">Tech Stack</h3>
              <div className="flex flex-wrap gap-2">
                {skills.map((skill) => (
                  <Badge key={skill} variant="outline">
                    {skill}
                  </Badge>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  )
}

Step 9: Membuat Contact Form

Buat file src/components/sections/contact.tsx:

"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Mail, Send } from "lucide-react"

export function Contact() {
  const [isLoading, setIsLoading] = useState(false)

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setIsLoading(true)
    
    // Implementasi pengiriman form
    // Bisa menggunakan API route atau service seperti Formspree
    await new Promise((resolve) => setTimeout(resolve, 1000))
    
    setIsLoading(false)
    alert("Pesan terkirim! Saya akan segera menghubungi Anda.")
  }

  return (
    <section id="contact" className="container py-24">
      <div className="max-w-xl mx-auto">
        <Card>
          <CardHeader className="text-center">
            <div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
              <Mail className="h-6 w-6 text-primary" />
            </div>
            <CardTitle className="text-2xl">Hubungi Saya</CardTitle>
            <CardDescription>
              Ada project menarik? Mari diskusi dan bekerja sama!
            </CardDescription>
          </CardHeader>
          <CardContent>
            <form onSubmit={handleSubmit} className="space-y-4">
              <div className="grid grid-cols-2 gap-4">
                <div className="space-y-2">
                  <label htmlFor="name" className="text-sm font-medium">
                    Nama
                  </label>
                  <Input 
                    id="name" 
                    placeholder="John Doe" 
                    required 
                  />
                </div>
                <div className="space-y-2">
                  <label htmlFor="email" className="text-sm font-medium">
                    Email
                  </label>
                  <Input 
                    id="email" 
                    type="email" 
                    placeholder="[email protected]" 
                    required 
                  />
                </div>
              </div>
              <div className="space-y-2">
                <label htmlFor="subject" className="text-sm font-medium">
                  Subject
                </label>
                <Input 
                  id="subject" 
                  placeholder="Project Collaboration" 
                  required 
                />
              </div>
              <div className="space-y-2">
                <label htmlFor="message" className="text-sm font-medium">
                  Pesan
                </label>
                <Textarea
                  id="message"
                  placeholder="Ceritakan tentang project Anda..."
                  rows={5}
                  required
                />
              </div>
              <Button type="submit" className="w-full" disabled={isLoading}>
                {isLoading ? (
                  "Mengirim..."
                ) : (
                  <>
                    Kirim Pesan <Send className="ml-2 h-4 w-4" />
                  </>
                )}
              </Button>
            </form>
          </CardContent>
        </Card>
      </div>
    </section>
  )
}

Buat file src/components/layout/footer.tsx:

import Link from "next/link"
import { Github, Linkedin, Twitter } from "lucide-react"

export function Footer() {
  return (
    <footer className="border-t py-8">
      <div className="container flex flex-col md:flex-row justify-between items-center gap-4">
        <p className="text-sm text-muted-foreground">
          © {new Date().getFullYear()} John Doe. All rights reserved.
        </p>
        <div className="flex gap-4">
          <Link 
            href="https://github.com" 
            target="_blank"
            className="text-muted-foreground hover:text-foreground"
          >
            <Github className="h-5 w-5" />
          </Link>
          <Link 
            href="https://linkedin.com" 
            target="_blank"
            className="text-muted-foreground hover:text-foreground"
          >
            <Linkedin className="h-5 w-5" />
          </Link>
          <Link 
            href="https://twitter.com" 
            target="_blank"
            className="text-muted-foreground hover:text-foreground"
          >
            <Twitter className="h-5 w-5" />
          </Link>
        </div>
      </div>
    </footer>
  )
}

Step 11: Gabungkan Semua di Homepage

Update src/app/page.tsx:

import { Hero } from "@/components/sections/hero"
import { Projects } from "@/components/sections/projects"
import { About } from "@/components/sections/about"
import { Contact } from "@/components/sections/contact"

export default function Home() {
  return (
    <>
      <Hero />
      <Projects />
      <About />
      <Contact />
    </>
  )
}

Step 12: Jalankan Project

Sekarang jalankan development server:

npm run dev

Buka http://localhost:3000 untuk melihat portfolio kamu!

Step 13: Deploy ke Vercel

Deploy portfolio ke Vercel sangat mudah:

npm install -g vercel
vercel

Atau cukup push ke GitHub dan connect repository di vercel.com. Vercel akan otomatis detect Next.js dan melakukan deployment.

Kesimpulan

Selamat! Kamu sudah berhasil membuat portfolio developer profesional dengan Next.js dan Shadcn UI. Beberapa keuntungan dari stack ini:

  1. Next.js 14 - App Router, Server Components, dan performa optimal
  2. Shadcn UI - Komponen yang accessible dan fully customizable
  3. TypeScript - Type safety untuk mengurangi bug
  4. Dark Mode - UX modern dengan next-themes
  5. Vercel - Deployment yang seamless dan gratis

Dari sini, kamu bisa mengembangkan lebih lanjut dengan menambahkan:

  • Blog section dengan MDX
  • Animasi dengan Framer Motion
  • CMS untuk mengelola projects
  • Analytics untuk tracking visitors

Ada pertanyaan atau ingin diskusi? Reach out ke saya di Twitter @nayakayp!