feat: implement initial fullstack portfolio application including dashboard, CMS, and analytics features.
This commit is contained in:
250
src/components/sections/portfolio/AboutSection.tsx
Normal file
250
src/components/sections/portfolio/AboutSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
267
src/components/sections/portfolio/ContactSection.tsx
Normal file
267
src/components/sections/portfolio/ContactSection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
266
src/components/sections/portfolio/DashboardSidebar.tsx
Normal file
266
src/components/sections/portfolio/DashboardSidebar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
129
src/components/sections/portfolio/ExperienceSection.tsx
Normal file
129
src/components/sections/portfolio/ExperienceSection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
79
src/components/sections/portfolio/Footer.tsx
Normal file
79
src/components/sections/portfolio/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
384
src/components/sections/portfolio/HeroSection.tsx
Normal file
384
src/components/sections/portfolio/HeroSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
120
src/components/sections/portfolio/Navbar.tsx
Normal file
120
src/components/sections/portfolio/Navbar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
251
src/components/sections/portfolio/PrincipleSection.tsx
Normal file
251
src/components/sections/portfolio/PrincipleSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
src/components/sections/portfolio/ProjectsSection.tsx
Normal file
48
src/components/sections/portfolio/ProjectsSection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
176
src/components/sections/portfolio/TechStackSection.tsx
Normal file
176
src/components/sections/portfolio/TechStackSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user