Mobile UI optimizations
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
This commit is contained in:
parent
35142f52bc
commit
0a57a97016
@ -1 +1 @@
|
|||||||
7c4503634c1566a112e17705d07e15f792647175-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
f60aaeaf5d529bc05bbbd41815d65d86e9010dfc-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||||
|
199
src/App.js
199
src/App.js
@ -72,6 +72,10 @@ function App() {
|
|||||||
const [retireProps, setRetireProps] = useState(null);
|
const [retireProps, setRetireProps] = useState(null);
|
||||||
const [supportOpen, setSupportOpen] = useState(false);
|
const [supportOpen, setSupportOpen] = useState(false);
|
||||||
const [loggingOut, setLoggingOut] = 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';
|
const AUTH_HOME = '/signin-landing';
|
||||||
@ -79,6 +83,9 @@ function App() {
|
|||||||
const prevPathRef = React.useRef(location.pathname);
|
const prevPathRef = React.useRef(location.pathname);
|
||||||
useEffect(() => { prevPathRef.current = location.pathname; }, [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');
|
const IN_OB = (p) => p.startsWith('/premium-onboarding');
|
||||||
|
|
||||||
/* ------------------------------------------
|
/* ------------------------------------------
|
||||||
@ -323,7 +330,7 @@ const cancelLogout = () => {
|
|||||||
// ====================================
|
// ====================================
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center">
|
<div className="flex min-h-[100dvh] items-center justify-center">
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -355,17 +362,29 @@ const cancelLogout = () => {
|
|||||||
setDrawerOpen(true);
|
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 */}
|
||||||
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
<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-lg font-semibold">
|
<h1 className="text-base md:text-lg font-semibold truncate pr-3">
|
||||||
AptivaAI - Career Guidance Platform
|
AptivaAI - Career Guidance Platform
|
||||||
</h1>
|
</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 && (
|
{showAuthedNav && (
|
||||||
<>
|
<>
|
||||||
{/* NAV MENU */}
|
{/* Desktop NAV */}
|
||||||
<nav className="flex space-x-6">
|
<nav className="hidden md:flex md:space-x-6">
|
||||||
{/* 1) Planning */}
|
{/* 1) Planning */}
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<Button
|
<Button
|
||||||
@ -557,8 +576,8 @@ const cancelLogout = () => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* LOGOUT + UPGRADE BUTTONS */}
|
{/* LOGOUT + UPGRADE BUTTONS (desktop) */}
|
||||||
<div className="flex items-center space-x-4 ml-4 relative z-10">
|
<div className="hidden md:flex items-center space-x-4 ml-4 relative z-10">
|
||||||
{showPremiumCTA && !canAccessPremium && (
|
{showPremiumCTA && !canAccessPremium && (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -586,11 +605,6 @@ const cancelLogout = () => {
|
|||||||
Support
|
Support
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<SupportModal
|
|
||||||
open={supportOpen}
|
|
||||||
onClose={() => setSupportOpen(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* LOGOUT BUTTON */}
|
{/* LOGOUT BUTTON */}
|
||||||
<button
|
<button
|
||||||
onClick={handleLogoutClick}
|
onClick={handleLogoutClick}
|
||||||
@ -617,8 +631,159 @@ const cancelLogout = () => {
|
|||||||
)}
|
)}
|
||||||
</header>
|
</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 & 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 & 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 CONTENT */}
|
||||||
<main className="flex-1 p-6">
|
<main className="flex-1 p-4 md:p-6">
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Default */}
|
{/* Default */}
|
||||||
<Route
|
<Route
|
||||||
@ -696,6 +861,12 @@ const cancelLogout = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Support modal mounted once at root so it centers correctly on desktop & mobile */}
|
||||||
|
<SupportModal
|
||||||
|
open={supportOpen}
|
||||||
|
onClose={() => setSupportOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<ChatDrawer
|
<ChatDrawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
|
@ -120,6 +120,17 @@ function CareerExplorer() {
|
|||||||
Good: 'Good - Less Strong Match',
|
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) ----------
|
// ---------- Load cache on mount (no profile call) ----------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cached = localStorage.getItem('careerSuggestionsCache');
|
const cached = localStorage.getItem('careerSuggestionsCache');
|
||||||
@ -547,7 +558,7 @@ function CareerExplorer() {
|
|||||||
|
|
||||||
// ---- Render
|
// ---- Render
|
||||||
return (
|
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()}
|
{renderLoadingOverlay()}
|
||||||
|
|
||||||
{showModal && (
|
{showModal && (
|
||||||
@ -588,7 +599,9 @@ function CareerExplorer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{careerList.length ? (
|
{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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border p-2">Career</th>
|
<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">{balanceRating}</td>
|
||||||
<td className="border p-2">{recognitionRating}</td>
|
<td className="border p-2">{recognitionRating}</td>
|
||||||
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
|
<td className="border p-2 font-bold">{matchScore.toFixed(1)}%</td>
|
||||||
<td className="border p-2 space-x-2">
|
<td className="border p-2">
|
||||||
<Button className="bg-red-600 text-black-500" onClick={() => removeCareerFromList(career.code)}>
|
<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
|
Remove
|
||||||
</Button>
|
</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={() => {
|
onClick={() => {
|
||||||
// native browser warning before leaving Career Explorer
|
// native browser warning before leaving Career Explorer
|
||||||
const ok = window.confirm(
|
const ok = window.confirm(
|
||||||
@ -692,44 +710,61 @@ function CareerExplorer() {
|
|||||||
state: { socCode: baseSoc, cipCodes: cleanedCips, careerTitle: career.title }
|
state: { socCode: baseSoc, cipCodes: cleanedCips, careerTitle: career.title }
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Plan your Education/Skills
|
Plan your Education/Skills
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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">
|
{/* Filters: stack on mobile; compact controls */}
|
||||||
<select className="border px-3 py-1 rounded" value={selectedJobZone} onChange={(e) => setSelectedJobZone(e.target.value)}>
|
<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>
|
<option value="">All Preparation Levels</option>
|
||||||
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
{Object.entries(jobZoneLabels).map(([zone, label]) => (
|
||||||
<option key={zone} value={zone}>{label}</option>
|
<option key={zone} value={zone}>{label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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>
|
<option value="">All Fit Levels</option>
|
||||||
{Object.entries(fitLabels).map(([key, label]) => (
|
{Object.entries(fitLabels).map(([key, label]) => (
|
||||||
<option key={key} value={key}>{label}</option>
|
<option key={key} value={key}>{label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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
|
Reload Career Suggestions
|
||||||
</Button>
|
</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 className="warning-icon">⚠️</span>
|
||||||
<span>= May have limited data for this career path</span>
|
<span>= May have limited data for this career path</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<CareerSuggestions
|
<CareerSuggestions
|
||||||
careerSuggestions={careerSuggestions}
|
careerSuggestions={filteredSuggestions}
|
||||||
onCareerClick={(career) => { setSelectedCareer(career); handleCareerClick(career); }}
|
onCareerClick={(career) => { setSelectedCareer(career); handleCareerClick(career); }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ export default function ChatDrawer({
|
|||||||
{/* side drawer */}
|
{/* side drawer */}
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
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) */}
|
{/* header (tabs only if retirement bot is allowed) */}
|
||||||
<div className="flex border-b">
|
<div className="flex border-b">
|
||||||
|
@ -458,14 +458,15 @@ const topSchools = filteredAndSortedSchools.slice(0, TOP_N).map(s => ({
|
|||||||
return (
|
return (
|
||||||
<tr key={idx} className="border-b text-sm">
|
<tr key={idx} className="border-b text-sm">
|
||||||
<td className="p-2 font-medium text-gray-800">
|
<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
|
<span
|
||||||
title={definition}
|
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
|
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"
|
||||||
rounded-full bg-blue-500 text-[0.6rem] font-bold text-white cursor-help"
|
|
||||||
>
|
>
|
||||||
i
|
i
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-center text-gray-800">{impStars}</td>
|
<td className="p-2 text-center text-gray-800">{impStars}</td>
|
||||||
<td className="p-2 text-center text-gray-800">{lvlBars}</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">Ability</th>
|
||||||
<th className="p-2 text-center">Importance</th>
|
<th className="p-2 text-center">Importance</th>
|
||||||
<th className="p-2 text-center">Level</th>
|
<th className="p-2 text-center">Level</th>
|
||||||
<th className="p-2 text-center">
|
<th className="p-2">
|
||||||
{/* Info icon for "Why no courses?" */}
|
<div className="w-full flex items-center justify-center gap-1">
|
||||||
<span className="relative inline-block">
|
<span className="whitespace-nowrap">Why no courses?</span>
|
||||||
<span>Why no courses?</span>
|
|
||||||
<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
|
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"
|
||||||
rounded-full bg-blue-500 text-[10px] font-italics text-white cursor-help"
|
|
||||||
title="Abilities are more innate in nature, and difficult to offer courses for them."
|
title="Abilities are more innate in nature, and difficult to offer courses for them."
|
||||||
>
|
>
|
||||||
i
|
i
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -23,7 +23,7 @@ const InterestInventory = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [userProfile, setUserProfile] = 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();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
@ -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 driver’s seat of your career. Whether you’re planning your first step, advancing your current role, or ready to pivot entirely, our platform keeps you in control—helping you adapt, grow, and thrive on your own terms.
|
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 driver’s seat of your career. Whether you’re planning your first step, advancing your current role, or ready to pivot entirely, our platform keeps you in control—helping you adapt, grow, and thrive on your own terms.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc ml-6 mb-4">
|
<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>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>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>
|
<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">
|
<p className="mb-4">
|
||||||
Where would you like to go next?
|
Where would you like to go next?
|
||||||
</p>
|
</p>
|
||||||
<div className="space-x-2">
|
{/* Mobile: stacked full-width; Desktop: original inline buttons with spacing */}
|
||||||
<Link to="/planning" className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
<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
|
Go to Exploring
|
||||||
</Link>
|
</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
|
Go to Preparing
|
||||||
</Link>
|
</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
|
Go to Enhancing
|
||||||
</Link>
|
</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
|
Go to Retirement
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
37
src/utils/net.js
Normal file
37
src/utils/net.js
Normal 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 () => {};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user