Commit e4a51f98 authored by QuanMB's avatar QuanMB

feat(ui): add light and dark mode theme toggle

parent 9bca74c2
@import "tailwindcss"; @import "tailwindcss";
@theme { :root {
--color-primary: #3b82f6; /* Default Dark Mode Values */
--color-primary-glow: rgba(59, 130, 246, 0.1); --primary: #3b82f6;
--color-secondary: #8b5cf6; --primary-glow: rgba(59, 130, 246, 0.1);
--secondary: #8b5cf6;
--bg: #0b0f19;
--surface: #111827;
--surface-border: rgba(255, 255, 255, 0.05);
--surface-hover-border: rgba(255, 255, 255, 0.12);
--text-primary: #f9fafb;
--text-secondary: #94a3b8;
--nav-bg: rgba(11, 15, 25, 0.8);
--nav-border: rgba(31, 41, 55, 0.55);
}
:root.light {
/* Light Mode Values */
--primary: #2563eb;
--primary-glow: rgba(37, 99, 235, 0.08);
--secondary: #7c3aed;
--bg: #f8fafc;
--surface: #ffffff;
--surface-border: rgba(0, 0, 0, 0.08);
--surface-hover-border: rgba(0, 0, 0, 0.15);
--text-primary: #0f172a;
--text-secondary: #475569;
--nav-bg: rgba(248, 250, 252, 0.8);
--nav-border: rgba(0, 0, 0, 0.06);
}
--color-dark-bg: #0b0f19; @theme {
--color-surface: #111827; --color-primary: var(--primary);
--color-surface-border: #1f2937; --color-primary-glow: var(--primary-glow);
--color-surface-hover-border: #374151; --color-secondary: var(--secondary);
--color-nav-bg: rgba(11, 15, 25, 0.8); --color-dark-bg: var(--bg);
--color-nav-border: rgba(31, 41, 55, 0.55); --color-surface: var(--surface);
--color-surface-border: var(--surface-border);
--color-surface-hover-border: var(--surface-hover-border);
--color-text-primary: var(--text-primary);
--color-text-secondary: var(--text-secondary);
--font-outfit: var(--font-outfit), sans-serif; --font-outfit: var(--font-outfit), sans-serif;
--font-inter: var(--font-inter), sans-serif; --font-inter: var(--font-inter), sans-serif;
...@@ -64,37 +93,47 @@ ...@@ -64,37 +93,47 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
color-scheme: dark; color-scheme: dark;
transition: color-scheme 0.3s ease;
}
html.light {
color-scheme: light;
} }
body { body {
background-color: var(--color-dark-bg); background-color: var(--bg);
color: #f9fafb; color: var(--text-primary);
font-family: var(--font-inter), system-ui, -apple-system, sans-serif; font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
line-height: 1.6; line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: var(--font-outfit), sans-serif; font-family: var(--font-outfit), sans-serif;
color: #f9fafb; color: var(--text-primary);
letter-spacing: -0.02em; letter-spacing: -0.02em;
transition: color 0.3s ease;
}
/* Transition for cards & buttons when theme switches */
div, section, header, nav, a, button, input, textarea, span {
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
} }
} }
/* Minimal design utility cards (Stripe/Linear style) */ /* Minimal design utility cards (Stripe/Linear style) */
@utility glass-panel { @utility glass-panel {
background-color: #111827; background-color: var(--surface);
border: 1px solid #1f2937; border: 1px solid var(--surface-border);
border-radius: 18px; border-radius: 18px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
} }
@utility glass-panel-hover { @utility glass-panel-hover {
&:hover { &:hover {
border-color: #374151; border-color: var(--surface-hover-border);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.5); box-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.2);
} }
} }
...@@ -103,25 +142,25 @@ ...@@ -103,25 +142,25 @@
width: 6px; width: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--color-dark-bg); background: var(--bg);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #1f2937; background: var(--surface-border);
border-radius: 10px; border-radius: 10px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #3b82f6; background: var(--primary);
} }
/* Gradient highlights */ /* Gradient highlights */
.gradient-text { .gradient-text {
background: linear-gradient(135deg, #ffffff 40%, #94a3b8 100%); background: linear-gradient(135deg, var(--text-primary) 40%, var(--text-secondary) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
.gradient-accent-text { .gradient-accent-text {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
...@@ -34,11 +34,11 @@ export default function Hero() { ...@@ -34,11 +34,11 @@ export default function Hero() {
<span className="inline-block w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_6px_var(--color-primary)]"></span> <span className="inline-block w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_6px_var(--color-primary)]"></span>
Aspiring Intern Aspiring Intern
</div> </div>
<h1 className="text-5xl sm:text-7xl font-extrabold mb-6 leading-[1.05] tracking-tighter text-white"> <h1 className="text-5xl sm:text-7xl font-extrabold mb-6 leading-[1.05] tracking-tighter text-text-primary">
Hi, I am <span className="bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent">Bui Minh Quan</span> Hi, I am <span className="bg-gradient-to-r from-text-primary to-text-secondary bg-clip-text text-transparent">Bui Minh Quan</span>
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent block">Full-Stack Developer</span> <span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent block">Full-Stack Developer</span>
</h1> </h1>
<p className="text-base sm:text-lg text-slate-400 mb-8 leading-relaxed max-w-[500px]"> <p className="text-base sm:text-lg text-text-secondary mb-8 leading-relaxed max-w-[500px]">
Information Technology student at PTIT HCMC specializing in Software Engineering. Information Technology student at PTIT HCMC specializing in Software Engineering.
Experienced in building RESTful APIs and modern web applications with Spring Boot, React, and Next.js. Experienced in building RESTful APIs and modern web applications with Spring Boot, React, and Next.js.
</p> </p>
...@@ -66,7 +66,7 @@ export default function Hero() { ...@@ -66,7 +66,7 @@ export default function Hero() {
<a <a
href="#contact" href="#contact"
onClick={(e) => handleScrollTo(e, "contact")} onClick={(e) => handleScrollTo(e, "contact")}
className="bg-[#111827] hover:bg-[#1f2937] text-white border border-white/5 px-7 py-3 rounded-full font-semibold text-[15px] hover:-translate-y-0.5 transition-all duration-200 cursor-pointer" className="bg-surface hover:bg-slate-500/10 text-text-primary border border-surface-border px-7 py-3 rounded-full font-semibold text-[15px] hover:-translate-y-0.5 transition-all duration-200 cursor-pointer"
> >
Contact Info Contact Info
</a> </a>
...@@ -107,7 +107,7 @@ export default function Hero() { ...@@ -107,7 +107,7 @@ export default function Hero() {
</div> </div>
{/* Floating Skill Badge */} {/* Floating Skill Badge */}
<div className="absolute bg-[#111827] border border-[#1f2937] px-4 py-2 rounded-full text-xs font-semibold text-slate-300 flex items-center gap-2 bottom-8 right-0 sm:right-4 animate-float" style={{ animationDelay: "1s" }}> <div className="absolute bg-surface border border-surface-border px-4 py-2 rounded-full text-xs font-semibold text-text-primary flex items-center gap-2 bottom-8 right-0 sm:right-4 animate-float" style={{ animationDelay: "1s" }}>
<svg <svg
width="16" width="16"
height="16" height="16"
......
...@@ -14,6 +14,7 @@ export default function Navbar() { ...@@ -14,6 +14,7 @@ export default function Navbar() {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [activeSection, setActiveSection] = useState("home"); const [activeSection, setActiveSection] = useState("home");
const [theme, setTheme] = useState<"dark" | "light">("dark");
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
...@@ -28,6 +29,22 @@ export default function Navbar() { ...@@ -28,6 +29,22 @@ export default function Navbar() {
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, []); }, []);
useEffect(() => {
// Check saved theme or preference
const savedTheme = localStorage.getItem("theme") as "dark" | "light" | null;
if (savedTheme) {
setTheme(savedTheme);
if (savedTheme === "light") {
document.documentElement.classList.add("light");
} else {
document.documentElement.classList.remove("light");
}
} else {
setTheme("dark");
document.documentElement.classList.remove("light");
}
}, []);
useEffect(() => { useEffect(() => {
const sections = NAV_ITEMS.map((item) => document.getElementById(item.id)); const sections = NAV_ITEMS.map((item) => document.getElementById(item.id));
...@@ -56,6 +73,17 @@ export default function Navbar() { ...@@ -56,6 +73,17 @@ export default function Navbar() {
}; };
}, []); }, []);
const toggleTheme = () => {
const nextTheme = theme === "dark" ? "light" : "dark";
setTheme(nextTheme);
localStorage.setItem("theme", nextTheme);
if (nextTheme === "light") {
document.documentElement.classList.add("light");
} else {
document.documentElement.classList.remove("light");
}
};
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => { const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault(); e.preventDefault();
setMobileMenuOpen(false); setMobileMenuOpen(false);
...@@ -75,13 +103,13 @@ export default function Navbar() { ...@@ -75,13 +103,13 @@ export default function Navbar() {
return ( return (
<header className={`fixed top-0 left-0 w-full z-50 transition-all duration-300 border-b ${ <header className={`fixed top-0 left-0 w-full z-50 transition-all duration-300 border-b ${
scrolled scrolled
? "py-3 bg-dark-bg/75 backdrop-blur-md border-white/5 shadow-md" ? "py-3 bg-dark-bg/75 backdrop-blur-md border-surface-border shadow-md"
: "py-6 bg-transparent border-transparent" : "py-6 bg-transparent border-transparent"
}`}> }`}>
<div className="max-w-[1200px] mx-auto px-6 flex justify-between items-center"> <div className="max-w-[1200px] mx-auto px-6 flex justify-between items-center">
{/* Logo */} {/* Logo */}
<a href="#home" onClick={(e) => handleLinkClick(e, "home")} className="font-outfit font-bold text-lg text-white flex items-center gap-1.5 tracking-tight"> <a href="#home" onClick={(e) => handleLinkClick(e, "home")} className="font-outfit font-bold text-lg text-text-primary flex items-center gap-1.5 tracking-tight">
MinhQuan<span className="inline-block w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_6px_rgba(59,130,246,0.6)]"></span> MinhQuan<span className="inline-block w-1.5 h-1.5 bg-primary rounded-full shadow-[0_0_6px_var(--color-primary)]"></span>
</a> </a>
{/* Desktop Navigation */} {/* Desktop Navigation */}
...@@ -94,8 +122,8 @@ export default function Navbar() { ...@@ -94,8 +122,8 @@ export default function Navbar() {
onClick={(e) => handleLinkClick(e, item.id)} onClick={(e) => handleLinkClick(e, item.id)}
className={`font-inter text-xs uppercase tracking-wider font-semibold relative py-1 transition-colors duration-200 after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-0 after:h-[1px] after:bg-primary after:transition-all after:duration-250 hover:after:w-full ${ className={`font-inter text-xs uppercase tracking-wider font-semibold relative py-1 transition-colors duration-200 after:content-[''] after:absolute after:bottom-0 after:left-0 after:w-0 after:h-[1px] after:bg-primary after:transition-all after:duration-250 hover:after:w-full ${
activeSection === item.id activeSection === item.id
? "text-white after:w-full" ? "text-text-primary after:w-full"
: "text-slate-400 hover:text-white" : "text-text-secondary hover:text-text-primary"
}`} }`}
> >
{item.label} {item.label}
...@@ -105,20 +133,71 @@ export default function Navbar() { ...@@ -105,20 +133,71 @@ export default function Navbar() {
</ul> </ul>
</nav> </nav>
{/* CTA Button */} {/* Action Controls & CTA */}
<div className="hidden md:block"> <div className="hidden md:flex items-center gap-4">
{/* Light/Dark Toggle */}
<button
onClick={toggleTheme}
className="w-8 h-8 rounded-full border border-surface-border flex items-center justify-center text-text-secondary hover:text-text-primary hover:border-primary/30 transition-all cursor-pointer"
aria-label="Toggle Theme"
>
{theme === "dark" ? (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
) : (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
)}
</button>
<a <a
href="#contact" href="#contact"
onClick={(e) => handleLinkClick(e, "contact")} onClick={(e) => handleLinkClick(e, "contact")}
className="border border-white/10 text-white px-5 py-2 rounded-full font-medium text-xs tracking-wide hover:border-primary hover:bg-primary/5 transition-all duration-200" className="border border-surface-border text-text-primary px-5 py-2 rounded-full font-semibold text-xs tracking-wide hover:border-primary hover:bg-primary/5 transition-all duration-200"
> >
Hire Me Hire Me
</a> </a>
</div> </div>
{/* Mobile Hamburger Toggle */} {/* Mobile Action buttons & Menu Trigger */}
<div className="flex md:hidden items-center gap-3">
{/* Mobile Theme Toggle */}
<button
onClick={toggleTheme}
className="w-8 h-8 rounded-full border border-surface-border flex items-center justify-center text-text-secondary hover:text-text-primary transition-all cursor-pointer"
aria-label="Toggle Theme"
>
{theme === "dark" ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
)}
</button>
<button <button
className="md:hidden text-slate-400 hover:text-white focus:outline-none z-50 p-1" className="text-text-secondary hover:text-text-primary focus:outline-none p-1"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu" aria-label="Toggle menu"
> >
...@@ -130,10 +209,11 @@ export default function Navbar() { ...@@ -130,10 +209,11 @@ export default function Navbar() {
)} )}
</svg> </svg>
</button> </button>
</div>
{/* Mobile Menu Dropdown */} {/* Mobile Menu Dropdown */}
<div <div
className={`fixed top-0 right-0 w-[75%] max-w-[320px] h-screen bg-[#0e1322] border-l border-white/5 p-8 pt-24 flex flex-col gap-6 z-40 transition-transform duration-300 ease-in-out ${ className={`fixed top-0 right-0 w-[75%] max-w-[320px] h-screen bg-surface border-l border-surface-border p-8 pt-24 flex flex-col gap-6 z-40 transition-transform duration-300 ease-in-out ${
mobileMenuOpen ? "translate-x-0" : "translate-x-full" mobileMenuOpen ? "translate-x-0" : "translate-x-full"
}`} }`}
> >
...@@ -146,7 +226,7 @@ export default function Navbar() { ...@@ -146,7 +226,7 @@ export default function Navbar() {
className={`font-inter text-sm uppercase tracking-wider font-semibold block py-2 transition-colors ${ className={`font-inter text-sm uppercase tracking-wider font-semibold block py-2 transition-colors ${
activeSection === item.id activeSection === item.id
? "text-primary" ? "text-primary"
: "text-slate-400 hover:text-white" : "text-text-secondary hover:text-text-primary"
}`} }`}
> >
{item.label} {item.label}
...@@ -157,7 +237,7 @@ export default function Navbar() { ...@@ -157,7 +237,7 @@ export default function Navbar() {
<a <a
href="#contact" href="#contact"
onClick={(e) => handleLinkClick(e, "contact")} onClick={(e) => handleLinkClick(e, "contact")}
className="border border-white/10 text-white py-2.5 rounded-full font-medium text-xs text-center hover:border-primary hover:bg-primary/5 transition-all mt-4" className="border border-surface-border text-text-primary py-2.5 rounded-full font-semibold text-xs text-center hover:border-primary hover:bg-primary/5 transition-all mt-4"
> >
Hire Me Hire Me
</a> </a>
......
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