Commit 3eb5b4e1 authored by DatLD's avatar DatLD

Update portfolio UI

parent b53436f5
This diff is collapsed.
...@@ -2,35 +2,17 @@ import Link from 'next/link' ...@@ -2,35 +2,17 @@ import Link from 'next/link'
export default function NotFound() { export default function NotFound() {
return ( return (
<div <div className="flex min-h-screen flex-col items-center justify-center bg-[var(--bg)] p-6 text-center">
className="min-h-screen flex flex-col items-center justify-center p-6 text-center" <h1 className="mb-4 text-7xl font-extrabold text-[var(--primary)] sm:text-8xl">
style={{ background: 'var(--bg)' }}
>
<h1
className="text-8xl font-extrabold mb-4 gradient-text"
style={{ fontFamily: 'var(--font-heading)' }}
>
404 404
</h1> </h1>
<p <p className="mb-2 text-2xl font-semibold text-[var(--text)]">
className="text-2xl font-semibold mb-2"
style={{ color: 'var(--color-text)', fontFamily: 'var(--font-heading)' }}
>
Page not found. Page not found.
</p> </p>
<p className="text-sm mb-8 max-w-md" style={{ color: 'var(--color-muted)' }}> <p className="mb-8 max-w-md text-sm text-[var(--muted)]">
Sorry, the page you are looking for does not exist or has been moved. Sorry, the page you are looking for does not exist or has been moved.
</p> </p>
<Link <Link href="/" className="btn-primary">
href="/"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-300 hover:scale-105"
style={{
background: 'linear-gradient(135deg, #F97316 0%, #FB923C 100%)',
color: '#0A0F1E',
fontFamily: 'var(--font-heading)',
boxShadow: '0 0 20px rgba(249,115,22,0.3)',
}}
>
Back to Home Back to Home
</Link> </Link>
</div> </div>
......
...@@ -14,17 +14,12 @@ export default function Home() { ...@@ -14,17 +14,12 @@ export default function Home() {
<> <>
<TopProgressBar /> <TopProgressBar />
<Navbar /> <Navbar />
<main id="main-content"> <main id="main-content" className="overflow-hidden">
<Hero /> <Hero />
<hr className="section-divider" />
<About /> <About />
<hr className="section-divider" />
<Skills /> <Skills />
<hr className="section-divider" />
<Projects /> <Projects />
<hr className="section-divider" />
<Experience /> <Experience />
<hr className="section-divider" />
<Contact /> <Contact />
</main> </main>
<Footer /> <Footer />
......
This diff is collapsed.
...@@ -27,11 +27,9 @@ export default function BackToTop() { ...@@ -27,11 +27,9 @@ export default function BackToTop() {
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }} exit={{ opacity: 0, scale: 0.5 }}
onClick={scrollToTop} onClick={scrollToTop}
className="fixed bottom-6 right-6 z-50 flex items-center justify-center w-[44px] h-[44px] rounded-full shadow-lg hover:scale-110 transition-transform duration-200 cursor-pointer" className="fixed bottom-6 right-6 z-50 flex h-11 w-11 cursor-pointer items-center justify-center rounded-md border border-[var(--border)] bg-white text-[var(--primary)] shadow-lg transition-transform duration-200 hover:scale-105"
style={{ style={{
background: 'linear-gradient(135deg, #F97316 0%, #FB923C 100%)', boxShadow: '0 12px 30px rgba(15, 23, 42, 0.14)',
color: '#0A0F1E',
boxShadow: '0 0 20px rgba(249,115,22,0.4)',
}} }}
aria-label="Back to top" aria-label="Back to top"
> >
......
This diff is collapsed.
'use client' 'use client'
import { useRef } from 'react' import { useRef } from 'react'
import { motion, useInView, useReducedMotion } from 'framer-motion' import { motion, useInView } from 'framer-motion'
import { timelineEvents, type TimelineEvent } from '@/lib/data' import { timelineEvents } from '@/lib/data'
import { fadeUp, fadeUpReduced } from '@/lib/animations'
const badgeStyles: Record<NonNullable<TimelineEvent['badgeColor']>, { bg: string; color: string; border: string }> = {
orange: { bg: 'rgba(249,115,22,0.12)', color: '#F97316', border: 'rgba(249,115,22,0.3)' },
blue: { bg: 'rgba(59,130,246,0.12)', color: '#60A5FA', border: 'rgba(59,130,246,0.3)' },
green: { bg: 'rgba(34,197,94,0.12)', color: '#4ADE80', border: 'rgba(34,197,94,0.3)' },
purple: { bg: 'rgba(168,85,247,0.12)', color: '#C084FC', border: 'rgba(168,85,247,0.3)' },
}
interface TimelineNodeProps {
event: TimelineEvent
index: number
isInView: boolean
isLast: boolean
prefersReduced: boolean | null
}
function TimelineNode({ event, index, isInView, isLast, prefersReduced }: TimelineNodeProps) {
const badgeStyle = event.badgeColor ? badgeStyles[event.badgeColor] : badgeStyles.orange
return (
<motion.div
initial={prefersReduced ? { opacity: 0 } : { opacity: 0, x: -32 }}
animate={isInView
? prefersReduced ? { opacity: 1 } : { opacity: 1, x: 0 }
: {}
}
transition={{ duration: prefersReduced ? 0.3 : 0.55, delay: index * 0.15, ease: 'easeOut' }}
className="relative flex gap-6"
>
{/* Left — Line & Dot */}
<div className="relative flex flex-col items-center" style={{ minWidth: '40px' }}>
{/* Animated vertical connector */}
{!isLast && (
<motion.div
className="absolute top-10 bottom-0 w-[2px]"
initial={{ scaleY: 0, originY: 0 }}
animate={isInView ? { scaleY: 1 } : {}}
transition={{ duration: 0.8, delay: 0.2 + index * 0.15, ease: 'easeOut' }}
style={{
background: 'linear-gradient(to bottom, rgba(249,115,22,0.5), rgba(249,115,22,0.05))',
left: '50%',
transform: 'translateX(-50%)',
transformOrigin: 'top',
}}
/>
)}
{/* Dot */}
<motion.div
initial={{ scale: 0 }}
animate={isInView ? { scale: 1 } : {}}
transition={{ duration: 0.4, delay: 0.1 + index * 0.15, ease: 'backOut' }}
className="relative z-10 w-10 h-10 rounded-full flex items-center justify-center shrink-0"
style={{
background: 'var(--color-surface)',
border: '2px solid #F97316',
boxShadow: '0 0 12px rgba(249,115,22,0.3)',
}}
>
<span
className="w-3 h-3 rounded-full"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
/>
</motion.div>
</div>
{/* Right — Content card */}
<div
className={`flex-1 rounded-xl p-5 glass-card ${isLast ? 'mb-0' : 'mb-6'}`}
style={{ marginTop: '5px' }}
>
{/* Date + Badge */}
<div className="flex flex-wrap items-center gap-3 mb-3">
<span
className="text-xs font-medium"
style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-mono)' }}
>
{event.date}
</span>
{event.badge && (
<span
className="px-2.5 py-0.5 rounded-full text-xs font-semibold"
style={{
background: badgeStyle.bg,
color: badgeStyle.color,
border: `1px solid ${badgeStyle.border}`,
fontFamily: 'var(--font-mono)',
}}
>
{event.badge}
</span>
)}
</div>
<h3
className="font-semibold text-base mb-1"
style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)' }}
>
{event.title}
</h3>
{event.subtitle && (
<p className="text-sm mb-1" style={{ color: 'var(--primary)' }}>
{event.subtitle}
</p>
)}
<p className="text-sm" style={{ color: 'var(--color-muted2)' }}>
{event.description}
</p>
</div>
</motion.div>
)
}
export default function Experience() { export default function Experience() {
const sectionRef = useRef<HTMLElement>(null) const sectionRef = useRef<HTMLElement>(null)
const isInView = useInView(sectionRef, { once: true, margin: '-80px' }) const isInView = useInView(sectionRef, { once: true, margin: '-80px' })
const prefersReduced = useReducedMotion()
const headingVariants = prefersReduced ? fadeUpReduced : fadeUp
return ( return (
<section <section id="experience" ref={sectionRef} className="section-shell" aria-label="Experience section">
id="experience"
ref={sectionRef}
className="py-24"
style={{ background: 'var(--color-surface)' }}
aria-label="Experience section"
>
<div className="section-container"> <div className="section-container">
{/* Section Heading */}
<motion.div <motion.div
variants={headingVariants} initial={{ opacity: 0, y: 18 }}
custom={0} animate={isInView ? { opacity: 1, y: 0 } : {}}
initial="hidden" transition={{ duration: 0.45 }}
animate={isInView ? 'visible' : 'hidden'} className="section-heading"
className="mb-16"
> >
<p className="muted-eyebrow mb-3" style={{ fontFamily: 'var(--font-mono)' }}> <p className="section-kicker">Experience</p>
04. Experience <h2 className="section-title">Recent milestones.</h2>
</p>
<h2 className="section-title">Where I&apos;ve been</h2>
<div
className="mt-3 h-1 w-16 rounded-full"
style={{ background: 'linear-gradient(90deg, #F97316, #FB923C)' }}
/>
</motion.div> </motion.div>
{/* Timeline */} <div className="grid gap-3">
<div className="max-w-3xl"> {timelineEvents.map((event, index) => (
{timelineEvents.map((event, i) => ( <motion.article
<TimelineNode
key={event.id} key={event.id}
event={event} initial={{ opacity: 0, y: 18 }}
index={i} animate={isInView ? { opacity: 1, y: 0 } : {}}
isInView={isInView} transition={{ duration: 0.38, delay: index * 0.05 }}
isLast={i === timelineEvents.length - 1} className="card grid gap-4 p-5 sm:grid-cols-[160px_1fr]"
prefersReduced={prefersReduced} >
/> <div>
<p className="text-sm font-bold text-[var(--primary)]">{event.date}</p>
{event.badge && (
<span className="mt-3 inline-flex rounded-full bg-[var(--surface-2)] px-3 py-1 text-xs font-bold text-[var(--muted)]">
{event.badge}
</span>
)}
</div>
<div>
<h3 className="text-lg">{event.title}</h3>
{event.subtitle && (
<p className="mt-1 text-sm font-semibold text-[var(--muted)]">{event.subtitle}</p>
)}
<p className="mt-3 text-sm leading-7 text-[var(--muted-2)]">{event.description}</p>
</div>
</motion.article>
))} ))}
</div> </div>
</div> </div>
......
'use client' 'use client'
import GithubIcon from '@/components/GithubIcon'
import { navLinks, personalInfo } from '@/lib/data' import { navLinks, personalInfo } from '@/lib/data'
const GithubIcon = ({ size = 18 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
)
export default function Footer() { export default function Footer() {
const handleNavClick = (href: string) => { const handleNavClick = (href: string) => {
const id = href.slice(1) document.getElementById(href.slice(1))?.scrollIntoView({ behavior: 'smooth' })
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
} }
return ( return (
<footer <footer className="border-t border-[var(--border)] bg-white py-8" role="contentinfo">
className="py-10 border-t" <div className="section-container flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
style={{ <p className="text-sm text-[var(--muted)]">
background: 'var(--color-surface)', Built with Next.js and Tailwind CSS. Ho Chi Minh City, 2026.
borderColor: 'rgba(255,255,255,0.06)', </p>
}}
role="contentinfo"
>
<div className="section-container">
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
{/* Copyright */}
<p
className="text-sm text-center sm:text-left"
style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-mono)' }}
>
Built with Next.js · Ho Chi Minh City · 2026
</p>
{/* Nav Links */} <nav className="flex flex-wrap gap-3" aria-label="Footer navigation">
<nav className="flex items-center flex-wrap gap-6 justify-center" aria-label="Footer navigation"> {navLinks.map((link) => (
{navLinks.map((link) => ( <button
<button key={link.href}
key={link.href} type="button"
onClick={() => handleNavClick(link.href)} onClick={() => handleNavClick(link.href)}
className="text-xs uppercase tracking-widest transition-colors duration-200 hover:text-[var(--color-primary)]" className="text-sm font-semibold text-[var(--muted)] hover:text-[var(--primary)]"
style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-mono)' }} >
> {link.label}
{link.label} </button>
</button> ))}
))} </nav>
</nav>
{/* GitHub Icon */} <a
<a href={personalInfo.githubUrl}
href={personalInfo.githubUrl} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="flex h-10 w-10 items-center justify-center rounded-md border border-[var(--border)] text-[var(--muted)] hover:text-[var(--primary)]"
id="footer-github" aria-label="GitHub profile"
className="flex items-center justify-center w-10 h-10 rounded-lg transition-all duration-200 hover:scale-110 hover:text-[var(--color-primary)]" >
style={{ <GithubIcon size={18} />
background: 'rgba(255,255,255,0.05)', </a>
border: '1px solid rgba(255,255,255,0.08)',
color: 'var(--color-muted)',
}}
aria-label="GitHub profile"
>
<GithubIcon size={18} />
</a>
</div>
</div> </div>
</footer> </footer>
) )
......
export default function GithubIcon({ size = 18 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
</svg>
)
}
This diff is collapsed.
'use client' 'use client'
import { useState, useEffect, useCallback } from 'react' import { useCallback, useEffect, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Menu, X } from 'lucide-react' import { Menu, X } from 'lucide-react'
import { navLinks, personalInfo } from '@/lib/data' import { navLinks, personalInfo } from '@/lib/data'
// ─── Active Section via Intersection Observer ────────────────
function useActiveSection(sectionIds: string[]): string { function useActiveSection(sectionIds: string[]): string {
const [active, setActive] = useState<string>('') const [active, setActive] = useState('')
useEffect(() => { useEffect(() => {
const observers: IntersectionObserver[] = [] const observers: IntersectionObserver[] = []
...@@ -20,26 +18,27 @@ function useActiveSection(sectionIds: string[]): string { ...@@ -20,26 +18,27 @@ function useActiveSection(sectionIds: string[]): string {
([entry]) => { ([entry]) => {
if (entry.isIntersecting) setActive(id) if (entry.isIntersecting) setActive(id)
}, },
{ rootMargin: '-40% 0px -55% 0px', threshold: 0 } { rootMargin: '-38% 0px -55% 0px', threshold: 0 }
) )
observer.observe(el) observer.observe(el)
observers.push(observer) observers.push(observer)
}) })
return () => observers.forEach((o) => o.disconnect()) return () => observers.forEach((observer) => observer.disconnect())
}, [sectionIds]) }, [sectionIds])
return active return active
} }
export default function Navbar() { export default function Navbar() {
const [scrolled, setScrolled] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const sectionIds = navLinks.map((l) => l.href.slice(1)) const [scrolled, setScrolled] = useState(false)
const sectionIds = navLinks.map((link) => link.href.slice(1))
const active = useActiveSection(sectionIds) const active = useActiveSection(sectionIds)
const handleScroll = useCallback(() => { const handleScroll = useCallback(() => {
setScrolled(window.scrollY > 60) setScrolled(window.scrollY > 24)
}, []) }, [])
useEffect(() => { useEffect(() => {
...@@ -49,128 +48,79 @@ export default function Navbar() { ...@@ -49,128 +48,79 @@ export default function Navbar() {
const handleNavClick = (href: string) => { const handleNavClick = (href: string) => {
setMobileOpen(false) setMobileOpen(false)
const id = href.slice(1) document.getElementById(href.slice(1))?.scrollIntoView({ behavior: 'smooth' })
const el = document.getElementById(id)
el?.scrollIntoView({ behavior: 'smooth' })
} }
return ( return (
<> <header
<motion.nav className={`fixed inset-x-0 top-0 z-50 border-b transition-colors ${
initial={{ y: -80, opacity: 0 }} scrolled ? 'border-[var(--border)] bg-white/92 backdrop-blur' : 'border-transparent bg-white/70 backdrop-blur'
animate={{ y: 0, opacity: 1 }} }`}
transition={{ duration: 0.6, ease: 'easeOut' }} >
className={`fixed top-0 left-0 right-0 z-50 ${ <nav className="section-container flex h-16 items-center justify-between" aria-label="Main navigation">
scrolled ? 'scrolled backdrop-blur-md border-b border-white/5' : 'bg-transparent' <a
}`} href="#hero"
style={{ onClick={(event) => {
backgroundColor: scrolled ? 'rgba(10,15,30,0.92)' : 'transparent', event.preventDefault()
}} window.scrollTo({ top: 0, behavior: 'smooth' })
> }}
<div className="section-container"> className="flex items-center gap-3 font-semibold text-[var(--text)]"
<div className="flex items-center justify-between h-16"> aria-label="Back to top"
{/* Logo Badge */} >
<a <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-[var(--primary)] text-sm font-bold text-white">
href="#" {personalInfo.initials}
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }) }} </span>
className="flex items-center gap-2" <span>{personalInfo.name}</span>
aria-label="Back to top" </a>
>
<span <ul className="hidden items-center gap-1 md:flex">
className="inline-flex items-center justify-center w-10 h-10 rounded-xl font-bold text-sm tracking-widest" {navLinks.map((link) => {
style={{ const sectionId = link.href.slice(1)
background: 'linear-gradient(135deg, #F97316 0%, #FB923C 100%)', const isActive = active === sectionId
color: '#0A0F1E',
fontFamily: 'var(--font-heading)', return (
boxShadow: '0 0 16px rgba(249,115,22,0.4)', <li key={link.href}>
}} <button
onClick={() => handleNavClick(link.href)}
className={`rounded-md px-3 py-2 text-sm font-semibold transition-colors ${
isActive
? 'bg-[var(--surface-2)] text-[var(--primary)]'
: 'text-[var(--muted)] hover:bg-[var(--surface-2)] hover:text-[var(--text)]'
}`}
>
{link.label}
</button>
</li>
)
})}
</ul>
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-md border border-[var(--border)] bg-white text-[var(--text)] md:hidden"
onClick={() => setMobileOpen((value) => !value)}
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileOpen}
>
{mobileOpen ? <X size={19} /> : <Menu size={19} />}
</button>
</nav>
{mobileOpen && (
<div className="border-t border-[var(--border)] bg-white md:hidden">
<div className="section-container py-2">
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => handleNavClick(link.href)}
className="block w-full rounded-md px-3 py-3 text-left text-sm font-semibold text-[var(--muted)] hover:bg-[var(--surface-2)] hover:text-[var(--text)]"
> >
{personalInfo.initials} {link.label}
</span> </button>
</a> ))}
{/* Desktop Nav Links */}
<ul className="hidden md:flex items-center gap-8" role="navigation">
{navLinks.map((link) => {
const sectionId = link.href.slice(1)
const isActive = active === sectionId
return (
<li key={link.href}>
<button
onClick={() => handleNavClick(link.href)}
className={`relative text-sm font-medium transition-colors duration-200 group pb-1 ${
isActive
? 'text-[var(--color-primary)]'
: 'text-[var(--color-muted2)] hover:text-[var(--color-text)]'
}`}
>
{link.label}
{/* Orange underline */}
<span
className={`absolute bottom-0 left-0 h-[2px] rounded-full transition-all duration-300 ${
isActive ? 'w-full' : 'w-0 group-hover:w-full'
}`}
style={{ background: 'linear-gradient(90deg, #F97316, #FB923C)' }}
/>
</button>
</li>
)
})}
</ul>
{/* Mobile Hamburger */}
<button
className="md:hidden flex items-center justify-center w-10 h-10 rounded-lg text-[var(--color-muted2)] hover:text-[var(--color-primary)] transition-colors"
onClick={() => setMobileOpen((v) => !v)}
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileOpen}
>
{mobileOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div> </div>
</div> </div>
</motion.nav> )}
</header>
{/* Mobile Menu Dropdown */}
<AnimatePresence>
{mobileOpen && (
<motion.div
key="mobile-menu"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
className="fixed top-16 left-0 right-0 z-40 border-b border-white/5"
style={{ backgroundColor: 'rgba(10,15,30,0.97)', backdropFilter: 'blur(16px)' }}
>
<ul className="section-container py-4 flex flex-col gap-1" role="navigation">
{navLinks.map((link, i) => {
const sectionId = link.href.slice(1)
const isActive = active === sectionId
return (
<motion.li
key={link.href}
initial={{ opacity: 0, x: -16 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
>
<button
onClick={() => handleNavClick(link.href)}
className={`w-full text-left px-4 py-3 rounded-lg text-sm font-medium transition-all duration-200 ${
isActive
? 'bg-[var(--color-primary-dim)] text-[var(--color-primary)]'
: 'text-[var(--color-muted2)] hover:bg-white/5 hover:text-[var(--color-text)]'
}`}
>
{link.label}
</button>
</motion.li>
)
})}
</ul>
</motion.div>
)}
</AnimatePresence>
</>
) )
} }
'use client' 'use client'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import GithubIcon from '@/components/GithubIcon'
import { type Project } from '@/lib/data' import { type Project } from '@/lib/data'
const GithubIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
)
function TechBadge({ label }: { label: string }) {
return (
<span
className="px-2 py-0.5 rounded text-xs font-medium"
style={{
background: 'rgba(249,115,22,0.1)',
color: 'var(--color-accent)',
border: '1px solid rgba(249,115,22,0.2)',
fontFamily: 'var(--font-mono)',
}}
>
{label}
</span>
)
}
function JsonPreview() {
return (
<div
className="rounded-lg overflow-hidden text-xs"
style={{
background: '#0D1117',
border: '1px solid rgba(249,115,22,0.15)',
fontFamily: 'var(--font-mono)',
}}
>
<div
className="flex items-center gap-2 px-3 py-2 text-xs"
style={{ background: '#161B22', borderBottom: '1px solid rgba(255,255,255,0.05)', color: 'var(--color-muted)' }}
>
<span style={{ color: '#22c55e' }}>GET</span>
<span style={{ color: 'var(--color-muted2)' }}>/api/orders/123</span>
<span className="ml-auto" style={{ color: '#22c55e' }}>200 OK</span>
</div>
<div className="px-4 py-3 space-y-0.5" style={{ lineHeight: 1.7 }}>
<div style={{ color: '#E6EDF3' }}>{`{`}</div>
<JsonLine k="orderId" v="123" vColor="#79C0FF" />
<JsonLine k="status" v={`"CONFIRMED"`} vColor="#A5D6FF" />
<JsonLine k="userId" v="42" vColor="#79C0FF" />
<JsonLine k="total" v="250000" vColor="#79C0FF" />
<JsonLine k="createdAt" v={`"2026-03-15T10:30:00Z"`} vColor="#A5D6FF" />
<div style={{ color: '#E6EDF3' }}>{`}`}</div>
</div>
</div>
)
}
function JsonLine({ k, v, vColor }: { k: string; v: string; vColor: string }) {
return (
<div className="pl-4">
<span style={{ color: '#7EE787' }}>&quot;{k}&quot;</span>
<span style={{ color: '#E6EDF3' }}>: </span>
<span style={{ color: vColor }}>{v}</span>
<span style={{ color: '#E6EDF3' }}>,</span>
</div>
)
}
export interface ProjectCardProps { export interface ProjectCardProps {
project: Project project: Project
index: number index: number
isInView: boolean isInView: boolean
large?: boolean
tall?: boolean
} }
export default function ProjectCard({ project, index, isInView, large = false, tall = false }: ProjectCardProps) { export default function ProjectCard({ project, index, isInView }: ProjectCardProps) {
return ( return (
<motion.div <motion.article
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 18 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: index * 0.1 }} transition={{ duration: 0.4, delay: index * 0.06 }}
className={`project-card group relative rounded-2xl overflow-hidden flex flex-col ${ className="card flex h-full flex-col p-5"
large ? 'bento-featured' : tall ? 'bento-tall' : ''
}`}
style={{
minHeight: tall ? '420px' : 'auto',
}}
> >
<div <div className="mb-4 flex items-start justify-between gap-4">
aria-hidden="true" <div>
className="absolute top-0 left-0 right-0 h-[2px] rounded-t-2xl" <p className="mb-2 text-sm font-bold text-[var(--primary)]">{project.period}</p>
style={{ background: 'linear-gradient(90deg, #F97316, #FB923C)' }} <h3 className="text-xl">{project.name}</h3>
/> {project.role && (
<p className="mt-2 text-sm font-semibold text-[var(--muted)]">{project.role}</p>
<div className="p-6 flex flex-col flex-1">
<div className="flex items-start justify-between mb-4">
<div className="flex-1 pr-4">
<h3
className={`font-bold mb-1 ${large ? 'text-xl' : 'text-base'} group-hover:text-[var(--color-primary)] transition-colors`}
style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)' }}
>
{project.name}
</h3>
{project.role && (
<p
className="text-xs mb-1"
style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-mono)' }}
>
{project.role}
</p>
)}
</div>
<span
className="shrink-0 px-2 py-1 rounded-md text-xs font-medium"
style={{
background: 'rgba(255,255,255,0.05)',
color: 'var(--color-muted)',
border: '1px solid rgba(255,255,255,0.08)',
fontFamily: 'var(--font-mono)',
}}
>
{project.period.split('–')[0].trim()}
</span>
</div>
<p
className={`leading-relaxed flex-1 mb-5 ${large ? 'text-sm' : 'text-sm'}`}
style={{ color: 'var(--color-muted2)' }}
>
{project.description}
</p>
{project.variant === 'featured' && (
<div className="mb-5">
<JsonPreview />
</div>
)}
<div className="flex items-end justify-between gap-3 mt-auto">
<div className="flex flex-wrap gap-1.5">
{project.stack.slice(0, large ? project.stack.length : 3).map((tech) => (
<TechBadge key={tech} label={tech} />
))}
{!large && project.stack.length > 3 && (
<span
className="px-2 py-0.5 rounded text-xs"
style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-mono)' }}
>
+{project.stack.length - 3}
</span>
)}
</div>
{project.githubUrl && (
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
id={`project-github-${project.id}`}
className="shrink-0 flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-200 hover:scale-110"
style={{
background: 'rgba(249,115,22,0.1)',
border: '1px solid rgba(249,115,22,0.25)',
color: 'var(--color-primary)',
}}
aria-label={`GitHub — ${project.name}`}
>
<GithubIcon size={16} />
</a>
)} )}
</div> </div>
</div> </div>
<div <p className="mb-5 flex-1 text-sm leading-7 text-[var(--muted-2)]">{project.description}</p>
className="absolute bottom-0 left-0 right-0 flex items-center justify-center gap-3 py-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300"
style={{ background: 'linear-gradient(to top, rgba(249,115,22,0.15), transparent)' }} <div className="mb-5 flex flex-wrap gap-2">
> {project.stack.slice(0, 6).map((tech) => (
{project.githubUrl && ( <span key={tech} className="tag">
<a {tech}
href={project.githubUrl} </span>
target="_blank" ))}
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-semibold transition-all hover:scale-105"
style={{
background: 'linear-gradient(135deg, #F97316, #FB923C)',
color: '#0A0F1E',
fontFamily: 'var(--font-heading)',
}}
aria-label={`View ${project.name} on GitHub`}
>
<GithubIcon size={14} />
View on GitHub
</a>
)}
</div> </div>
</motion.div>
{project.githubUrl && (
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm font-bold text-[var(--primary)] hover:text-[var(--primary-dark)]"
aria-label={`View ${project.name} on GitHub`}
>
<GithubIcon size={17} />
View source
</a>
)}
</motion.article>
) )
} }
'use client' 'use client'
import { useRef, Suspense } from 'react' import { useRef } from 'react'
import dynamic from 'next/dynamic'
import { motion, useInView } from 'framer-motion' import { motion, useInView } from 'framer-motion'
import { ExternalLink } from 'lucide-react' import GithubIcon from '@/components/GithubIcon'
import { projects } from '@/lib/data' import ProjectCard from '@/components/ProjectCard'
import SkeletonGrid from '@/components/SkeletonCard' import { personalInfo, projects } from '@/lib/data'
const ProjectCard = dynamic(() => import('@/components/ProjectCard'), {
loading: () => <div className="h-full w-full min-h-[260px] bg-[var(--surface)] animate-pulse rounded-xl" />,
})
const GithubIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
</svg>
)
export default function Projects() { export default function Projects() {
const sectionRef = useRef<HTMLElement>(null) const sectionRef = useRef<HTMLElement>(null)
const isInView = useInView(sectionRef, { once: true, margin: '-80px' }) const isInView = useInView(sectionRef, { once: true, margin: '-80px' })
const featured = projects.find((p) => p.variant === 'featured')!
const card2 = projects.find((p) => p.variant === 'card2')!
const card3 = projects.find((p) => p.variant === 'card3')!
const card4 = projects.find((p) => p.variant === 'card4')!
return ( return (
<section <section id="projects" ref={sectionRef} className="section-shell" aria-label="Projects section">
id="projects"
ref={sectionRef}
className="py-24"
aria-label="Projects section"
>
<div className="section-container"> <div className="section-container">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 18 }}
animate={isInView ? { opacity: 1, y: 0 } : {}} animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }} transition={{ duration: 0.45 }}
className="mb-16" className="section-heading flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
> >
<p <div>
className="muted-eyebrow mb-3" <p className="section-kicker">Projects</p>
style={{ fontFamily: 'var(--font-mono)' }} <h2 className="section-title">Selected backend work.</h2>
>
03. Projects
</p>
<h2 className="section-title">
Things I&apos;ve built
</h2>
<div
className="mt-3 h-1 w-16 rounded-full"
style={{ background: 'linear-gradient(90deg, #F97316, #FB923C)' }}
/>
</motion.div>
<Suspense fallback={<SkeletonGrid />}>
<div className="bento-grid">
<div
className="bento-featured"
style={{ gridColumn: 'span 2', gridRow: 'span 1' }}
>
<ProjectCard project={featured} index={0} isInView={isInView} large />
</div>
<div
className="bento-card2"
style={{ gridColumn: '3', gridRow: 'span 2' }}
>
<ProjectCard project={card2} index={1} isInView={isInView} tall />
</div>
<div className="bento-card3">
<ProjectCard project={card3} index={2} isInView={isInView} />
</div>
<div className="bento-card4">
<ProjectCard project={card4} index={3} isInView={isInView} />
</div>
</div> </div>
</Suspense>
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.6 }}
className="mt-10 flex justify-center"
>
<a <a
href="https://github.com/datdat054" href={personalInfo.githubUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
id="projects-github-all" className="btn-secondary w-fit"
className="flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold transition-all duration-200 hover:scale-105"
style={{
background: 'rgba(249,115,22,0.1)',
color: 'var(--color-primary)',
border: '1px solid rgba(249,115,22,0.3)',
fontFamily: 'var(--font-heading)',
}}
> >
<GithubIcon size={16} /> <GithubIcon size={18} />
View All on GitHub GitHub
<ExternalLink size={14} />
</a> </a>
</motion.div> </motion.div>
<div className="grid gap-4 md:grid-cols-2">
{projects.map((project, index) => (
<ProjectCard key={project.id} project={project} index={index} isInView={isInView} />
))}
</div>
</div> </div>
</section> </section>
) )
......
'use client' 'use client'
export function SkeletonCard({ large = false, tall = false }: { large?: boolean; tall?: boolean }) { export function SkeletonCard() {
return ( return (
<div <div className="card flex min-h-[240px] animate-pulse flex-col justify-between p-5">
className="animate-pulse rounded-[12px] p-6 flex flex-col justify-between"
style={{
background: 'var(--surface)',
minHeight: tall ? '420px' : large ? '320px' : '260px',
border: '1px solid rgba(255,255,255,0.05)',
}}
>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="h-5 w-1/3 rounded bg-[var(--surface-2)]" />
<div className="h-6 w-1/3 bg-white/10 rounded" /> <div className="h-7 w-2/3 rounded bg-[var(--surface-2)]" />
<div className="h-4 w-12 bg-white/10 rounded" /> <div className="h-4 w-full rounded bg-[var(--surface-2)]" />
</div> <div className="h-4 w-4/5 rounded bg-[var(--surface-2)]" />
<div className="h-4 w-3/4 bg-white/10 rounded" />
<div className="h-4 w-1/2 bg-white/10 rounded" />
</div> </div>
<div className="flex gap-2 pt-6"> <div className="flex gap-2 pt-6">
<div className="h-6 w-16 bg-white/10 rounded" /> <div className="h-7 w-16 rounded-full bg-[var(--surface-2)]" />
<div className="h-6 w-20 bg-white/10 rounded" /> <div className="h-7 w-20 rounded-full bg-[var(--surface-2)]" />
<div className="h-6 w-14 bg-white/10 rounded" /> <div className="h-7 w-14 rounded-full bg-[var(--surface-2)]" />
</div> </div>
</div> </div>
) )
...@@ -29,19 +20,11 @@ export function SkeletonCard({ large = false, tall = false }: { large?: boolean; ...@@ -29,19 +20,11 @@ export function SkeletonCard({ large = false, tall = false }: { large?: boolean;
export default function SkeletonGrid() { export default function SkeletonGrid() {
return ( return (
<div className="bento-grid"> <div className="grid gap-4 md:grid-cols-2">
<div className="bento-featured" style={{ gridColumn: 'span 2', gridRow: 'span 1' }}> <SkeletonCard />
<SkeletonCard large /> <SkeletonCard />
</div> <SkeletonCard />
<div className="bento-card2" style={{ gridColumn: '3', gridRow: 'span 2' }}> <SkeletonCard />
<SkeletonCard tall />
</div>
<div className="bento-card3">
<SkeletonCard />
</div>
<div className="bento-card4">
<SkeletonCard />
</div>
</div> </div>
) )
} }
'use client' 'use client'
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { motion, AnimatePresence, useInView, useReducedMotion } from 'framer-motion' import { motion, useInView, useReducedMotion } from 'framer-motion'
import { fadeUp, fadeUpReduced } from '@/lib/animations' import { skillCategories, type SkillTab } from '@/lib/data'
import { skillCategories, type SkillTab, type Skill } from '@/lib/data'
// ─── Skill Progress Bar ───────────────────────────────────────
function SkillBar({
skill,
trigger,
index,
prefersReduced,
}: {
skill: Skill
trigger: boolean
index: number
prefersReduced: boolean | null
}) {
const variants = prefersReduced ? fadeUpReduced : fadeUp
return (
<motion.div
variants={variants}
custom={index}
initial="hidden"
animate={trigger ? 'visible' : 'hidden'}
className="skill-card group p-4"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span
className="w-2 h-2 rounded-full shrink-0"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
/>
<span
className="text-sm font-medium"
style={{ color: 'var(--color-text)', fontFamily: 'var(--font-body)' }}
>
{skill.name}
</span>
</div>
<span
className="text-xs font-semibold tabular-nums"
style={{ color: 'var(--primary)', fontFamily: 'var(--font-mono)' }}
>
{skill.level}%
</span>
</div>
<div className="progress-track">
<motion.div
className="progress-fill"
initial={{ width: 0 }}
animate={trigger ? { width: `${skill.level}%` } : { width: 0 }}
transition={{
duration: prefersReduced ? 0 : 1.1,
delay: prefersReduced ? 0 : index * 0.08,
ease: [0.25, 0.46, 0.45, 0.94],
}}
/>
</div>
</motion.div>
)
}
// ─── Skills Section ───────────────────────────────────────────
export default function Skills() { export default function Skills() {
const [activeTab, setActiveTab] = useState<SkillTab>('Backend') const [activeTab, setActiveTab] = useState<SkillTab>('Backend')
const sectionRef = useRef<HTMLElement>(null) const sectionRef = useRef<HTMLElement>(null)
const isInView = useInView(sectionRef, { once: true, margin: '-100px' }) const isInView = useInView(sectionRef, { once: true, margin: '-80px' })
const prefersReduced = useReducedMotion() const prefersReduced = useReducedMotion()
const currentCategory = skillCategories.find((c) => c.tab === activeTab)!
const tabs: SkillTab[] = ['Backend', 'Frontend', 'DevOps', 'Database'] const tabs: SkillTab[] = ['Backend', 'Frontend', 'DevOps', 'Database']
const headingVariants = prefersReduced ? fadeUpReduced : fadeUp const currentCategory = skillCategories.find((category) => category.tab === activeTab)!
return ( return (
<section <section id="skills" ref={sectionRef} className="section-shell" aria-label="Skills section">
id="skills"
ref={sectionRef}
className="py-24"
style={{ background: 'var(--color-surface)' }}
aria-label="Skills section"
>
<div className="section-container"> <div className="section-container">
{/* Section Heading */}
<motion.div <motion.div
variants={headingVariants} initial={{ opacity: 0, y: 18 }}
custom={0} animate={isInView ? { opacity: 1, y: 0 } : {}}
initial="hidden" transition={{ duration: 0.45 }}
animate={isInView ? 'visible' : 'hidden'} className="section-heading"
className="mb-16"
> >
<p className="muted-eyebrow mb-3" style={{ fontFamily: 'var(--font-mono)' }}> <p className="section-kicker">Skills</p>
02. Skills <h2 className="section-title">Tools I use most often.</h2>
</p>
<h2 className="section-title">What I work with</h2>
<div
className="mt-3 h-1 w-16 rounded-full"
style={{ background: 'linear-gradient(90deg, #F97316, #FB923C)' }}
/>
</motion.div> </motion.div>
{/* Tab Switcher */}
<motion.div <motion.div
variants={headingVariants} initial={{ opacity: 0, y: 18 }}
custom={1} animate={isInView ? { opacity: 1, y: 0 } : {}}
initial="hidden" transition={{ duration: 0.45, delay: 0.08 }}
animate={isInView ? 'visible' : 'hidden'} className="mb-6 flex flex-wrap gap-2"
className="flex flex-wrap gap-2 mb-10"
role="tablist" role="tablist"
aria-label="Skill categories" aria-label="Skill categories"
> >
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab} key={tab}
type="button"
role="tab" role="tab"
id={`skills-tab-${tab.toLowerCase()}`}
aria-selected={activeTab === tab} aria-selected={activeTab === tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className="relative px-5 py-2 rounded-full text-sm font-semibold transition-all duration-200 focus:outline-none" className={`rounded-md border px-4 py-2 text-sm font-bold transition-colors ${
style={{ activeTab === tab
background: activeTab === tab ? 'linear-gradient(135deg, #F97316, #FB923C)' : 'rgba(255,255,255,0.05)', ? 'border-[var(--primary)] bg-[var(--primary)] text-white'
color: activeTab === tab ? '#0A0F1E' : 'var(--color-muted2)', : 'border-[var(--border)] bg-white text-[var(--muted)] hover:text-[var(--text)]'
border: activeTab === tab ? 'none' : '1px solid rgba(255,255,255,0.08)', }`}
fontFamily: 'var(--font-heading)',
boxShadow: activeTab === tab ? '0 0 16px rgba(249,115,22,0.35)' : 'none',
}}
> >
{tab} {tab}
</button> </button>
))} ))}
</motion.div> </motion.div>
{/* Skills Grid — AnimatePresence tab slide */} <div className="grid gap-4 sm:grid-cols-2">
<AnimatePresence mode="wait"> {currentCategory.skills.map((skill, index) => (
<motion.div <motion.div
key={activeTab} key={`${activeTab}-${skill.name}`}
initial={prefersReduced ? { opacity: 0 } : { opacity: 0, x: 16 }} initial={{ opacity: 0, y: prefersReduced ? 0 : 14 }}
animate={prefersReduced ? { opacity: 1 } : { opacity: 1, x: 0 }} animate={isInView ? { opacity: 1, y: 0 } : {}}
exit={prefersReduced ? { opacity: 0 } : { opacity: 0, x: -16 }} transition={{ duration: 0.35, delay: index * 0.03 }}
transition={{ duration: prefersReduced ? 0.2 : 0.22 }} className="card p-4"
role="tabpanel" >
aria-labelledby={`skills-tab-${activeTab.toLowerCase()}`} <div className="mb-3 flex items-center justify-between gap-4">
className="grid grid-cols-1 sm:grid-cols-2 gap-x-12 gap-y-6" <span className="font-semibold text-[var(--text)]">{skill.name}</span>
> <span className="text-sm font-bold text-[var(--primary)]">{skill.level}%</span>
{currentCategory.skills.map((skill, i) => ( </div>
<SkillBar <div className="progress-track">
key={`${activeTab}-${skill.name}`} <motion.div
skill={skill} className="progress-fill"
trigger={isInView} initial={{ width: 0 }}
index={i} animate={isInView ? { width: `${skill.level}%` } : { width: 0 }}
prefersReduced={prefersReduced} transition={{ duration: prefersReduced ? 0 : 0.8, delay: index * 0.03 }}
/> />
))} </div>
</motion.div> </motion.div>
</AnimatePresence> ))}
</div>
{/* Languages */}
<motion.div
variants={headingVariants}
custom={2}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
className="mt-16 pt-10 border-t"
style={{ borderColor: 'rgba(255,255,255,0.06)' }}
>
<p
className="text-xs uppercase tracking-widest mb-5 font-medium"
style={{ color: 'var(--color-muted)', fontFamily: 'var(--font-mono)' }}
>
Languages
</p>
<div className="flex flex-wrap gap-3">
{['Java', 'SQL', 'JavaScript', 'Python', 'C++'].map((lang) => (
<span
key={lang}
className="px-4 py-2 rounded-lg text-sm font-semibold transition-all duration-200 hover:scale-105"
style={{
background: 'var(--color-surface2)',
color: 'var(--color-text)',
border: '1px solid var(--color-border-subtle)',
fontFamily: 'var(--font-mono)',
}}
>
{lang}
</span>
))}
</div>
</motion.div>
</div> </div>
</section> </section>
) )
......
...@@ -9,9 +9,12 @@ function ProgressBarContent() { ...@@ -9,9 +9,12 @@ function ProgressBarContent() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
useEffect(() => { useEffect(() => {
setLoading(true) const start = setTimeout(() => setLoading(true), 0)
const timer = setTimeout(() => setLoading(false), 500) const timer = setTimeout(() => setLoading(false), 500)
return () => clearTimeout(timer) return () => {
clearTimeout(start)
clearTimeout(timer)
}
}, [pathname, searchParams]) }, [pathname, searchParams])
if (!loading) return null if (!loading) return null
...@@ -20,8 +23,8 @@ function ProgressBarContent() { ...@@ -20,8 +23,8 @@ function ProgressBarContent() {
<div <div
className="fixed top-0 left-0 right-0 h-0.5 z-[10000] animate-pulse transition-all duration-300" className="fixed top-0 left-0 right-0 h-0.5 z-[10000] animate-pulse transition-all duration-300"
style={{ style={{
background: 'linear-gradient(90deg, #F97316, #FB923C)', background: 'var(--primary)',
boxShadow: '0 0 10px #F97316', boxShadow: '0 0 10px rgba(37, 99, 235, 0.35)',
}} }}
/> />
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment