Commit e4a51f98 authored by QuanMB's avatar QuanMB

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

parent 9bca74c2
@import "tailwindcss";
:root {
/* Default Dark Mode Values */
--primary: #3b82f6;
--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);
}
@theme {
--color-primary: #3b82f6;
--color-primary-glow: rgba(59, 130, 246, 0.1);
--color-secondary: #8b5cf6;
--color-primary: var(--primary);
--color-primary-glow: var(--primary-glow);
--color-secondary: var(--secondary);
--color-dark-bg: #0b0f19;
--color-surface: #111827;
--color-surface-border: #1f2937;
--color-surface-hover-border: #374151;
--color-nav-bg: rgba(11, 15, 25, 0.8);
--color-nav-border: rgba(31, 41, 55, 0.55);
--color-dark-bg: var(--bg);
--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-inter: var(--font-inter), sans-serif;
......@@ -64,37 +93,47 @@
html {
scroll-behavior: smooth;
color-scheme: dark;
transition: color-scheme 0.3s ease;
}
html.light {
color-scheme: light;
}
body {
background-color: var(--color-dark-bg);
color: #f9fafb;
background-color: var(--bg);
color: var(--text-primary);
font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
min-height: 100vh;
overflow-x: hidden;
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 {
font-family: var(--font-outfit), sans-serif;
color: #f9fafb;
color: var(--text-primary);
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) */
@utility glass-panel {
background-color: #111827;
border: 1px solid #1f2937;
background-color: var(--surface);
border: 1px solid var(--surface-border);
border-radius: 18px;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@utility glass-panel-hover {
&:hover {
border-color: #374151;
border-color: var(--surface-hover-border);
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 @@
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-dark-bg);
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: #1f2937;
background: var(--surface-border);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #3b82f6;
background: var(--primary);
}
/* Gradient highlights */
.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-text-fill-color: transparent;
}
.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-text-fill-color: transparent;
}
......@@ -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>
Aspiring Intern
</div>
<h1 className="text-5xl sm:text-7xl font-extrabold mb-6 leading-[1.05] tracking-tighter text-white">
Hi, I am <span className="bg-gradient-to-r from-white to-slate-300 bg-clip-text text-transparent">Bui Minh Quan</span>
<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-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>
</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.
Experienced in building RESTful APIs and modern web applications with Spring Boot, React, and Next.js.
</p>
......@@ -66,7 +66,7 @@ export default function Hero() {
<a
href="#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
</a>
......@@ -107,7 +107,7 @@ export default function Hero() {
</div>
{/* 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
width="16"
height="16"
......
......@@ -14,6 +14,7 @@ export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [activeSection, setActiveSection] = useState("home");
const [theme, setTheme] = useState<"dark" | "light">("dark");
useEffect(() => {
const handleScroll = () => {
......@@ -28,6 +29,22 @@ export default function Navbar() {
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(() => {
const sections = NAV_ITEMS.map((item) => document.getElementById(item.id));
......@@ -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) => {
e.preventDefault();
setMobileMenuOpen(false);
......@@ -75,13 +103,13 @@ export default function Navbar() {
return (
<header className={`fixed top-0 left-0 w-full z-50 transition-all duration-300 border-b ${
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"
}`}>
<div className="max-w-[1200px] mx-auto px-6 flex justify-between items-center">
{/* 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">
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>
<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_var(--color-primary)]"></span>
</a>
{/* Desktop Navigation */}
......@@ -94,8 +122,8 @@ export default function Navbar() {
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 ${
activeSection === item.id
? "text-white after:w-full"
: "text-slate-400 hover:text-white"
? "text-text-primary after:w-full"
: "text-text-secondary hover:text-text-primary"
}`}
>
{item.label}
......@@ -105,35 +133,87 @@ export default function Navbar() {
</ul>
</nav>
{/* CTA Button */}
<div className="hidden md:block">
{/* Action Controls & CTA */}
<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
href="#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
</a>
</div>
{/* Mobile Hamburger Toggle */}
<button
className="md:hidden text-slate-400 hover:text-white focus:outline-none z-50 p-1"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2.5">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
{/* 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>
) : (
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
<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>
)}
</svg>
</button>
</button>
<button
className="text-text-secondary hover:text-text-primary focus:outline-none p-1"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth="2.5">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
{/* Mobile Menu Dropdown */}
<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"
}`}
>
......@@ -146,7 +226,7 @@ export default function Navbar() {
className={`font-inter text-sm uppercase tracking-wider font-semibold block py-2 transition-colors ${
activeSection === item.id
? "text-primary"
: "text-slate-400 hover:text-white"
: "text-text-secondary hover:text-text-primary"
}`}
>
{item.label}
......@@ -157,7 +237,7 @@ export default function Navbar() {
<a
href="#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
</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