Skip to content

Commit 2d9bae1

Browse files
committed
refactor(ui): fix broken mobile layout and clean up component structure
1 parent 348303e commit 2d9bae1

File tree

6 files changed

+218
-124
lines changed

6 files changed

+218
-124
lines changed

app/(root)/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function Portfolio() {
5050
const renderActiveTab = () => {
5151
switch (activeTab) {
5252
case "profile":
53-
return <Profile />
53+
return <Profile setActiveTab={setActiveTab} />
5454
case "experience":
5555
return <Experience />
5656
case "projects":
@@ -65,7 +65,7 @@ export default function Portfolio() {
6565

6666
return (
6767
<div className="min-h-screen w-full relative">
68-
<Header setActiveTab={setActiveTab} />
68+
<Header />
6969
<Navigation activeTab={activeTab} setActiveTab={setActiveTab} />
7070
<main className="container mx-auto px-3 lg:px-20 xl:px-32 py-8">{renderActiveTab()}</main>
7171
<Footer />

app/_components/Hero.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"use client"
2+
3+
import { Button } from '@/components/ui/button'
4+
import { cn } from '@/lib/utils'
5+
import { useTheme } from 'next-themes'
6+
import Image from 'next/image'
7+
import React, { useEffect, useState } from 'react'
8+
import { TABS } from '../(root)/page'
9+
10+
interface HeroProps {
11+
setActiveTab: (tab: typeof TABS[number]) => void
12+
}
13+
14+
export default function Hero({ setActiveTab }: HeroProps) {
15+
const { resolvedTheme } = useTheme()
16+
const [isMounted, setIsMounted] = useState(false)
17+
18+
useEffect(() => {
19+
setIsMounted(true)
20+
}, [])
21+
22+
if (!isMounted) {
23+
// Skeleton fallback for hydration-safe UI
24+
return (
25+
<div className="sm:hidden border-b border-border pb-8 px-2 py-4 lg:px-20 xl:px-32 mb-6">
26+
<div className="flex items-center justify-between gap-1 pt-5 animate-pulse">
27+
<div className="flex space-x-4 gap-4 flex-col">
28+
<div className="size-20 rounded-lg bg-muted/40" />
29+
<div>
30+
<div className="h-5 w-32 bg-muted/40 rounded mb-2" />
31+
<div className="h-4 w-52 bg-muted/40 rounded" />
32+
</div>
33+
<div className="flex gap-2">
34+
<div className="h-8 w-32 bg-muted/40 rounded-full" />
35+
<div className="h-8 w-24 bg-muted/40 rounded-full" />
36+
</div>
37+
</div>
38+
</div>
39+
</div>
40+
)
41+
}
42+
43+
return (
44+
<div className="sm:hidden border-b border-border pb-8 px-2 py-4 lg:px-20 xl:px-32 mb-6">
45+
<div className="flex items-center justify-between gap-1 pt-5">
46+
<div className="flex space-x-4 gap-4 flex-col">
47+
<div className="relative">
48+
<div
49+
className={cn(
50+
"size-20 rounded-lg bg-gradient-to-br flex items-center justify-center relative outline outline-offset-[3px] outline-border",
51+
resolvedTheme === "dark"
52+
? "from-zinc-600 to-zinc-900"
53+
: "from-zinc-50 to-zinc-200"
54+
)}
55+
>
56+
<Image
57+
src="/avatar-p.png"
58+
alt="Profile photo"
59+
height={80}
60+
width={80}
61+
className="absolute h-full w-full top-0 left-0 rounded-lg object-cover saturate-100"
62+
aria-label="Profile photo of Alex Developer"
63+
quality={100}
64+
/>
65+
</div>
66+
</div>
67+
<div>
68+
<h1 className="text-xl font-medium flex items-center gap-2">
69+
Alex Developer
70+
<div className="size-1.5 animate-pulse relative after:content-[''] after:absolute flex items-center justify-center after:h-full after:w-full after:bg-green-400 after:rounded-full after:animate-ping rounded-full bg-primary"></div>
71+
</h1>
72+
<p className="text-muted-foreground text-sm">
73+
Full Stack Developer & UI/UX Designer
74+
</p>
75+
</div>
76+
77+
<div className="flex justify-between gap-2 items-center">
78+
<a
79+
href="/resume.pdf"
80+
target="_blank"
81+
rel="noopener noreferrer"
82+
aria-label="Open Resume"
83+
>
84+
<Button
85+
variant="outline"
86+
size="sm"
87+
className="rounded-full text-primary hover:bg-primary/70"
88+
>
89+
Download Resume
90+
</Button>
91+
</a>
92+
<Button
93+
variant="outline"
94+
size="sm"
95+
className="rounded-full text-muted-foreground"
96+
onClick={() => setActiveTab("contact")}
97+
aria-label="Contact Me"
98+
>
99+
Contact Me
100+
</Button>
101+
</div>
102+
</div>
103+
</div>
104+
</div>
105+
)
106+
}

app/_components/header.tsx

Lines changed: 46 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,105 +9,76 @@ import { cn } from "@/lib/utils"
99
import HeaderSkeleton from './skeleton/header'
1010
import Image from 'next/image'
1111
import Link from 'next/link'
12-
import { TABS } from '../(root)/page'
13-
interface HeaderProps {
14-
activeTab?: typeof TABS[number]
15-
setActiveTab: (tab: typeof TABS[number]) => void
16-
}
17-
export default function Header({ setActiveTab }: HeaderProps) {
12+
13+
export default function Header() {
1814
const { resolvedTheme } = useTheme()
1915
const [isMounted, setIsMounted] = useState(false)
16+
const [showImage, setShowImage] = useState(true)
2017

2118
useEffect(() => {
2219
setIsMounted(true)
20+
21+
const handleScroll = () => {
22+
const isMobileView = window.innerWidth <= 640
23+
const scrolledEnough = window.scrollY >= 120
24+
setShowImage(!isMobileView || scrolledEnough)
25+
}
26+
27+
handleScroll()
28+
window.addEventListener('scroll', handleScroll)
29+
window.addEventListener('resize', handleScroll)
30+
31+
return () => {
32+
window.removeEventListener('scroll', handleScroll)
33+
window.removeEventListener('resize', handleScroll)
34+
}
2335
}, [])
2436

25-
if (!isMounted) {
26-
return (
27-
<>
28-
<HeaderSkeleton />
29-
</>
30-
)
31-
}
37+
38+
if (!isMounted) return <HeaderSkeleton />
3239

3340
return (
3441
<header className="sticky top-0 z-40 bg-background/80 backdrop-blur-md border-b border-border">
3542
<div className="container mx-auto px-4 py-4 lg:px-20 xl:px-32">
36-
<div className="flex items-center justify-between gap-1 pt-6 sm:pt-0">
37-
<div className="flex sm:items-center space-x-4 sm:flex-row gap-4 sm:gap-0 flex-col">
43+
<div className="flex items-center justify-between gap-1">
44+
<div className="flex items-center space-x-4">
3845
<div className="relative">
39-
40-
{/* use image instead */}
41-
<div
42-
className={cn(
43-
"size-20 sm:size-13 rounded-lg bg-gradient-to-br flex items-center justify-center relative outline outline-offset-[3px] outline-border",
44-
resolvedTheme === "dark"
45-
? "from-zinc-600 to-zinc-900"
46-
: "from-zinc-50 to-zinc-200"
47-
)}
48-
>
49-
50-
<Image src="/avatar-p.png" alt='Profile photo' height="40" width="40" className='absolute h-full w-full top-0 left-0 rounded-xl object-cover saturate-100' aria-label='Profile photo of Alex Developer' quality={100} />
51-
</div>
52-
53-
{/* use dots grid instead */}
54-
55-
{/* <div
56-
className={cn(
57-
"size-10 sm:size-13 rounded-lg bg-gradient-to-br flex items-center justify-center ",
58-
resolvedTheme === "dark"
59-
? "from-zinc-600 to-zinc-900"
60-
: "from-zinc-50 to-zinc-200"
61-
)}
62-
>
63-
64-
<div className="grid grid-cols-2 gap-1">
65-
<div className="size-2 sm:size-3 bg-zinc-300 rounded-sm" />
66-
<div className="size-2 sm:size-3 bg-zinc-400 rounded-sm" />
67-
<div className="size-2 sm:size-3 bg-zinc-500 rounded-sm" />
68-
<div className="size-2 sm:size-3 bg-zinc-600 rounded-sm" />
46+
{/* Only show image when showImage is true */}
47+
{showImage && (
48+
<div
49+
className={cn(
50+
"size-10 sm:size-13 rounded-lg bg-gradient-to-br flex items-center justify-center relative outline outline-offset-[3px] outline-border",
51+
resolvedTheme === "dark"
52+
? "from-zinc-600 to-zinc-900"
53+
: "from-zinc-50 to-zinc-200"
54+
)}
55+
>
56+
<Image
57+
src="/avatar-p.png"
58+
alt="Profile photo"
59+
height={40}
60+
width={40}
61+
className="absolute h-full w-full top-0 left-0 rounded-xl object-cover saturate-100"
62+
aria-label="Profile photo of Alex Developer"
63+
quality={100}
64+
/>
6965
</div>
70-
</div> */}
66+
)}
7167
</div>
72-
<div>
73-
<h1 className="text-xl font-medium flex items-center gap-2">
68+
<div className='hidden sm:block'>
69+
<h1 className="text-base sm:text-xl font-medium flex items-center gap-2">
7470
Alex Developer
7571
<div className="size-1.5 sm:size-2.5 animate-pulse relative after:content-[''] after:absolute flex items-center justify-center after:h-full after:w-full after:bg-green-400 after:rounded-full after:animate-ping rounded-full bg-primary"></div>
7672
</h1>
77-
<p className="text-muted-foreground text-sm">
73+
<p className="text-muted-foreground text-[11px] sm:text-sm">
7874
Full Stack Developer & UI/UX Designer
7975
</p>
8076
</div>
81-
82-
<div className="sm:hidden flex justify-between gap-2 items-center">
83-
{/* Download resume button one more button */}
84-
<a
85-
href="/resume.pdf"
86-
target="_blank"
87-
rel="noopener noreferrer"
88-
aria-label="Open Resume"
89-
>
90-
<Button
91-
variant="outline"
92-
size="sm"
93-
className="rounded-full text-primary hover:bg-primary/70"
94-
>
95-
Download Resume
96-
</Button>
97-
</a>
98-
<Button variant="outline" size="sm" className='rounded-full text-muted-foreground'
99-
onClick={() => setActiveTab("contact")}
100-
aria-label="Contact Me"
101-
>
102-
Contact Me
103-
</Button>
104-
</div>
10577
</div>
106-
<div className="flex items-center space-x-2 self-start flex-col sm:flex-row sm:self-center gap-1">
78+
<div className="flex items-center space-x-2">
10779
<ThemeToggle />
10880
<Link href="https://github.com/psparwez" aria-label="View GitHub Profile" target="_blank" rel="noopener noreferrer">
10981
<Button variant="outline" size="sm">
110-
11182
<Github className="w-4 h-4 sm:mr-1" />
11283
<span className="hidden sm:inline">View GitHub</span>
11384
</Button>

app/_components/navigation.tsx

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import React, { Fragment } from "react"
1+
"use client"
2+
3+
import React, { Fragment, useEffect, useState } from "react"
24
import { TABS } from "../(root)/page"
35
import { cn } from "@/lib/utils"
46
import { useIsMobile } from "@/hooks/use-mobile"
57
import { User, Briefcase, Folder, Mail } from "lucide-react"
8+
69
interface NavigationProps {
710
activeTab?: typeof TABS[number]
811
setActiveTab: (tab: typeof TABS[number]) => void
@@ -12,6 +15,14 @@ export default function Navigation({
1215
activeTab = "profile",
1316
setActiveTab,
1417
}: NavigationProps) {
18+
const [isMounted, setIsMounted] = useState(false)
19+
const isMobile = useIsMobile()
20+
21+
useEffect(() => {
22+
setIsMounted(true)
23+
}, [])
24+
25+
if (!isMounted) return null
1526

1627
const tabs = [
1728
{ id: "profile", label: "Profile", icon: <User size={16} /> },
@@ -20,45 +31,48 @@ export default function Navigation({
2031
{ id: "contact", label: "Contact", icon: <Mail size={16} /> },
2132
] as const
2233

23-
const isMobile = useIsMobile()
24-
2534
return (
26-
<nav className={`bg-card border-b border-border ${isMobile ? "fixed bottom-0 rounded-ss-2xl left-0 right-0 w-full border-t border-border shadow-lg" : "relative"}`}>
35+
<nav className={cn(
36+
"bg-card border-b border-border",
37+
isMobile
38+
? "fixed bottom-0 left-0 right-0 w-full border-t border-border shadow-lg"
39+
: "relative"
40+
)}>
2741
<div className="container mx-auto px-4 lg:px-20 xl:px-32">
28-
<div className={`sm:flex space-x-2 sm:space-x-8 overflow-x-auto ${isMobile ? "flex justify-between" : ""}`}>
42+
<div className={cn(
43+
"sm:flex space-x-2 sm:space-x-8 overflow-x-auto",
44+
isMobile && "flex justify-between"
45+
)}>
2946
{tabs.map((tab) => (
3047
<Fragment key={tab.id}>
31-
{
32-
isMobile ? (
33-
<button
34-
onClick={() => setActiveTab(tab.id)}
35-
className={cn(
36-
"py-4 px-2 text-[10px] font-medium border-b-2 transition-colors flex flex-col gap-1 items-center justify-between w-full m-0",
37-
activeTab === tab.id
38-
? "border-primary text-primary"
39-
: "border-transparent text-muted-foreground hover:text-foreground"
40-
)}
41-
aria-label={`Navigate to ${tab.label} section`}
42-
>
43-
<span>{tab.icon}</span>
44-
<span>{tab.label}</span>
45-
</button>
46-
)
47-
: (
48-
<button
49-
onClick={() => setActiveTab(tab.id)}
50-
className={cn(
51-
"py-4 px-2 text-sm font-medium border-b-2 transition-colors",
52-
activeTab === tab.id
53-
? "border-primary text-primary"
54-
: "border-transparent text-muted-foreground hover:text-foreground"
55-
)}
56-
aria-label={`Navigate to ${tab.label} section`}
57-
>
58-
{tab.label}
59-
</button>
60-
)
61-
}
48+
{isMobile ? (
49+
<button
50+
onClick={() => setActiveTab(tab.id)}
51+
className={cn(
52+
"py-4 px-2 text-[10px] font-medium border-b-2 transition-colors flex flex-col gap-1 items-center justify-between w-full m-0",
53+
activeTab === tab.id
54+
? "border-primary text-primary"
55+
: "border-transparent text-muted-foreground hover:text-foreground"
56+
)}
57+
aria-label={`Navigate to ${tab.label} section`}
58+
>
59+
<span>{tab.icon}</span>
60+
<span>{tab.label}</span>
61+
</button>
62+
) : (
63+
<button
64+
onClick={() => setActiveTab(tab.id)}
65+
className={cn(
66+
"py-4 px-2 text-sm font-medium border-b-2 transition-colors",
67+
activeTab === tab.id
68+
? "border-primary text-primary"
69+
: "border-transparent text-muted-foreground hover:text-foreground"
70+
)}
71+
aria-label={`Navigate to ${tab.label} section`}
72+
>
73+
{tab.label}
74+
</button>
75+
)}
6276
</Fragment>
6377
))}
6478
</div>

0 commit comments

Comments
 (0)