Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom'
import { SignIn, SignUp } from '@clerk/clerk-react'
import { ThemeProvider, useTheme } from './context/ThemeContext'
import { DashboardNavBridgeProvider } from './context/DashboardNavBridge'
import { ToastProvider } from './context/ToastContext'
import Navbar from './components/Navbar'
import ScrollToTop from './components/ScrollToTop'
Expand Down Expand Up @@ -140,11 +141,13 @@ function App() {
<ThemeProvider>
<ClerkThemeProvider>
<ToastProvider>
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ScrollToTop />
<SeoManager />
<AppRoutes />
</Router>
<DashboardNavBridgeProvider>
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ScrollToTop />
<SeoManager />
<AppRoutes />
</Router>
</DashboardNavBridgeProvider>
</ToastProvider>
</ClerkThemeProvider>
</ThemeProvider>
Expand Down
27 changes: 5 additions & 22 deletions frontend/src/components/DashboardLayout.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { Navigate, Outlet, useLocation } from 'react-router-dom'
import { Menu } from 'lucide-react'
import { getCurrentUser, getMyProfile } from '../utils/api'
import {
hasCompletedProfileSetup,
Expand All @@ -9,7 +8,7 @@ import {
resolveResumeRouteGuard,
} from '../utils/profileSetup'
import { ErrorState, LoadingState } from './AsyncState'
import { ICON_STROKE } from './IconTile'
import { useDashboardNavBridge } from '../context/DashboardNavBridge'
import DashboardSidebar from './DashboardSidebar'

const DashboardLayoutContext = createContext(null)
Expand All @@ -22,21 +21,6 @@ export function useDashboardLayout() {
return context
}

export function SidebarMenuButton({ className = '' }) {
const { openSidebar } = useDashboardLayout()

return (
<button
type="button"
onClick={openSidebar}
className={`md:hidden p-2.5 rounded-xl bg-tertiary text-secondary hover:text-primary hover:bg-surface-hover transition-colors flex-shrink-0 ${className}`}
aria-label="Open navigation menu"
>
<Menu size={20} strokeWidth={ICON_STROKE} />
</button>
)
}

function resolveDbUser(userData, profileData) {
if (userData) {
return userData
Expand All @@ -51,7 +35,7 @@ function resolveDbUser(userData, profileData) {

function DashboardLayout() {
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(false)
const { sidebarOpen, closeSidebar } = useDashboardNavBridge()
const [dbUser, setDbUser] = useState(null)
const [profile, setProfile] = useState(null)
const [basicDetails, setBasicDetails] = useState(null)
Expand Down Expand Up @@ -112,9 +96,9 @@ function DashboardLayout() {
}, [])

useEffect(() => {
setSidebarOpen(false)
closeSidebar()
setHasPendingResume(hasPendingResumeData())
}, [location.pathname])
}, [location.pathname, closeSidebar])

useEffect(() => {
refreshData()
Expand Down Expand Up @@ -154,7 +138,6 @@ function DashboardLayout() {
}, [routeGuardDecision.type, userLoadError, profileLoadError])

const contextValue = useMemo(() => ({
openSidebar: () => setSidebarOpen(true),
dbUser,
profile,
basicDetails,
Expand Down Expand Up @@ -201,7 +184,7 @@ function DashboardLayout() {
hasPendingResume={hasPendingResume}
isLoading={isLoading}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
onClose={closeSidebar}
/>
<main className="flex-1 min-w-0">
{mainContent}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/DashboardSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const dashboardNavItems = [

function NavLink({ item, isActive, onNavigate, disabled = false }) {
const Icon = item.icon
const baseClassName = `group flex items-start gap-3 px-3 py-2.5 rounded-xl transition-colors ${
const baseClassName = `group flex items-start gap-3 px-3 py-3 min-h-[44px] rounded-xl transition-colors ${
disabled
? 'text-muted/70 cursor-not-allowed'
: isActive
Expand Down Expand Up @@ -134,7 +134,7 @@ function SidebarContent({
target="_blank"
rel="noopener noreferrer"
onClick={onNavigate}
className="btn-primary flex items-center justify-center gap-2 w-full text-sm"
className="btn-primary flex items-center justify-center gap-2 w-full min-h-[44px] text-sm"
>
<ExternalLink size={16} strokeWidth={ICON_STROKE} />
View Live Portfolio
Expand Down Expand Up @@ -233,7 +233,7 @@ function DashboardSidebar({
ref={closeButtonRef}
type="button"
onClick={onClose}
className="p-2 rounded-lg text-secondary hover:text-primary hover:bg-surface-hover transition-colors"
className="inline-flex min-h-[44px] min-w-[44px] items-center justify-center rounded-lg text-secondary hover:text-primary hover:bg-surface-hover transition-colors"
aria-label="Close menu"
>
<X size={18} strokeWidth={ICON_STROKE} />
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/MobileTabBar.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
function MobileTabBar({ tabs, activeId, onChange, className = 'mb-6' }) {
return (
<div
className={`lg:hidden sticky top-navbar z-20 bg-surface/90 backdrop-blur-md -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-2 border-b border-border overflow-x-auto hide-scrollbar flex gap-2 ${className}`}
className={`md:hidden sticky top-navbar z-20 bg-surface/90 backdrop-blur-md -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-2 border-b border-border overflow-x-auto hide-scrollbar flex gap-2 ${className}`}
role="tablist"
aria-label="Section navigation"
>
Expand All @@ -12,7 +12,7 @@ function MobileTabBar({ tabs, activeId, onChange, className = 'mb-6' }) {
role="tab"
aria-selected={activeId === tab.id}
onClick={() => onChange(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full whitespace-nowrap text-sm font-medium transition-colors flex-shrink-0 ${
className={`flex items-center gap-2 px-4 py-2.5 min-h-[44px] rounded-full whitespace-nowrap text-sm font-medium transition-colors flex-shrink-0 ${
activeId === tab.id
? 'bg-primary-500/10 text-primary ring-1 ring-primary-500/25'
: 'text-secondary hover:text-primary hover:bg-surface-hover'
Expand Down
141 changes: 80 additions & 61 deletions frontend/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ICON_STROKE } from './IconTile'
import { dashboardNavItems } from './DashboardSidebar'
import ThemeToggle from './ThemeToggle'
import { useTheme } from '../context/ThemeContext'
import { useDashboardNavBridge } from '../context/DashboardNavBridge'
import { getUserButtonAppearance } from '../utils/clerkAppearance'
import BrandLogo from './BrandLogo'
import { BRAND_NAME_DISPLAY } from '../constants/brand'
Expand Down Expand Up @@ -58,20 +59,39 @@ function Navbar() {
const { theme } = useTheme()
const location = useLocation()
const menuButtonRef = useRef(null)
const dashboardNav = useDashboardNavBridge()
const openDashboardSidebar = dashboardNav?.openSidebar
const closeDashboardSidebar = dashboardNav?.closeSidebar
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)

const onDashboard = isDashboardPath(location.pathname)
const userButtonAppearance = getUserButtonAppearance(theme)
const showMobileMenu = !onDashboard
const usesDashboardDrawer = onDashboard && Boolean(dashboardNav)
const drawerOpen = usesDashboardDrawer && dashboardNav.sidebarOpen
const showMobileMenuButton = !onDashboard
const menuOpen = usesDashboardDrawer ? drawerOpen : mobileMenuOpen

const closeMobileMenu = () => setMobileMenuOpen(false)
const toggleMobileMenu = () => setMobileMenuOpen((open) => !open)

const toggleMobileMenu = () => {
if (usesDashboardDrawer) {
if (dashboardNav.sidebarOpen) {
closeDashboardSidebar?.()
} else {
openDashboardSidebar?.()
}
return
}

setMobileMenuOpen((open) => !open)
}

useLayoutEffect(() => {
closeMobileMenu()
closeDashboardSidebar?.()
setScrolled(window.scrollY > 8)
}, [location.pathname])
}, [location.pathname, closeDashboardSidebar])

useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 8)
Expand All @@ -80,7 +100,7 @@ function Navbar() {
}, [])

useEffect(() => {
if (!mobileMenuOpen) return
if (!mobileMenuOpen || usesDashboardDrawer) return

const previousOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
Expand All @@ -97,7 +117,7 @@ function Navbar() {
document.body.style.overflow = previousOverflow
document.removeEventListener('keydown', handleKeyDown)
}
}, [mobileMenuOpen])
}, [mobileMenuOpen, usesDashboardDrawer])

return (
<header className="fixed top-0 left-0 right-0 z-50 navbar-safe-top">
Expand All @@ -107,18 +127,22 @@ function Navbar() {
scrolled ? 'navbar-scrolled' : ''
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8">
<div className="flex justify-between items-center gap-2 h-16 min-w-0">
<Link
to="/"
className="flex items-center gap-3 group min-w-0 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50"
className="flex items-center gap-2 sm:gap-3 group min-w-0 flex-shrink rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50"
aria-label={`${BRAND_NAME_DISPLAY} home`}
>
<BrandLogo className="transition-transform group-hover:scale-[1.02]" />
<BrandLogo
size="sm"
className="transition-transform group-hover:scale-[1.02] min-w-0"
nameClassName="max-[420px]:hidden"
/>
</Link>

{/* Desktop */}
<div className="hidden md:flex items-center gap-2">
<div className="hidden md:flex items-center gap-2 flex-shrink-0">
<ThemeToggle />

<SignedOut>
Expand All @@ -144,40 +168,66 @@ function Navbar() {
</SignedIn>
</div>

{/* Mobile — fixed-width action slots prevent layout shift on route change */}
<div className="flex md:hidden items-center gap-2">
<ThemeToggle />
{/* Mobile */}
<div className="flex md:hidden items-center gap-1.5 sm:gap-2 flex-shrink-0">
<ThemeToggle className="min-h-[44px] min-w-[44px] flex items-center justify-center" />

<SignedIn>
<UserButton
afterSignOutUrl="/"
appearance={userButtonAppearance}
/>
{!onDashboard && (
<Link
to="/dashboard"
className="btn-primary inline-flex min-h-[44px] items-center justify-center gap-1.5 px-3 sm:px-4 text-sm whitespace-nowrap"
aria-label="Go to dashboard"
>
<LayoutDashboard size={16} strokeWidth={ICON_STROKE} className="flex-shrink-0" />
<span className="max-[360px]:hidden">Dashboard</span>
</Link>
)}

<div className="flex items-center justify-center min-h-[44px] min-w-[44px]">
<UserButton
afterSignOutUrl="/"
appearance={userButtonAppearance}
/>
</div>
</SignedIn>

{showMobileMenu ? (
{showMobileMenuButton ? (
<SignedOut>
<button
ref={menuButtonRef}
type="button"
onClick={toggleMobileMenu}
className={`${MOBILE_ACTION_SLOT} rounded-xl text-secondary hover:text-primary hover:bg-surface-hover transition-colors`}
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={menuOpen}
aria-controls={MOBILE_MENU_ID}
>
{menuOpen
? <X size={22} strokeWidth={ICON_STROKE} />
: <Menu size={22} strokeWidth={ICON_STROKE} />}
</button>
</SignedOut>
) : (
<button
ref={menuButtonRef}
type="button"
onClick={toggleMobileMenu}
className={`${MOBILE_ACTION_SLOT} rounded-xl text-secondary hover:text-primary hover:bg-surface-hover transition-colors`}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
aria-controls={MOBILE_MENU_ID}
aria-label={menuOpen ? 'Close navigation' : 'Open navigation'}
aria-expanded={menuOpen}
>
{mobileMenuOpen
{menuOpen
? <X size={22} strokeWidth={ICON_STROKE} />
: <Menu size={22} strokeWidth={ICON_STROKE} />}
</button>
) : (
<span className={MOBILE_ACTION_SLOT} aria-hidden="true" />
)}
</div>
</div>
</div>

<AnimatePresence initial={false}>
{showMobileMenu && mobileMenuOpen && (
{showMobileMenuButton && mobileMenuOpen && (
<motion.div
id={MOBILE_MENU_ID}
role="navigation"
Expand All @@ -188,7 +238,7 @@ function Navbar() {
transition={{ duration: 0.2 }}
className="md:hidden border-t border-border overflow-hidden bg-surface"
>
<div className="px-4 py-4 space-y-1 max-h-below-navbar overflow-y-auto">
<div className="px-3 sm:px-4 py-4 space-y-2 max-h-below-navbar overflow-y-auto overscroll-contain">
<SignedOut>
<MobileNavLink
to="/sign-in"
Expand All @@ -198,46 +248,15 @@ function Navbar() {
>
Sign In
</MobileNavLink>
<MobileNavLink
<Link
to="/sign-up"
onClick={closeMobileMenu}
icon={UserPlus}
isActive={location.pathname.startsWith('/sign-up')}
className="btn-primary flex items-center justify-center gap-2 w-full min-h-[44px] text-sm"
>
<UserPlus size={18} strokeWidth={ICON_STROKE} />
Get Started
</MobileNavLink>
</Link>
</SignedOut>

<SignedIn>
<p className="px-4 pt-1 pb-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted">
Menu
</p>

<MobileNavLink
to="/dashboard"
onClick={closeMobileMenu}
icon={LayoutDashboard}
isActive={location.pathname === '/dashboard'}
>
Dashboard
</MobileNavLink>

<div className="space-y-0.5">
{dashboardNavItems
.filter((item) => item.to !== '/dashboard')
.map((item) => (
<MobileNavLink
key={item.to}
to={item.to}
onClick={closeMobileMenu}
icon={item.icon}
isActive={location.pathname === item.to}
>
{item.label}
</MobileNavLink>
))}
</div>
</SignedIn>
</div>
</motion.div>
)}
Expand Down
Loading
Loading