Mobile UI optimizations
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

This commit is contained in:
Josh 2025-09-22 15:27:42 +00:00
parent 35142f52bc
commit 0a57a97016
8 changed files with 308 additions and 64 deletions

View File

@ -1 +1 @@
7c4503634c1566a112e17705d07e15f792647175-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
f60aaeaf5d529bc05bbbd41815d65d86e9010dfc-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b

View File

@ -72,6 +72,10 @@ function App() {
const [retireProps, setRetireProps] = useState(null);
const [supportOpen, setSupportOpen] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
// Mobile nav
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const [mobileSection, setMobileSection] = useState(null); // 'planning' | 'preparing' | 'enhancing' | 'retirement' | 'profile' | null
const toggleMobileSection = (key) => setMobileSection((k) => (k === key ? null : key));
const AUTH_HOME = '/signin-landing';
@ -79,6 +83,9 @@ function App() {
const prevPathRef = React.useRef(location.pathname);
useEffect(() => { prevPathRef.current = location.pathname; }, [location.pathname]);
// Close mobile nav on route change
useEffect(() => { setMobileNavOpen(false); }, [location.pathname]);
const IN_OB = (p) => p.startsWith('/premium-onboarding');
/* ------------------------------------------
@ -323,7 +330,7 @@ const cancelLogout = () => {
// ====================================
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="flex min-h-[100dvh] items-center justify-center">
<p>Loading...</p>
</div>
);
@ -355,17 +362,29 @@ const cancelLogout = () => {
setDrawerOpen(true);
}
}}>
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
<div className="flex min-h-[100dvh] flex-col bg-gray-50 text-gray-800">
{/* Header */}
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
<h1 className="text-lg font-semibold">
<header className="sticky top-0 z-50 flex items-center justify-between border-b bg-white/95 px-4 py-3 md:px-6 md:py-4 backdrop-blur supports-[backdrop-filter]:bg-white/70">
<h1 className="text-base md:text-lg font-semibold truncate pr-3">
AptivaAI - Career Guidance Platform
</h1>
{/* Mobile hamburger */}
<button
type="button"
aria-label="Open menu"
className="md:hidden inline-flex items-center justify-center rounded-md p-2 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-600"
onClick={() => setMobileNavOpen(v => !v)}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-6 w-6">
<path d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h16.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</button>
{showAuthedNav && (
<>
{/* NAV MENU */}
<nav className="flex space-x-6">
{/* Desktop NAV */}
<nav className="hidden md:flex md:space-x-6">
{/* 1) Planning */}
<div className="relative group">
<Button
@ -557,8 +576,8 @@ const cancelLogout = () => {
</div>
</nav>
{/* LOGOUT + UPGRADE BUTTONS */}
<div className="flex items-center space-x-4 ml-4 relative z-10">
{/* LOGOUT + UPGRADE BUTTONS (desktop) */}
<div className="hidden md:flex items-center space-x-4 ml-4 relative z-10">
{showPremiumCTA && !canAccessPremium && (
<Button
className={cn(
@ -586,11 +605,6 @@ const cancelLogout = () => {
Support
</button>
<SupportModal
open={supportOpen}
onClose={() => setSupportOpen(false)}
/>
{/* LOGOUT BUTTON */}
<button
onClick={handleLogoutClick}
@ -617,8 +631,159 @@ const cancelLogout = () => {
)}
</header>
{/* Mobile slide-down menu */}
{showAuthedNav && (
<div
className={cn(
"md:hidden border-b bg-white shadow-sm",
mobileNavOpen ? "block" : "hidden"
)}
>
<div className="px-4 py-3 space-y-2">
{/* Upgrade CTA (mobile) */}
{showPremiumCTA && !canAccessPremium && (
<Button
className={cn('bg-green-500 hover:bg-green-600 text-white w-full h-12')}
onClick={() => navigate('/paywall')}
>
Upgrade to Premium
</Button>
)}
{/* Primary sections (touch-friendly, no shadcn Button to avoid style overrides) */}
<button
type="button"
onClick={() => toggleMobileSection('planning')}
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
>
<span className="text-base">Find Your Career</span>
<span className={cn("transition-transform", mobileSection==='planning' ? "rotate-180" : "")}></span>
</button>
{mobileSection === 'planning' && (
<div className="pl-3 space-y-1">
<Link
to="/planning"
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
>
Find Your Career Overview
</Link>
<Link to="/career-explorer" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Career Explorer</Link>
<Link to="/interest-inventory" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Interest Inventory</Link>
</div>
)}
<button
type="button"
onClick={() => toggleMobileSection('preparing')}
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
>
<span className="text-base">Preparing &amp; UpSkilling for Your Career</span>
<span className={cn("transition-transform", mobileSection==='preparing' ? "rotate-180" : "")}></span>
</button>
{mobileSection === 'preparing' && (
<div className="pl-3 space-y-1">
<Link
to="/preparing"
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
>
Preparing Overview
</Link>
<Link to="/educational-programs" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Educational Programs</Link>
</div>
)}
<button
type="button"
onClick={() => toggleMobileSection('enhancing')}
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
>
<span className="text-base">
Enhancing Your Career {!canAccessPremium && <span className="ml-1 text-xs text-gray-600">(Premium)</span>}
</span>
<span className={cn("transition-transform", mobileSection==='enhancing' ? "rotate-180" : "")}></span>
</button>
{mobileSection === 'enhancing' && (
<div className="pl-3 space-y-1">
<Link
to="/enhancing"
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
>
Enhancing Overview {!canAccessPremium && <span className="ml-1 text-xs text-gray-500">(Premium)</span>}
</Link>
<Link to="/career-roadmap" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Roadmap &amp; AI Career Coach</Link>
<Link to="/resume-optimizer" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Optimize Resume</Link>
</div>
)}
<button
type="button"
onClick={() => toggleMobileSection('retirement')}
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
>
<span className="text-base">
Retirement Planning (beta) {!canAccessPremium && <span className="ml-1 text-xs text-gray-600">(Premium)</span>}
</span>
<span className={cn("transition-transform", mobileSection==='retirement' ? "rotate-180" : "")}></span>
</button>
{mobileSection === 'retirement' && (
<div className="pl-3 space-y-1">
<Link
to="/retirement"
className="block px-2 py-2 text-sm font-medium text-blue-700 rounded hover:bg-blue-50"
>
Retirement Overview {!canAccessPremium && <span className="ml-1 text-xs text-gray-500">(Premium)</span>}
</Link>
</div>
)}
<button
type="button"
onClick={() => toggleMobileSection('profile')}
className="w-full h-12 px-3 inline-flex items-center justify-between rounded border text-gray-800 bg-white active:bg-gray-50"
>
<span className="text-base">Profile</span>
<span className={cn("transition-transform", mobileSection==='profile' ? "rotate-180" : "")}></span>
</button>
{mobileSection === 'profile' && (
<div className="pl-3 space-y-1">
<Link to="/profile" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Account</Link>
<Link to="/financial-profile" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Financial Profile</Link>
{canAccessPremium ? (
<Link to="/profile/careers" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">Career Profiles</Link>
) : (
<span className="block px-2 py-2 text-sm text-gray-400">Career Profiles (Premium)</span>
)}
{canAccessPremium ? (
<Link to="/profile/college" className="block px-2 py-2 text-sm text-gray-700 rounded hover:bg-gray-100">College Profiles</Link>
) : (
<span className="block px-2 py-2 text-sm text-gray-400">College Profiles (Premium)</span>
)}
</div>
)}
{/* Support + Logout */}
<div className="flex items-center gap-3 pt-2">
<button
type="button"
onClick={() => setSupportOpen(true)}
className="px-3 py-1 rounded hover:bg-gray-100"
>
Support
</button>
<button
onClick={handleLogoutClick}
disabled={loggingOut}
className="flex-1 px-3 py-3 rounded border border-red-300 text-red-600 hover:bg-red-50 disabled:opacity-60"
>
{loggingOut ? 'Signing out…' : 'Logout'}
</button>
</div>
</div>
</div>
)}
{/* MAIN CONTENT */}
<main className="flex-1 p-6">
<main className="flex-1 p-4 md:p-6">
<Routes>
{/* Default */}
<Route
@ -696,6 +861,12 @@ const cancelLogout = () => {
</Routes>
</main>
{/* Support modal mounted once at root so it centers correctly on desktop & mobile */}
<SupportModal
open={supportOpen}
onClose={() => setSupportOpen(false)}
/>
{isAuthenticated && (
<ChatDrawer
open={drawerOpen}

View File

@ -120,6 +120,17 @@ function CareerExplorer() {
Good: 'Good - Less Strong Match',
};
// VISUAL-ONLY FILTER: Never mutates source, never triggers network.
const filteredSuggestions = useMemo(() => {
const src = Array.isArray(careerSuggestions) ? careerSuggestions : [];
if (!selectedJobZone && !selectedFit) return src;
return src.filter((c) => {
const zoneMatch = selectedJobZone ? String(c?.job_zone ?? '') === String(selectedJobZone) : true;
const fitMatch = selectedFit ? String(c?.fit ?? '') === String(selectedFit) : true;
return zoneMatch && fitMatch;
});
}, [careerSuggestions, selectedJobZone, selectedFit]);
// ---------- Load cache on mount (no profile call) ----------
useEffect(() => {
const cached = localStorage.getItem('careerSuggestionsCache');
@ -547,7 +558,7 @@ function CareerExplorer() {
// ---- Render
return (
<div className="career-explorer-container bg-white p-6 rounded shadow">
<div className="career-explorer-container bg-white p-4 md:p-6 rounded shadow">
{renderLoadingOverlay()}
{showModal && (
@ -588,7 +599,9 @@ function CareerExplorer() {
</div>
{careerList.length ? (
<table className="w-full mb-4">
/* Mobile: contain + allow horizontal scroll so the table never bleeds past card */
<div className="mb-4 -mx-4 md:mx-0 overflow-x-auto">
<table className="w-full min-w-[720px]">
<thead>
<tr>
<th className="border p-2">Career</th>
@ -649,11 +662,16 @@ function CareerExplorer() {
<td className="border p-2">{balanceRating}</td>
<td className="border p-2">{recognitionRating}</td>
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
<td className="border p-2 space-x-2">
<Button className="bg-red-600 text-black-500" onClick={() => removeCareerFromList(career.code)}>
<td className="border p-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-2">
<Button
className="bg-red-600 text-black-500 h-10 sm:h-9 px-3 text-sm w-full sm:w-auto"
onClick={() => removeCareerFromList(career.code)}
>
Remove
</Button>
<Button className="bg-green-600 text-white px-2 py-1 text-xs sm:text-sm whitespace-nowrap"
<Button
className="bg-green-600 text-white h-10 sm:h-9 px-3 text-sm whitespace-nowrap w-full sm:w-auto"
onClick={() => {
// native browser warning before leaving Career Explorer
const ok = window.confirm(
@ -692,44 +710,61 @@ function CareerExplorer() {
state: { socCode: baseSoc, cipCodes: cleanedCips, careerTitle: career.title }
});
})();
}}>
}}
>
Plan your Education/Skills
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
) : (<p>No careers added to comparison.</p>)}
</div>
) : (<p className="mb-4">No careers added to comparison.</p>)}
<div className="flex gap-4 mb-4">
<select className="border px-3 py-1 rounded" value={selectedJobZone} onChange={(e) => setSelectedJobZone(e.target.value)}>
{/* Filters: stack on mobile; compact controls */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-2">
<select
className="border rounded w-full sm:w-auto h-10 px-3 text-sm"
value={selectedJobZone}
onChange={(e) => setSelectedJobZone(e.target.value)}
>
<option value="">All Preparation Levels</option>
{Object.entries(jobZoneLabels).map(([zone, label]) => (
<option key={zone} value={zone}>{label}</option>
))}
</select>
<select className="border px-3 py-1 rounded" value={selectedFit} onChange={(e) => setSelectedFit(e.target.value)}>
<select
className="border rounded w-full sm:w-auto h-10 px-3 text-sm"
value={selectedFit}
onChange={(e) => setSelectedFit(e.target.value)}
>
<option value="">All Fit Levels</option>
{Object.entries(fitLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
<Button onClick={handleReloadSuggestions} className="bg-green-600 text-white px-3 py-1 text-xs sm:text-sm">
<Button
onClick={handleReloadSuggestions}
className="bg-green-600 text-white h-10 px-3 text-sm w-full sm:w-auto"
>
Reload Career Suggestions
</Button>
<div className="flex items-center gap-1 ml-4">
</div>
{/* Warning note: separate row on mobile to avoid squish */}
<div className="mb-4 text-xs sm:text-sm text-gray-700 flex items-center gap-1">
<span className="warning-icon"></span>
<span>= May have limited data for this career path</span>
</div>
</div>
<CareerSuggestions
careerSuggestions={careerSuggestions}
careerSuggestions={filteredSuggestions}
onCareerClick={(career) => { setSelectedCareer(career); handleCareerClick(career); }}
/>

View File

@ -182,7 +182,7 @@ export default function ChatDrawer({
{/* side drawer */}
<SheetContent
side="right"
className="flex h-full w-[370px] flex-col p-0 md:w-[420px]"
className="flex h-full w-[88vw] max-w-[380px] flex-col p-0 sm:w-[360px] md:w-[420px]"
>
{/* header (tabs only if retirement bot is allowed) */}
<div className="flex border-b">

View File

@ -458,14 +458,15 @@ const topSchools = filteredAndSortedSchools.slice(0, TOP_N).map(s => ({
return (
<tr key={idx} className="border-b text-sm">
<td className="p-2 font-medium text-gray-800">
{elementName}{' '}
<span className="inline-flex items-center gap-1 align-middle">
<span className="break-words">{elementName}</span>
<span
title={definition}
className="top-0 left-0 -translate-y-3/4 translate-x-1/8 ml-.5 inline-flex h-2.5 w-2.5 items-center justify-center
rounded-full bg-blue-500 text-[0.6rem] font-bold text-white cursor-help"
className="shrink-0 inline-flex h-4 w-4 items-center justify-center rounded-full bg-blue-500 text-[10px] font-bold text-white cursor-help leading-none"
>
i
</span>
</span>
</td>
<td className="p-2 text-center text-gray-800">{impStars}</td>
<td className="p-2 text-center text-gray-800">{lvlBars}</td>
@ -564,18 +565,16 @@ const topSchools = filteredAndSortedSchools.slice(0, TOP_N).map(s => ({
<th className="p-2">Ability</th>
<th className="p-2 text-center">Importance</th>
<th className="p-2 text-center">Level</th>
<th className="p-2 text-center">
{/* Info icon for "Why no courses?" */}
<span className="relative inline-block">
<span>Why no courses?</span>
<th className="p-2">
<div className="w-full flex items-center justify-center gap-1">
<span className="whitespace-nowrap">Why no courses?</span>
<span
className="top-0 left-0 -translate-y-3/4 translate-x-1/8 ml-.5 inline-flex h-2.5 w-2.5 items-center justify-center
rounded-full bg-blue-500 text-[10px] font-italics text-white cursor-help"
className="shrink-0 inline-flex h-4 w-4 items-center justify-center rounded-full bg-blue-500 text-[10px] italic text-white cursor-help leading-none"
title="Abilities are more innate in nature, and difficult to offer courses for them."
>
i
</span>
</span>
</div>
</th>
</tr>
</thead>

View File

@ -23,7 +23,7 @@ const InterestInventory = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [userProfile, setUserProfile] = useState(null);
const isProd = (process.env.REACT_APP_ENV_NAME || '').toLowerCase() === 'prod';
const isProd = (process.env.ENV_NAME || '').toLowerCase() === 'prod';
const navigate = useNavigate();

View File

@ -14,6 +14,7 @@ function SignInLanding({ user }) {
We blend data-backed insights with human-centered design, enhanced -not driven by- AI. Giving you practical recommendations and real-world context so you stay in the drivers seat of your career. Whether youre planning your first step, advancing your current role, or ready to pivot entirely, our platform keeps you in controlhelping you adapt, grow, and thrive on your own terms.
</p>
<ul className="list-disc ml-6 mb-4">
<li><strong>Planning:</strong> Just starting out? Looking for a different career that is a better fit? Explore options and figure out what careers match your interests and skills.</li>
<li><strong>Preparing:</strong> Know what you want but just not how to get there? Gain education, skills, or certifications required to start or transition.</li>
<li><strong>Enhancing:</strong> You've got some experience in your field but want to know how to get to the next level? Advance, seek promotions, or shift roles for an established professional.</li>
@ -22,17 +23,18 @@ We blend data-backed insights with human-centered design, enhanced -not driven b
<p className="mb-4">
Where would you like to go next?
</p>
<div className="space-x-2">
<Link to="/planning" className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
{/* Mobile: stacked full-width; Desktop: original inline buttons with spacing */}
<div className="flex flex-col gap-2 md:block md:space-x-2">
<Link to="/planning" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-blue-600 text-white rounded hover:bg-blue-700">
Go to Exploring
</Link>
<Link to="/preparing" className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
<Link to="/preparing" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-blue-600 text-white rounded hover:bg-blue-700">
Go to Preparing
</Link>
<Link to="/enhancing" className="inline-block px-4 py-2 bg-green-600 text-white rounded hover:bg-blue-700">
<Link to="/enhancing" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-green-600 text-white rounded hover:bg-green-700">
Go to Enhancing
</Link>
<Link to="/retirement" className="inline-block px-4 py-2 bg-green-600 text-white rounded hover:bg-blue-700">
<Link to="/retirement" className="w-full md:w-auto h-12 md:h-auto inline-flex items-center justify-center md:inline-block px-4 bg-green-600 text-white rounded hover:bg-green-700">
Go to Retirement
</Link>
</div>

37
src/utils/net.js Normal file
View File

@ -0,0 +1,37 @@
// src/utils/net.js
export function computeNetState() {
if (typeof navigator === 'undefined') return { slow: false, effectiveType: '', downlink: 0 };
const nc = navigator.connection || navigator.webkitConnection || navigator.mozConnection;
const effectiveType = nc?.effectiveType || '';
const downlink = Number(nc?.downlink || 0);
const slow = ['slow-2g', '2g', '3g'].includes(effectiveType) || (downlink > 0 && downlink < 3);
return { slow, effectiveType, downlink };
}
export function getNetState() {
// Fallback keeps things safe in SSR or older browsers
return (typeof window !== 'undefined' && window.__APT_NET_STATE) || { slow: false, effectiveType: '', downlink: 0 };
}
export function setNetState(s) {
if (typeof window !== 'undefined') window.__APT_NET_STATE = s;
}
export function initNetObserver() {
const update = () => setNetState(computeNetState());
update(); // seed immediately
const nc = (typeof navigator !== 'undefined') && (navigator.connection || navigator.webkitConnection || navigator.mozConnection);
if (nc && nc.addEventListener) {
nc.addEventListener('change', update);
return () => nc.removeEventListener('change', update);
}
// Some browsers use onChange
if (nc && 'onchange' in nc) {
const handler = () => update();
nc.onchange = handler;
return () => { if (nc.onchange === handler) nc.onchange = null; };
}
// No-op cleanup
return () => {};
}