feat: implement initial fullstack portfolio application including dashboard, CMS, and analytics features.

This commit is contained in:
Moh Dzulfikri Maulana
2026-03-07 16:32:49 +07:00
commit bdd61d11d3
59 changed files with 11107 additions and 0 deletions

View File

@@ -0,0 +1,250 @@
"use client";
import { useRef, useState, useEffect } from "react";
import { motion, useInView } from "framer-motion";
import { Code2, Server, Database, GitBranch } from "lucide-react";
const skills = [
"Next.js",
"NestJS",
"TypeScript",
"React",
"Node.js",
"PostgreSQL",
"Prisma",
"TypeORM",
"Redis",
"Docker",
"REST API",
"Git",
"Microservices",
];
const stats = [
{ label: "Years Experience", value: "3+", icon: Code2 },
{ label: "System Built", value: "15+", icon: GitBranch },
{ label: "Modules Delivered", value: "15+", icon: Server },
{ label: "Production Deployments", value: "10+", icon: Database },
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
// Custom hook for counting animation
const useCountingAnimation = (target: number, isInView: boolean, duration: number = 2000, delay: number = 0) => {
const [count, setCount] = useState(0);
const [shouldStart, setShouldStart] = useState(false);
useEffect(() => {
if (!isInView) {
setCount(0);
setShouldStart(false);
return;
}
// Start counting after delay
const timer = setTimeout(() => {
setShouldStart(true);
}, delay);
return () => clearTimeout(timer);
}, [isInView, delay]);
useEffect(() => {
if (!shouldStart) {
setCount(0);
return;
}
const startTime = Date.now();
const endTime = startTime + duration;
const animate = () => {
const now = Date.now();
const progress = Math.min((now - startTime) / duration, 1);
// Easing function for smooth animation
const easeOutQuad = 1 - Math.pow(1 - progress, 2);
const currentCount = Math.floor(easeOutQuad * target);
setCount(currentCount);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}, [shouldStart, target, duration]);
return count;
};
export function AboutSection() {
const ref = useRef(null);
const statsRef = useRef(null);
const quoteRef = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-10%" });
const statsInView = useInView(statsRef, { once: true, margin: "-10%" });
const quoteInView = useInView(quoteRef, { once: true, margin: "-10%" });
// Extract numeric values and use counting animation
// Calculate delay: staggerChildren: 0.08, delayChildren: 0.1, item duration: 0.5
// Stats are in the right column, so they appear after left column items
const entranceDelay = 600; // Reduced delay - after entrance animation completes
const yearsCount = useCountingAnimation(3, statsInView, 1500, entranceDelay);
const systemsCount = useCountingAnimation(15, statsInView, 2000, entranceDelay + 100);
const modulesCount = useCountingAnimation(15, statsInView, 2000, entranceDelay + 200);
const deploymentsCount = useCountingAnimation(10, statsInView, 1800, entranceDelay + 300);
return (
<section id="about" className="py-5 lg:py-32 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-background via-surface/30 to-background" />
<div className="relative z-10 max-w-6xl mx-auto px-6">
<motion.div
ref={ref}
variants={containerVariants}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
className="grid lg:grid-cols-2 gap-16 items-start"
>
{/* Left: Text content */}
<div>
<motion.div variants={itemVariants} className="mb-3">
<span className="text-accent font-mono text-sm tracking-widest uppercase">
About Me
</span>
</motion.div>
<motion.h2
variants={itemVariants}
className="font-display font-bold text-4xl md:text-5xl text-foreground mb-6 leading-tight"
>
Engineering Beyond
<br />
<span className="gradient-text-blue">Just Code</span>
</motion.h2>
<motion.div
variants={itemVariants}
className="space-y-4 text-foreground-muted leading-relaxed"
>
<p>
I'm a Fullstack Developer with 3+ years of experience building production-ready web applications using{" "}
<span className="text-foreground font-medium">Next.js</span> and{" "}
<span className="text-foreground font-medium">NestJS</span>.
My experience includes developing WhatsApp chatbot integrations,
building high-traffic lottery web applications, creating internal business dashboards,
and contributing to ERP-based POS systems.
</p>
<p>
My expertise spans scalable frontend architecture with Next.js,
Zustand, Tailwind CSS, and Framer Motion, as well as secure
backend systems using{" "}
<span className="text-foreground font-medium">NestJS</span>,{" "}
<span className="text-foreground font-medium">PostgreSQL</span>,
and <span className="text-foreground font-medium">MongoDB</span>
.
</p>
<p>
I focus on writing clean, maintainable code while ensuring
performance, validation, and system reliability. Every feature I
build is designed with scalability and long-term maintainability
in mind.
</p>
<p>
Whether it's architecting an internal dashboard from scratch,
optimizing complex database queries, or designing modular
backend systems I bring both execution and engineering
thinking to every project.
</p>
</motion.div>
{/* Skill badges */}
<motion.div
variants={containerVariants}
className="flex flex-wrap gap-2 mt-8"
>
{skills.map((skill) => (
<motion.span
key={skill}
variants={itemVariants}
className="px-3 py-1.5 text-xs font-mono font-medium bg-surface-elevated border border-border text-foreground-muted rounded-lg hover:border-accent/40 hover:text-accent hover:bg-accent/5 transition-all duration-200 cursor-default"
>
{skill}
</motion.span>
))}
</motion.div>
</div>
{/* Right: Stats grid */}
<motion.div
ref={statsRef}
variants={containerVariants}
initial="hidden"
animate={statsInView ? "visible" : "hidden"}
className="grid grid-cols-2 gap-4"
>
{stats.map((stat, index) => {
const Icon = stat.icon;
let displayValue = stat.value;
// Use animated values based on index
if (index === 0) displayValue = `${yearsCount}+`;
else if (index === 1) displayValue = `${systemsCount}+`;
else if (index === 2) displayValue = `${modulesCount}+`;
else if (index === 3) displayValue = `${deploymentsCount}+`;
return (
<motion.div
key={stat.label}
variants={itemVariants}
className="p-6 rounded-2xl bg-surface-elevated border border-border hover:border-accent/30 hover:shadow-glow transition-all duration-300 group"
>
<div className="w-10 h-10 rounded-xl bg-accent/10 border border-border flex items-center justify-center mb-4 group-hover:bg-accent/20 transition-colors">
<Icon size={18} className="text-accent" />
</div>
<p className="font-display font-bold text-4xl text-foreground mb-1">
{displayValue}
</p>
<p className="text-sm text-foreground-muted">{stat.label}</p>
</motion.div>
);
})}
{/* Quote card */}
<motion.div
ref={quoteRef}
variants={itemVariants}
className="col-span-2 p-6 rounded-2xl bg-gradient-to-br from-accent/10 to-purple-500/5 border border-border"
initial="hidden"
animate={quoteInView ? "visible" : "hidden"}
>
<p className="text-foreground-muted text-sm leading-relaxed italic">
"I focus on writing clean, maintainable code while thinking about how systems grow over time. I value good architecture, scalability, and building solutions that are easy for teams to maintain."
</p>
<p className="text-accent text-sm font-medium mt-3">
Engineering Philosophy
</p>
</motion.div>
</motion.div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,267 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Send, CheckCircle2, Mail, MapPin, Clock } from 'lucide-react'
import type { ContactFormData } from '@/types'
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email'),
subject: z.string().min(4, 'Subject must be at least 4 characters'),
message: z.string().min(10, 'Message must be at least 10 characters'),
})
const contactInfo = [
{ icon: Mail, label: 'Email', value: 'hello@yourportfolio.dev' },
{ icon: MapPin, label: 'Location', value: 'Indonesia' },
{ icon: Clock, label: 'Response time', value: 'Within 24 hours' },
]
export function ContactSection() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<ContactFormData>({ resolver: zodResolver(schema) })
const onSubmit = async (data: ContactFormData) => {
setStatus('loading')
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
const json = await res.json()
if (json.success) {
setStatus('success')
reset()
} else {
setErrorMsg(json.error || 'Something went wrong')
setStatus('error')
}
} catch {
setErrorMsg('Network error. Please try again.')
setStatus('error')
}
}
return (
<section id="contact" className="py-32 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface/20 to-background" />
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[600px] h-[300px] bg-accent/6 rounded-full blur-[120px] pointer-events-none" />
<div className="relative z-10 max-w-6xl mx-auto px-6">
{/* Header */}
<div className="text-center mb-16">
<motion.span
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="text-accent font-mono text-sm tracking-widest uppercase"
>
Get in Touch
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="font-display font-bold text-4xl md:text-5xl text-foreground mt-3"
>
Let's Build Something
<br />
<span className="gradient-text-blue">Together</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-foreground-muted mt-4 max-w-xl mx-auto"
>
Have a project in mind? Let's talk about how I can help you build something
remarkable.
</motion.p>
</div>
<div className="grid lg:grid-cols-5 gap-8 items-start">
{/* Left: Contact info */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="lg:col-span-2 space-y-4"
>
{contactInfo.map((item) => {
const Icon = item.icon
return (
<div
key={item.label}
className="flex items-center gap-4 p-4 rounded-xl bg-surface-elevated border border-border"
>
<div className="w-10 h-10 rounded-xl bg-accent/10 border border-border flex items-center justify-center flex-shrink-0">
<Icon size={16} className="text-accent" />
</div>
<div>
<p className="text-xs text-foreground-muted font-mono">{item.label}</p>
<p className="text-sm text-foreground font-medium">{item.value}</p>
</div>
</div>
)
})}
<div className="p-5 rounded-xl bg-gradient-to-br from-accent/10 to-purple-500/5 border border-border mt-6">
<p className="text-sm text-foreground-muted leading-relaxed">
I'm currently open to{' '}
<span className="text-foreground font-medium">freelance projects</span>,{' '}
<span className="text-foreground font-medium">full-time positions</span>, and
interesting technical collaborations.
</p>
</div>
</motion.div>
{/* Right: Form */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="lg:col-span-3"
>
<div className="rounded-2xl bg-surface-elevated border border-border p-8">
<AnimatePresence mode="wait">
{status === 'success' ? (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="text-center py-12"
>
<div className="w-16 h-16 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mx-auto mb-4">
<CheckCircle2 size={28} className="text-emerald-400" />
</div>
<h3 className="font-display font-semibold text-xl text-foreground mb-2">
Message Sent!
</h3>
<p className="text-foreground-muted text-sm">
Thank you for reaching out. I'll get back to you within 24 hours.
</p>
<button
onClick={() => setStatus('idle')}
className="mt-6 text-sm text-accent hover:text-accent/80 transition-colors"
>
Send another message
</button>
</motion.div>
) : (
<motion.form
key="form"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onSubmit={handleSubmit(onSubmit)}
className="space-y-5"
>
<div className="grid sm:grid-cols-2 gap-5">
<div>
<label className="block text-sm text-foreground-muted mb-2">
Name
</label>
<input
{...register('name')}
placeholder="Your Name"
className="w-full px-4 py-3 bg-surface border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20 transition-all text-sm"
/>
{errors.name && (
<p className="mt-1.5 text-xs text-red-400">{errors.name.message}</p>
)}
</div>
<div>
<label className="block text-sm text-foreground-muted mb-2">
Email
</label>
<input
{...register('email')}
type="email"
placeholder="you@email.com"
className="w-full px-4 py-3 bg-surface border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20 transition-all text-sm"
/>
{errors.email && (
<p className="mt-1.5 text-xs text-red-400">{errors.email.message}</p>
)}
</div>
</div>
<div>
<label className="block text-sm text-foreground-muted mb-2">
Subject
</label>
<input
{...register('subject')}
placeholder="Project inquiry / Collaboration / etc."
className="w-full px-4 py-3 bg-surface border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20 transition-all text-sm"
/>
{errors.subject && (
<p className="mt-1.5 text-xs text-red-400">{errors.subject.message}</p>
)}
</div>
<div>
<label className="block text-sm text-foreground-muted mb-2">
Message
</label>
<textarea
{...register('message')}
rows={5}
placeholder="Tell me about your project, goals, and timeline..."
className="w-full px-4 py-3 bg-surface border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20 transition-all text-sm resize-none"
/>
{errors.message && (
<p className="mt-1.5 text-xs text-red-400">{errors.message.message}</p>
)}
</div>
{status === 'error' && (
<p className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 px-4 py-3 rounded-xl">
{errorMsg}
</p>
)}
<button
type="submit"
disabled={status === 'loading'}
className="w-full flex items-center justify-center gap-2 px-6 py-3.5 bg-accent text-background font-semibold rounded-xl hover:bg-accent/90 transition-all hover:shadow-glow disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === 'loading' ? (
<>
<div className="w-4 h-4 border-2 border-background/30 border-t-background rounded-full animate-spin" />
Sending...
</>
) : (
<>
<Send size={16} />
Send Message
</>
)}
</button>
</motion.form>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,266 @@
"use client";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import {
Menu,
X,
LayoutDashboard,
LogOut,
MessageSquare,
Briefcase,
FileText,
FolderGit2,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
const dashboardLinks = [
{ label: "Overview", href: "/dashboard", icon: LayoutDashboard },
{ label: "Messages", href: "/dashboard/messages", icon: MessageSquare },
{ label: "Experience", href: "/dashboard/experience", icon: Briefcase },
{ label: "Blog", href: "/dashboard/blog", icon: FileText },
{ label: "Projects", href: "/dashboard/projects", icon: FolderGit2 },
];
export function DashboardSidebar() {
const pathname = usePathname();
const [isCollapsed, setIsCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// Hide sidebar entirely if user is on login page
if (pathname === "/dashboard/auth") return null;
if (!mounted) return null; // Prevent hydration mismatch
return (
<>
{/* Desktop Sidebar (Sticky) */}
<motion.aside
initial={false}
animate={{ width: isCollapsed ? 80 : 256 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="hidden md:flex flex-col sticky top-0 h-screen bg-background border-r border-border/50 shrink-0 z-40 group/sidebar"
>
<div className="flex items-center p-4 h-16 border-b border-border/50">
<Link
href="/dashboard"
className={cn(
"flex items-center gap-2 group w-full",
isCollapsed && "justify-center",
)}
>
<div className="w-8 h-8 shrink-0 rounded-md bg-primary/20 border border-primary/30 flex items-center justify-center group-hover:bg-primary/30 transition-colors">
<LayoutDashboard size={16} className="text-primary" />
</div>
{!isCollapsed && (
<span className="font-display font-semibold text-foreground text-sm tracking-wide whitespace-nowrap overflow-hidden">
Admin<span className="text-primary">Panel</span>
</span>
)}
</Link>
</div>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3.5 top-20 bg-background border border-border rounded-full p-1.5 hover:bg-surface-elevated text-foreground-muted hover:text-foreground z-50 opacity-0 group-hover/sidebar:opacity-100 transition-opacity"
aria-label="Toggle Sidebar"
>
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
</button>
<div className="flex-1 py-6 flex flex-col gap-2 px-3 overflow-y-auto overflow-x-hidden">
{dashboardLinks.map((link) => {
const Icon = link.icon;
const isActive =
link.href === "/dashboard"
? pathname === "/dashboard"
: pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
title={isCollapsed ? link.label : undefined}
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors overflow-hidden group/link",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-foreground-muted hover:bg-surface-elevated hover:text-foreground",
isCollapsed && "justify-center px-0",
)}
>
<Icon
size={18}
className={cn(
"shrink-0",
isActive
? "text-primary"
: "text-foreground-muted group-hover/link:text-foreground",
)}
/>
{!isCollapsed && (
<span className="whitespace-nowrap">{link.label}</span>
)}
</Link>
);
})}
</div>
<div className="p-4 border-t border-border/50 flex flex-col gap-2 relative">
{!isCollapsed && (
<div className="flex items-center justify-between px-3 py-2">
<span className="text-sm text-foreground-muted font-medium">
Theme
</span>
<ThemeToggle />
</div>
)}
{isCollapsed && (
<div className="flex justify-center py-2">
<ThemeToggle />
</div>
)}
<button
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors w-full text-destructive hover:bg-destructive/10 hover:text-destructive overflow-hidden group/logout",
isCollapsed && "justify-center px-0",
)}
title={isCollapsed ? "Logout" : undefined}
>
<LogOut
size={18}
className="shrink-0 group-hover/logout:text-destructive text-destructive"
/>
{!isCollapsed && (
<span className="font-medium whitespace-nowrap">Logout</span>
)}
</button>
</div>
</motion.aside>
{/* Mobile Header (Fixed) */}
<div className="md:hidden fixed top-0 left-0 right-0 h-16 bg-background/90 backdrop-blur-xl border-b border-border/50 z-40 flex items-center justify-between px-4">
<Link href="/dashboard" className="flex items-center gap-2">
<div className="w-8 h-8 rounded-md bg-primary/20 border border-primary/30 flex items-center justify-center">
<LayoutDashboard size={14} className="text-primary" />
</div>
<span className="font-display font-semibold text-foreground text-sm tracking-wide">
Admin<span className="text-primary">Panel</span>
</span>
</Link>
<div className="flex items-center gap-2">
<ThemeToggle />
<button
onClick={() => setMobileOpen(true)}
className="p-2 -mr-2 rounded-lg hover:bg-surface-elevated transition-colors text-foreground"
aria-label="Open menu"
>
<Menu size={20} />
</button>
</div>
</div>
{/* Mobile Sidebar Off-Canvas */}
<AnimatePresence>
{mobileOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setMobileOpen(false)}
className="md:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-50 cursor-pointer"
/>
)}
</AnimatePresence>
<AnimatePresence>
{mobileOpen && (
<motion.div
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", bounce: 0, duration: 0.4 }}
className="md:hidden fixed top-0 left-0 bottom-0 w-[280px] bg-background border-r border-border z-50 flex flex-col shadow-2xl"
>
<div className="flex items-center justify-between p-4 h-16 border-b border-border/50">
<Link
href="/dashboard"
className="flex items-center gap-2"
onClick={() => setMobileOpen(false)}
>
<div className="w-8 h-8 rounded-md bg-primary/20 border border-primary/30 flex items-center justify-center">
<LayoutDashboard size={16} className="text-primary" />
</div>
<span className="font-display font-semibold text-foreground text-sm tracking-wide">
Admin<span className="text-primary">Panel</span>
</span>
</Link>
<button
onClick={() => setMobileOpen(false)}
className="p-2 rounded-lg hover:bg-surface-elevated transition-colors text-foreground-muted"
aria-label="Close menu"
>
<X size={20} />
</button>
</div>
<div className="flex-1 py-6 px-4 flex flex-col gap-2 overflow-y-auto">
<span className="text-xs font-semibold text-foreground-muted mb-2 px-3 uppercase tracking-wider">
Navigation
</span>
{dashboardLinks.map((link) => {
const Icon = link.icon;
const isActive =
link.href === "/dashboard"
? pathname === "/dashboard"
: pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className={cn(
"flex items-center gap-3 px-3 py-3 rounded-lg transition-colors",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-foreground-muted hover:bg-surface-elevated hover:text-foreground",
)}
>
<Icon
size={18}
className={cn(
isActive ? "text-primary" : "text-foreground-muted",
)}
/>
<span>{link.label}</span>
</Link>
);
})}
</div>
<div className="p-4 border-t border-border/50 flex flex-col gap-2">
<button
className="flex items-center gap-3 px-3 py-3 rounded-lg transition-colors w-full text-destructive hover:bg-destructive/10"
onClick={() => setMobileOpen(false)}
>
<LogOut size={18} />
<span className="font-medium">Logout</span>
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View File

@@ -0,0 +1,129 @@
'use client'
import { useRef } from 'react'
import { motion, useInView } from 'framer-motion'
import { Briefcase, Calendar, CheckCircle2 } from 'lucide-react'
import type { Experience } from '@/types'
interface ExperienceSectionProps {
experiences: Experience[]
}
function formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
})
}
function ExperienceItem({ exp, index }: { exp: Experience; index: number }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-50px' })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: index * 0.15, ease: 'easeOut' }}
className="relative pl-8 pb-12 last:pb-0"
>
{/* Timeline dot */}
<div className="absolute left-0 top-1 w-3 h-3 rounded-full bg-accent border-2 border-accent/40 shadow-glow z-10" />
{/* Content */}
<div className="bg-surface-elevated border border-border rounded-2xl p-6 hover:border-accent/30 hover:shadow-glow transition-all duration-300 overflow-hidden">
{/* Header */}
<div className="flex flex-wrap items-start justify-between gap-4 mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<Briefcase size={14} className="text-accent" />
<h3 className="font-display font-semibold text-foreground text-lg">
{exp.role}
</h3>
</div>
<p className="text-accent font-medium text-sm">{exp.company}</p>
</div>
<div className="flex items-center gap-1.5 text-xs text-foreground-muted font-mono bg-surface border border-border px-3 py-1.5 rounded-lg">
<Calendar size={11} />
{formatDate(exp.startDate)} {exp.current ? 'Present' : exp.endDate ? formatDate(exp.endDate) : ''}
</div>
</div>
<p className="text-foreground-muted text-sm leading-relaxed mb-5">
{exp.description}
</p>
{/* Highlights */}
<ul className="space-y-2 mb-5">
{exp.highlights.map((h) => (
<li key={h} className="flex items-start gap-2.5 text-sm text-foreground-muted">
<CheckCircle2 size={14} className="text-accent mt-0.5 flex-shrink-0" />
<span>{h}</span>
</li>
))}
</ul>
{/* Tech stack */}
<div className="flex flex-wrap gap-1.5">
{exp.techStack.map((tech) => (
<span
key={tech}
className="px-2.5 py-1 text-xs font-mono bg-surface border border-border-subtle text-foreground-muted rounded-md"
>
{tech}
</span>
))}
</div>
</div>
</motion.div>
)
}
export function ExperienceSection({ experiences }: ExperienceSectionProps) {
return (
<section id="experience" className="py-16 sm:py-20 lg:py-32 relative">
<div className="max-w-4xl mx-auto px-4 sm:px-6">
{/* Header */}
<div className="text-center mb-16">
<motion.span
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="text-accent font-mono text-sm tracking-widest uppercase"
>
Career
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="font-display font-bold text-4xl md:text-5xl text-foreground mt-3"
>
Experience
</motion.h2>
</div>
{/* Timeline */}
<div className="relative">
{/* Vertical line */}
<motion.div
initial={{ scaleY: 0 }}
whileInView={{ scaleY: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="absolute left-1.5 top-0 bottom-0 w-px bg-gradient-to-b from-accent via-accent/50 to-transparent origin-top"
/>
<div>
{experiences.map((exp, i) => (
<ExperienceItem key={exp.id} exp={exp} index={i} />
))}
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,79 @@
"use client";
import { Github, Linkedin, Mail, Terminal } from "lucide-react";
import { usePathname } from "next/navigation";
import { NavLinks, ContactUrls } from "@/lib/constant";
export function Footer() {
const pathname = usePathname();
if (pathname?.startsWith("/dashboard")) return null;
return (
<footer className="border-t border-border bg-surface/30">
<div className="max-w-6xl mx-auto px-6 py-12">
<div className="flex flex-col md:flex-row items-center justify-between gap-8">
{/* Logo */}
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-md bg-accent/20 border border-accent/30 flex items-center justify-center">
<Terminal size={14} className="text-accent" />
</div>
<span className="font-display font-semibold text-foreground text-sm">
fikri<span className="text-accent">.</span>maulana
</span>
</div>
{/* Nav links */}
<nav className="flex items-center gap-6">
{NavLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm text-foreground-muted hover:text-foreground transition-colors"
>
{link.label}
</a>
))}
</nav>
{/* Social */}
<div className="flex items-center gap-3">
<a
href={ContactUrls.github}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg text-foreground-muted hover:text-foreground hover:bg-surface-elevated transition-all"
aria-label="GitHub"
>
<Github size={18} />
</a>
<a
href={ContactUrls.linkedin}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg text-foreground-muted hover:text-foreground hover:bg-surface-elevated transition-all"
aria-label="LinkedIn"
>
<Linkedin size={18} />
</a>
<a
href={ContactUrls.email}
className="p-2 rounded-lg text-foreground-muted hover:text-foreground hover:bg-surface-elevated transition-all"
aria-label="Email"
>
<Mail size={18} />
</a>
</div>
</div>
<div className="mt-8 pt-8 border-t border-border/50 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs text-foreground-subtle font-mono">
© {new Date().getFullYear()} Fikri Maulana. Built with Next.js &
TailwindCSS.
</p>
<p className="text-xs text-foreground-subtle">
Designed with precision. Engineered for scale.
</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,384 @@
"use client";
import { motion } from "framer-motion";
import { ArrowRight, Github, Linkedin, Mail, ChevronDown, MessageCircle, Zap } from "lucide-react";
import dynamic from "next/dynamic";
import { HeroSection as HeroData } from "@/lib/constant";
import { useEffect, useState } from "react";
import TerminalWindow from "@/components/TerminalWindow";
// Dynamic import for Lottie animation
const LottieAnimation = dynamic(
() => import("lottie-react").then((mod) => mod.default),
{
ssr: false,
loading: () => (
<div className="w-20 h-20 animate-pulse bg-accent/20 rounded-xl" />
),
}
);
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.12,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 24 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" },
},
};
export function HeroSection() {
const [loadingAnimation, setLoadingAnimation] = useState(null);
const [isDark, setIsDark] = useState(false);
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
useEffect(() => {
fetch("/lottie/loading.json")
.then((res) => res.json())
.then((data) => setLoadingAnimation(data))
.catch((err) =>
console.error("Failed to load Lottie animation:", err)
);
// Check dark mode
const checkDarkMode = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkDarkMode();
// Listen for theme changes
const observer = new MutationObserver(checkDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// Hide scroll indicator when user is at the bottom (within 50px of bottom)
if (scrollTop + windowHeight >= documentHeight - 50) {
setShowScrollIndicator(false);
} else {
setShowScrollIndicator(true);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section
id="hero"
aria-labelledby="hero-title"
className="relative min-h-screen flex flex-col items-center justify-center overflow-hidden"
>
{/* Background effects */}
<div
className="absolute inset-0 bg-grid opacity-100"
aria-hidden="true"
/>
<div
className="absolute inset-0 bg-gradient-to-b from-background via-background/95 to-background"
aria-hidden="true"
/>
{/* Mobile photo background */}
<div
className="absolute inset-0 lg:hidden opacity-10"
style={{
backgroundImage: `url(${HeroData.image})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}
aria-hidden="true"
/>
{/* Blur orbs */}
<div
className="absolute top-1/4 left-1/4 w-96 h-96 bg-accent/8 rounded-full blur-[120px] pointer-events-none"
aria-hidden="true"
/>
<div
className="absolute bottom-1/3 right-1/4 w-80 h-80 bg-purple-500/6 rounded-full blur-[100px] pointer-events-none"
aria-hidden="true"
/>
<div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-accent/4 rounded-full blur-[150px] pointer-events-none"
aria-hidden="true"
/>
<div className="relative z-10 max-w-7xl mx-auto px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center min-h-[70vh] py-20 lg:py-0 lg:gap-16">
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="text-center lg:text-left flex flex-col items-center lg:items-start"
>
{/* Status badge */}
<motion.div variants={itemVariants} className="mb-8">
<p
role="status"
aria-live="polite"
className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-border bg-accent/10 text-accent text-sm font-medium"
>
<span
className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse-slow"
aria-hidden="true"
/>
{HeroData.badge || "Available for opportunities"}
</p>
</motion.div>
{/* Lottie animation */}
<motion.div
variants={itemVariants}
className="-mt-8 block lg:hidden overflow-hidden rounded-md"
>
{loadingAnimation && (
<LottieAnimation
animationData={loadingAnimation}
loop
autoplay
className="w-[300px] h-[200px] object-cover"
/>
)}
</motion.div>
{/* Main headline */}
<motion.h1
id="hero-title"
variants={itemVariants}
className="font-display font-bold text-5xl sm:text-6xl md:text-7xl lg:text-8xl leading-[1.05] tracking-tight mb-6"
>
<span className="text-foreground">
{HeroData.headline || "Building Systems"}
</span>
<br />
<span className="gradient-text">
{HeroData.headline2 || "That Scale."}
</span>
</motion.h1>
{/* Tagline */}
<motion.p
variants={itemVariants}
className="text-lg md:text-xl text-foreground-muted mb-4 font-light leading-relaxed max-w-2xl"
>
{HeroData.tagline1 ||
"Senior Fullstack Engineer — Next.js & NestJS Specialist"}
</motion.p>
<motion.p
variants={itemVariants}
className="text-base text-foreground-subtle mb-10 leading-relaxed max-w-xl"
>
{HeroData.tagline2 ||
"I architect and ship production-grade web applications with a focus on clean architecture, performance, and developer experience."}
</motion.p>
{/* CTA Buttons */}
<motion.nav
aria-label="Primary Actions"
variants={itemVariants}
className="flex flex-wrap items-center justify-center lg:justify-start gap-4 mb-14"
>
<a
href="#projects"
className="group inline-flex items-center gap-2 px-6 py-3 bg-accent text-white font-semibold rounded-xl hover:bg-accent/90 transition-all hover:shadow-glow hover:-translate-y-0.5 duration-200"
>
View Projects
<ArrowRight
size={16}
className="group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</a>
<a
href="#contact"
className="inline-flex items-center gap-2 px-6 py-3 bg-surface-elevated border border-border text-foreground font-medium rounded-xl hover:border-accent/40 hover:bg-surface-elevated/80 transition-all hover:-translate-y-0.5 duration-200"
>
<Mail size={16} className="text-accent" aria-hidden="true" />
Contact Me
</a>
</motion.nav>
{/* Social links */}
<motion.nav
aria-label="Social Media Profiles"
variants={itemVariants}
className="flex items-center justify-center lg:justify-start gap-4"
>
<ul className="flex items-center gap-4 m-0 p-0 list-none">
<li>
<a
href="https://github.com/Dzuuul"
target="_blank"
rel="noopener noreferrer"
className="block p-2.5 rounded-xl border border-border hover:border-accent/40 hover:bg-surface-elevated text-foreground-muted hover:text-foreground transition-all hover:-translate-y-0.5 duration-200"
aria-label="GitHub Profile"
>
<Github size={18} aria-hidden="true" />
</a>
</li>
<li>
<a
href="https://www.linkedin.com/in/dzulfikrimaulana"
target="_blank"
rel="noopener noreferrer"
className="block p-2.5 rounded-xl border border-border hover:border-accent/40 hover:bg-surface-elevated text-foreground-muted hover:text-foreground transition-all hover:-translate-y-0.5 duration-200"
aria-label="LinkedIn Profile"
>
<Linkedin size={18} aria-hidden="true" />
</a>
</li>
<li>
<a
href="https://wa.me/6285163616363?text=Hi%20Fikri,%0D%0AI%20am%20[Name]%20from%20[Company/Organization%20Name].%0D%0AI%20recently%20explored%20your%20portfolio%20and%20was%20impressed%20by%20your%20work%20as%20a%20Fullstack%20Developer.%0D%0AI%20would%20like%20to%20discuss%20a%20potential%20opportunity%20with%20you,%20whether%20it%20be%20for%20a%20career%20role%20within%20our%20team%20or%20a%20professional%20collaboration%20on%20an%20upcoming%20project.%0D%0AAre%20you%20available%20for%20a%20brief%20introductory%20call%20or%20a%20chat%20sometime%20this%20week%20to%20discuss%20this%20further?%0D%0A%0D%0ABest%20regards,%0D%0A[Name]%0D%0A[LinkedIn%20Profile/Contact%20Info]"
target="_blank"
rel="noopener noreferrer"
className="block p-2.5 rounded-xl border border-border hover:border-accent/40 hover:bg-surface-elevated text-foreground-muted hover:text-foreground transition-all hover:-translate-y-0.5 duration-200"
aria-label="WhatsApp Contact"
>
<MessageCircle size={18} aria-hidden="true" />
</a>
</li>
</ul>
<div className="w-px h-5 bg-border mx-1" aria-hidden="true" />
<span className="text-foreground-subtle text-sm font-mono">
3+ yrs experience
</span>
</motion.nav>
</motion.div>
{/* Right Section: Photo - Desktop Only */}
<motion.div
variants={itemVariants}
initial="hidden"
animate="visible"
className="relative hidden lg:flex justify-end"
>
<motion.div
className="relative group max-w-xs sm:max-w-md w-full aspect-square"
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{/* Window frame */}
<div className="relative h-full w-full rounded-lg border border-border bg-surface-elevated shadow-2xl overflow-hidden">
{/* Window header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-border bg-surface-elevated/70">
<div className="flex gap-2">
<div className="w-3 h-3 bg-red-500 rounded-full" />
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
<div className="w-3 h-3 bg-green-500 rounded-full" />
</div>
<span className="text-xs text-foreground-muted">
portfolio.jpg
</span>
</div>
{/* Image content */}
<div className="flex-1 flex items-center justify-center bg-surface">
<img
src={HeroData.image}
alt="Professional portrait"
className="max-w-full max-h-full object-contain group-hover:scale-105 transition-transform duration-700"
/>
</div>
</div>
{/* Floating card decor */}
<div className="absolute -bottom-8 -left-8 rounded-xl bg-surface-elevated border border-border p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/10 text-accent">
<Zap
size={24}
strokeWidth={2}
aria-hidden="true"
/>
</div>
<div>
<p className="text-lg font-semibold text-foreground">10+ Projects</p>
<p className="text-sm text-foreground-muted">Completed successfully</p>
</div>
</div>
</div>
{/* Floating card decor */}
<div className="absolute -top-12 -right-8 rounded-xl bg-surface-elevated border border-border p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent/10 text-accent">
<Zap
size={24}
strokeWidth={2}
aria-hidden="true"
/>
</div>
<div>
<p className="text-lg font-semibold text-foreground">Production Systems</p>
<p className="text-sm text-foreground-muted">Built & Maintained</p>
</div>
</div>
</div>
{/* Terminal Window */}
<div className="absolute -bottom-[45px] -right-[100px]">
<TerminalWindow />
</div>
</motion.div>
</motion.div>
</div>
</div>
{/* Scroll indicator */}
{showScrollIndicator && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5, duration: 0.5 }}
className="fixed -bottom-3 left-1/2 transform -translate-x-1/2 z-50"
aria-hidden="true"
>
{/* Rounded rectangle container with content inside */}
<div className="w-32 h-16 rounded-t-2xl border border-border bg-surface-elevated/80 backdrop-blur-sm flex flex-col items-center justify-center -gap-1 shadow-lg">
<span className="text-foreground-subtle text-xs font-mono tracking-widest uppercase">
Scroll Down
</span>
<motion.div
animate={{ y: [0, 4, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
className="flex items-center justify-center"
>
<ChevronDown size={16} className="text-accent" />
</motion.div>
</div>
</motion.div>
)}
</section>
);
}

View File

@@ -0,0 +1,120 @@
"use client";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, Terminal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
import { NavLinks } from "@/lib/constant";
export function Navbar() {
const pathname = usePathname();
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
if (pathname?.startsWith("/dashboard")) return null;
return (
<>
<motion.header
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={cn(
"fixed top-0 left-0 right-0 z-50 transition-all duration-300",
scrolled
? "bg-background/90 backdrop-blur-xl border-b border-border"
: "bg-transparent border-border",
)}
>
<nav className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Logo */}
<a href="#" className="flex items-center gap-2 group">
<div className="w-7 h-7 rounded-md bg-accent/20 border border-border flex items-center justify-center group-hover:bg-accent/30 transition-colors">
<Terminal size={14} className="text-accent" />
</div>
<span className="font-display font-semibold text-foreground text-sm tracking-wide">
fikri<span className="text-accent">.</span>maulana
</span>
</a>
{/* Desktop nav */}
<div className="hidden md:flex items-center gap-1">
{NavLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="px-4 py-2 text-sm text-foreground-muted hover:text-foreground transition-colors rounded-lg hover:bg-surface-elevated"
>
{link.label}
</a>
))}
</div>
{/* CTA + Theme Toggle */}
<div className="hidden md:flex items-center gap-2">
<ThemeToggle />
<a
href="#contact"
className="px-4 py-2 text-sm font-medium bg-accent text-white rounded-lg hover:bg-accent/90 transition-all hover:shadow-glow"
>
Download CV
</a>
</div>
{/* Mobile toggle */}
<div className="md:hidden flex items-center gap-2">
<ThemeToggle />
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="p-2 rounded-lg hover:bg-surface-elevated transition-colors"
aria-label="Toggle menu"
>
{mobileOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
</nav>
</motion.header>
{/* Mobile menu */}
<AnimatePresence>
{mobileOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="fixed top-16 left-0 right-0 z-40 bg-background/95 backdrop-blur-xl border-b border-border"
>
<div className="w-full px-6 py-4 flex flex-col gap-1">
{NavLinks.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className="px-4 py-3 text-foreground-muted hover:text-foreground rounded-lg hover:bg-surface-elevated transition-colors"
>
{link.label}
</a>
))}
<a
href="#contact"
onClick={() => setMobileOpen(false)}
className="mt-2 px-4 py-3 text-center font-medium bg-accent text-background rounded-lg"
>
Download CV
</a>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View File

@@ -0,0 +1,251 @@
"use client";
import { useRef, useState, useEffect } from "react";
import { motion, useInView } from "framer-motion";
import {
Layers,
ShieldCheck,
Zap,
RefreshCcw,
GitBranch,
Box,
} from "lucide-react";
import { useTheme } from "next-themes";
import SyntaxHighlighter from "react-syntax-highlighter";
import {
atomOneDark,
atomOneLight,
} from "react-syntax-highlighter/dist/esm/styles/hljs";
const principles = [
{
icon: Layers,
title: "Clean Architecture Mindset",
description:
"I structure applications with clear separation of concerns so business logic stays maintainable and easy to evolve as the project grows.",
tag: "Architecture",
},
{
icon: Box,
title: "Modular Code Structure",
description:
"I organize features into well-defined modules to keep the codebase scalable and easier for teams to understand and maintain.",
tag: "Scalability",
},
{
icon: GitBranch,
title: "Abstraction & Reusability",
description:
"I prefer abstractions and reusable patterns to keep the codebase flexible and easier to extend in future iterations.",
tag: "Code Design",
},
{
icon: ShieldCheck,
title: "SOLID Principles",
description:
"I apply SOLID principles to write clean, understandable code where each component has a clear responsibility.",
tag: "Code Quality",
},
{
icon: Zap,
title: "Performance Awareness",
description:
"I consider performance early by applying efficient queries, pagination, and thoughtful data handling in web applications.",
tag: "Performance",
},
{
icon: RefreshCcw,
title: "Maintainable Development",
description:
"I focus on writing maintainable code, using version control effectively, and building systems that teams can work on confidently.",
tag: "Workflow",
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export function PrincipleSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-80px" });
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const isDark = resolvedTheme === "dark";
const codeSnippet = `// ✅ Application Layer — Pure Business Logic
@Injectable()
export class UserService {
constructor(
private readonly userRepository: IUserRepository, // Interface, not implementation
private readonly eventEmitter: IEventEmitter,
) {}
async createUser(dto: CreateUserDto): Promise<UserEntity> {
const exists = await this.userRepository.findByEmail(dto.email)
if (exists) throw new ConflictException('Email already registered')
const user = UserEntity.create(dto) // Domain entity handles business rules
await this.userRepository.save(user)
await this.eventEmitter.emit('user.created', user)
return user
}
}
// ✅ Infrastructure Layer — Implementation Detail
@Injectable()
export class PrismaUserRepository implements IUserRepository {
constructor(private prisma: PrismaService) {}
async findByEmail(email: string) { /* ... */ }
async save(user: UserEntity) { /* ... */ }
}`;
return (
<section id="principles" className="py-16 sm:py-20 lg:py-32 relative overflow-hidden">
{/* Background */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface/30 to-transparent" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[400px] bg-accent/4 rounded-full blur-[150px] pointer-events-none" />
<div className="relative z-10 max-w-6xl mx-auto px-6">
{/* Header */}
<div className="text-center mb-12 sm:mb-16">
<motion.span
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="text-accent font-mono text-xs sm:text-sm tracking-widest uppercase"
>
System Design
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="font-display font-bold text-3xl sm:text-4xl md:text-5xl text-foreground mt-3"
>
Engineering
<span className="gradient-text-blue"> Principles</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-foreground-muted mt-4 max-w-2xl mx-auto leading-relaxed text-sm sm:text-base px-4"
>
I aim to build systems that are clean, maintainable, and ready to grow as products evolve.
These principles guide how I structure applications and approach technical decisions.
</motion.p>
</div>
{/* Principles Grid */}
<motion.div
ref={ref}
variants={containerVariants}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5"
>
{principles.map((p) => {
const Icon = p.icon;
return (
<motion.div
key={p.title}
variants={itemVariants}
className="group p-4 sm:p-6 rounded-2xl bg-surface-elevated border border-border hover:border-accent/30 hover:shadow-glow transition-all duration-300"
>
<div className="flex items-start justify-between gap-4 mb-4">
<div className="w-10 h-10 rounded-xl bg-accent/10 border border-border flex items-center justify-center group-hover:bg-accent/20 transition-colors">
<Icon size={18} className="text-accent" />
</div>
<span className="text-xs font-mono text-foreground-subtle bg-surface border border-border-subtle px-2.5 py-1 rounded-lg">
{p.tag}
</span>
</div>
<h3 className="font-display font-semibold text-foreground text-sm sm:text-base mb-3 leading-snug">
{p.title}
</h3>
<p className="text-xs sm:text-sm text-foreground-muted leading-relaxed">
{p.description}
</p>
</motion.div>
);
})}
</motion.div>
{/* Code snippet preview */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mt-8 sm:mt-10 rounded-2xl bg-surface-elevated border border-border overflow-hidden"
>
{/* macOS-style title bar */}
<div className="flex items-center gap-2 sm:gap-3 px-3 sm:px-5 py-3 sm:py-3.5 border-b border-border bg-surface">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500/70" />
<div className="w-3 h-3 rounded-full bg-yellow-500/70" />
<div className="w-3 h-3 rounded-full bg-green-500/70" />
</div>
<span className="text-xs font-mono text-foreground-muted ml-1 truncate">
user.service.ts
</span>
<span className="hidden sm:inline ml-auto text-xs font-mono text-foreground-subtle opacity-60">
Clean Architecture Example
</span>
</div>
{/* Syntax-highlighted code */}
{mounted ? (
<SyntaxHighlighter
language="typescript"
style={isDark ? atomOneDark : atomOneLight}
showLineNumbers={false}
wrapLongLines={true}
customStyle={{
margin: 0,
padding: "1rem 1.5rem",
fontSize: "0.6875rem",
lineHeight: "1.6",
background: "transparent",
overflowX: "auto",
}}
lineNumberStyle={{
color: isDark ? "#3d4451" : "#c4c9d4",
paddingRight: "1.5rem",
minWidth: "2.5rem",
userSelect: "none",
}}
>
{codeSnippet}
</SyntaxHighlighter>
) : (
<div
className="p-6 overflow-x-auto text-[0.75rem] font-mono leading-[1.7] opacity-0 text-transparent"
aria-hidden="true"
>
<pre>{codeSnippet}</pre>
</div>
)}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,48 @@
import { ProjectCard } from '@/components/ui/ProjectCard'
import type { Project } from '@/types'
interface ProjectsSectionProps {
projects: Project[]
}
export function ProjectsSection({ projects }: ProjectsSectionProps) {
return (
<section id="projects" className="py-32 relative">
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface/20 to-transparent" />
<div className="relative z-10 max-w-6xl mx-auto px-6">
{/* Header */}
<div className="text-center mb-16">
<span className="text-accent font-mono text-sm tracking-widest uppercase">
Work
</span>
<h2 className="font-display font-bold text-4xl md:text-5xl text-foreground mt-3">
Featured Projects
</h2>
<p className="text-foreground-muted mt-4 max-w-xl mx-auto">
Production-grade systems I've built from concept to deployment
</p>
</div>
{/* Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project, index) => (
<ProjectCard key={project.id} project={project} index={index} />
))}
</div>
{/* View more */}
<div className="text-center mt-12">
<a
href="https://github.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 border border-border hover:border-accent/40 text-foreground-muted hover:text-foreground rounded-xl transition-all hover:-translate-y-0.5 duration-200"
>
View All on GitHub
</a>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,176 @@
"use client";
import { useRef } from "react";
import { motion, useInView } from "framer-motion";
import {
SiNextdotjs,
SiReact,
SiTypescript,
SiTailwindcss,
SiFramer,
SiNestjs,
SiNodedotjs,
SiExpress,
SiPostgresql,
SiMongodb,
SiRedis,
SiPrisma,
SiDocker,
SiGit,
SiGithub,
SiLinux,
SiTypeorm,
SiRedbull,
} from "react-icons/si";
// Note: If react-icons is not installed, replace with text-based badges
// npm install react-icons
const categories = [
{
label: "Frontend",
color: "from-blue-500/20 to-cyan-500/10",
borderColor: "border-blue-500/20",
iconColor: "text-blue-400",
tech: [
{ name: "Next.js", Icon: SiNextdotjs },
{ name: "React", Icon: SiReact },
{ name: "TypeScript", Icon: SiTypescript },
{ name: "Tailwind CSS", Icon: SiTailwindcss },
{ name: "Framer Motion", Icon: SiFramer },
],
},
{
label: "Backend",
color: "from-emerald-500/20 to-teal-500/10",
borderColor: "border-emerald-500/20",
iconColor: "text-emerald-400",
tech: [
{ name: "NestJS", Icon: SiNestjs },
{ name: "Node.js", Icon: SiNodedotjs },
{ name: "Express", Icon: SiExpress },
{ name: "BullMQ", Icon: SiRedbull },
],
},
{
label: "Database",
color: "from-orange-500/20 to-amber-500/10",
borderColor: "border-orange-500/20",
iconColor: "text-orange-400",
tech: [
{ name: "PostgreSQL", Icon: SiPostgresql },
{ name: "MongoDB", Icon: SiMongodb },
{ name: "Redis", Icon: SiRedis },
{ name: "Prisma ORM", Icon: SiPrisma },
{ name: "TypeORM", Icon: SiTypeorm },
],
},
{
label: "DevOps",
color: "from-purple-500/20 to-violet-500/10",
borderColor: "border-purple-500/20",
iconColor: "text-purple-400",
tech: [
{ name: "Docker", Icon: SiDocker },
{ name: "Git", Icon: SiGit },
{ name: "GitHub", Icon: SiGithub },
{ name: "Linux", Icon: SiLinux },
],
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: 0.1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export function TechStackSection() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<section id="stack" className="py-5 lg:py-32 relative">
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div className="text-center mb-16">
<motion.span
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="text-accent font-mono text-sm tracking-widest uppercase"
>
Technology
</motion.span>
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="font-display font-bold text-4xl md:text-5xl text-foreground mt-3"
>
My Tech Stack
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-foreground-muted mt-4 max-w-xl mx-auto"
>
Battle-tested tools and frameworks I use to build production systems
</motion.p>
</div>
{/* Grid */}
<motion.div
ref={ref}
variants={containerVariants}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6"
>
{categories.map((category) => (
<motion.div
key={category.label}
variants={itemVariants}
className={`p-6 rounded-2xl bg-gradient-to-br ${category.color} border ${category.borderColor} hover:shadow-glow transition-all duration-300 group`}
>
<h3
className={`font-display font-semibold text-sm tracking-widest uppercase mb-5 ${category.iconColor}`}
>
{category.label}
</h3>
<div className="space-y-3">
{category.tech.map((item) => {
const Icon = item.Icon;
return (
<div
key={item.name}
className="flex items-center gap-3 p-2.5 rounded-xl bg-background/30 hover:bg-background/60 transition-all duration-200 group/item cursor-default"
>
<Icon
size={18}
className={`${category.iconColor} group-hover/item:scale-110 transition-transform flex-shrink-0`}
/>
<span className="text-sm text-foreground-muted group-hover/item:text-foreground transition-colors font-medium">
{item.name}
</span>
</div>
);
})}
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}