Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
P
portfolio
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
QuanMB
portfolio
Commits
e4a51f98
Commit
e4a51f98
authored
Jun 29, 2026
by
QuanMB
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): add light and dark mode theme toggle
parent
9bca74c2
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
170 additions
and
51 deletions
+170
-51
globals.css
src/app/globals.css
+62
-23
Hero.tsx
src/components/Hero/Hero.tsx
+5
-5
Navbar.tsx
src/components/Navbar/Navbar.tsx
+103
-23
No files found.
src/app/globals.css
View file @
e4a51f98
@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
;
}
src/components/Hero/Hero.tsx
View file @
e4a51f98
...
...
@@ -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"
...
...
src/components/Navbar/Navbar.tsx
View file @
e4a51f98
...
...
@@ -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
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment