diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index effdadd..f9c1b73 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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' @@ -140,11 +141,13 @@ function App() { - - - - - + + + + + + + diff --git a/frontend/src/components/DashboardLayout.jsx b/frontend/src/components/DashboardLayout.jsx index 426e077..ca7eaf4 100644 --- a/frontend/src/components/DashboardLayout.jsx +++ b/frontend/src/components/DashboardLayout.jsx @@ -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, @@ -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) @@ -22,21 +21,6 @@ export function useDashboardLayout() { return context } -export function SidebarMenuButton({ className = '' }) { - const { openSidebar } = useDashboardLayout() - - return ( - - ) -} - function resolveDbUser(userData, profileData) { if (userData) { return userData @@ -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) @@ -112,9 +96,9 @@ function DashboardLayout() { }, []) useEffect(() => { - setSidebarOpen(false) + closeSidebar() setHasPendingResume(hasPendingResumeData()) - }, [location.pathname]) + }, [location.pathname, closeSidebar]) useEffect(() => { refreshData() @@ -154,7 +138,6 @@ function DashboardLayout() { }, [routeGuardDecision.type, userLoadError, profileLoadError]) const contextValue = useMemo(() => ({ - openSidebar: () => setSidebarOpen(true), dbUser, profile, basicDetails, @@ -201,7 +184,7 @@ function DashboardLayout() { hasPendingResume={hasPendingResume} isLoading={isLoading} open={sidebarOpen} - onClose={() => setSidebarOpen(false)} + onClose={closeSidebar} />
{mainContent} diff --git a/frontend/src/components/DashboardSidebar.jsx b/frontend/src/components/DashboardSidebar.jsx index 88db0ef..98c853f 100644 --- a/frontend/src/components/DashboardSidebar.jsx +++ b/frontend/src/components/DashboardSidebar.jsx @@ -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 @@ -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" > View Live Portfolio @@ -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" > diff --git a/frontend/src/components/MobileTabBar.jsx b/frontend/src/components/MobileTabBar.jsx index c17ae62..ec2141a 100644 --- a/frontend/src/components/MobileTabBar.jsx +++ b/frontend/src/components/MobileTabBar.jsx @@ -1,7 +1,7 @@ function MobileTabBar({ tabs, activeId, onChange, className = 'mb-6' }) { return (
@@ -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' diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 5eeadd7..2777147 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -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' @@ -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) @@ -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' @@ -97,7 +117,7 @@ function Navbar() { document.body.style.overflow = previousOverflow document.removeEventListener('keydown', handleKeyDown) } - }, [mobileMenuOpen]) + }, [mobileMenuOpen, usesDashboardDrawer]) return (
@@ -107,18 +127,22 @@ function Navbar() { scrolled ? 'navbar-scrolled' : '' }`} > -
-
+
+
- + {/* Desktop */} -
+
@@ -144,40 +168,66 @@ function Navbar() {
- {/* Mobile — fixed-width action slots prevent layout shift on route change */} -
- + {/* Mobile */} +
+ - + {!onDashboard && ( + + + Dashboard + + )} + +
+ +
- {showMobileMenu ? ( + {showMobileMenuButton ? ( + + + + ) : ( - ) : ( -
- {showMobileMenu && mobileMenuOpen && ( + {showMobileMenuButton && mobileMenuOpen && ( -
+
Sign In - + Get Started - + - - -

- Menu -

- - - Dashboard - - -
- {dashboardNavItems - .filter((item) => item.to !== '/dashboard') - .map((item) => ( - - {item.label} - - ))} -
-
)} diff --git a/frontend/src/components/PageHeader.jsx b/frontend/src/components/PageHeader.jsx index 0d59400..18c1f5f 100644 --- a/frontend/src/components/PageHeader.jsx +++ b/frontend/src/components/PageHeader.jsx @@ -1,10 +1,7 @@ -import { SidebarMenuButton } from './DashboardLayout' - function PageHeader({ title, description, className = '', children }) { return ( -
- -
+
+

{title}

diff --git a/frontend/src/context/DashboardNavBridge.jsx b/frontend/src/context/DashboardNavBridge.jsx new file mode 100644 index 0000000..e81f452 --- /dev/null +++ b/frontend/src/context/DashboardNavBridge.jsx @@ -0,0 +1,27 @@ +import { createContext, useCallback, useContext, useMemo, useState } from 'react' + +const DashboardNavBridgeContext = createContext(null) + +export function DashboardNavBridgeProvider({ children }) { + const [sidebarOpen, setSidebarOpen] = useState(false) + + const openSidebar = useCallback(() => setSidebarOpen(true), []) + const closeSidebar = useCallback(() => setSidebarOpen(false), []) + + const value = useMemo(() => ({ + sidebarOpen, + openSidebar, + closeSidebar, + setSidebarOpen, + }), [sidebarOpen, openSidebar, closeSidebar]) + + return ( + + {children} + + ) +} + +export function useDashboardNavBridge() { + return useContext(DashboardNavBridgeContext) +} \ No newline at end of file diff --git a/frontend/src/pages/Landing.jsx b/frontend/src/pages/Landing.jsx index 5a753c5..49e4c93 100644 --- a/frontend/src/pages/Landing.jsx +++ b/frontend/src/pages/Landing.jsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { SignedIn, SignedOut } from '@clerk/clerk-react' import { motion, useScroll, useTransform } from 'framer-motion' @@ -25,10 +26,22 @@ const fadeUp = { } function Landing() { + const [compactHero, setCompactHero] = useState(() => { + if (typeof window === 'undefined') return true + return window.matchMedia('(max-width: 639px)').matches + }) const { scrollY } = useScroll() const heroY = useTransform(scrollY, [0, 400], [0, 60]) const heroOpacity = useTransform(scrollY, [0, 280], [1, 0.4]) + useEffect(() => { + const mediaQuery = window.matchMedia('(max-width: 639px)') + const updateCompactHero = () => setCompactHero(mediaQuery.matches) + updateCompactHero() + mediaQuery.addEventListener('change', updateCompactHero) + return () => mediaQuery.removeEventListener('change', updateCompactHero) + }, []) + const features = [ { icon: FileUp, @@ -78,8 +91,8 @@ function Landing() { return (
- {/* Hero Section */} -
+ {/* Hero Section — top-aligned on mobile so CTAs stay above the fold */} +
@@ -88,24 +101,24 @@ function Landing() {
- AI-Powered Portfolio Builder + AI-Powered Portfolio Builder Your Resume.
@@ -118,7 +131,7 @@ function Landing() { initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.25, duration: 0.4 }} - className="text-base sm:text-lg md:text-xl text-secondary max-w-2xl mx-auto mb-6 sm:mb-10 leading-relaxed px-2" + className="text-sm sm:text-lg md:text-xl text-secondary max-w-2xl mx-auto mb-5 sm:mb-10 leading-relaxed px-1 sm:px-2" > Upload your resume and let AI transform it into a stunning, professional portfolio that showcases your skills and experience. @@ -128,21 +141,30 @@ function Landing() { initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.35, duration: 0.4 }} - className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 mb-10 sm:mb-16 w-full max-w-md sm:max-w-none mx-auto px-4 sm:px-0" + className="flex flex-col sm:flex-row items-stretch sm:items-center justify-center gap-3 sm:gap-4 mb-6 sm:mb-16 w-full max-w-sm sm:max-w-none mx-auto" > - + Start Building Free - + - + Sign In - + Go to Dashboard - + @@ -151,7 +173,7 @@ function Landing() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.5, duration: 0.4 }} - className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3 sm:gap-4 max-w-4xl mx-auto px-2" + className="hidden sm:grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3 sm:gap-4 max-w-4xl mx-auto px-2" > {benefits.map((benefit, i) => { const Icon = benefit.icon @@ -292,15 +314,21 @@ function Landing() { Start for free, no credit card required.

- + Get Started Free - + - + Go to Dashboard - +
@@ -308,7 +336,7 @@ function Landing() {
{/* Footer */} -