feat: dashboard session timer and verification UI improvements

- Add SessionTimer component with 13min warning / 15min idle auto-logout
- Move VerificationSubmissionGuide from ProfilePage to MyDashboardPage
- Remove duplicate VerificationSubmissionGuide from ProfilePage
- Fix 'Go to My Portfolio' button to navigate properly
- Change error messages to 'Service unavailable' for failed widget loads
- Brand color updates for VerificationSubmissionGuide
This commit is contained in:
Tracewebstudio Dev 2026-05-05 17:22:04 +02:00
parent a3cc407207
commit eb61206810
6 changed files with 410 additions and 227 deletions

View file

@ -0,0 +1,106 @@
import { Show, createSignal, onCleanup, onMount } from 'solid-js';
import { useAuth } from '~/lib/auth';
const WARNING_AT = 13 * 60 * 1000;
const LOGOUT_AT = 15 * 60 * 1000;
export default function SessionTimer() {
const auth = useAuth();
const [showWarning, setShowWarning] = createSignal(false);
let warningTimer: ReturnType<typeof setTimeout> | null = null;
let logoutTimer: ReturnType<typeof setTimeout> | null = null;
let lastActivity = Date.now();
const resetTimers = () => {
lastActivity = Date.now();
setShowWarning(false);
if (warningTimer) clearTimeout(warningTimer);
if (logoutTimer) clearTimeout(logoutTimer);
warningTimer = setTimeout(() => setShowWarning(true), WARNING_AT);
logoutTimer = setTimeout(() => auth.logout(), LOGOUT_AT);
};
const onActivity = () => {
if (auth.isAuthenticated()) resetTimers();
};
onMount(() => {
if (!auth.isAuthenticated()) return;
resetTimers();
const events = ['mousedown', 'keydown', 'touchstart', 'scroll'];
for (const ev of events) {
document.addEventListener(ev, onActivity, { passive: true });
}
onCleanup(() => {
if (warningTimer) clearTimeout(warningTimer);
if (logoutTimer) clearTimeout(logoutTimer);
for (const ev of events) {
document.removeEventListener(ev, onActivity);
}
});
});
return (
<Show when={showWarning()}>
<div style={{
position: 'fixed',
top: '0',
left: '0',
right: '0',
bottom: '0',
background: 'rgba(0,0,0,0.5)',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'z-index': '9999',
}}>
<div style={{
background: '#FFF',
'border-radius': '16px',
padding: '32px',
'max-width': '420px',
width: '90%',
'text-align': 'center',
'box-shadow': '0 20px 60px rgba(0,0,0,0.3)',
}}>
<p style={{
margin: '0 0 8px',
'font-size': '20px',
'font-weight': '800',
color: '#0D0D2A',
}}>
Session Expiring
</p>
<p style={{
margin: '0 0 24px',
'font-size': '14px',
color: '#6B7280',
'line-height': '1.6',
}}>
Your session will expire soon due to inactivity.
Click continue to stay signed in.
</p>
<button
type="button"
onClick={resetTimers}
style={{
background: '#FF5E13',
color: '#FFF',
border: 'none',
'border-radius': '10px',
padding: '12px 32px',
'font-size': '15px',
'font-weight': '700',
cursor: 'pointer',
width: '100%',
}}
>
Continue Session
</button>
</div>
</div>
</Show>
);
}

View file

@ -1,7 +1,7 @@
import { For, Show, createMemo, createSignal, onMount, Switch, Match } from 'solid-js'; import { For, Show, createMemo, createSignal, onMount, Switch, Match } from 'solid-js';
import { LayoutDashboard } from 'lucide-solid'; import { LayoutDashboard } from 'lucide-solid';
import { BTN_GHOST, BTN_PRIMARY, CARD } from '~/components/DashboardShell'; import { BTN_GHOST, BTN_PRIMARY, CARD } from '~/components/DashboardShell';
import { normalizeRole, PROFESSIONAL_ROLE_SET, type RoleKey } from './RoleDashboardShared'; import { normalizeRole, PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
import WalletWidget from './widgets/WalletWidget'; import WalletWidget from './widgets/WalletWidget';
import LeadsWidget from './widgets/LeadsWidget'; import LeadsWidget from './widgets/LeadsWidget';
import JobsWidget from './widgets/JobsWidget'; import JobsWidget from './widgets/JobsWidget';
@ -11,6 +11,8 @@ import ShortlistedWidget from './widgets/ShortlistedWidget';
import PortfolioWidget from './widgets/PortfolioWidget'; import PortfolioWidget from './widgets/PortfolioWidget';
import ProfileCompletionWidget from './widgets/ProfileCompletionWidget'; import ProfileCompletionWidget from './widgets/ProfileCompletionWidget';
import VerificationWidget from './widgets/VerificationWidget'; import VerificationWidget from './widgets/VerificationWidget';
import VerificationSubmissionGuide from './VerificationSubmissionGuide';
import { fetchProfile } from '~/lib/api';
const NAVY = '#0D0D2A'; const NAVY = '#0D0D2A';
const ORANGE = '#FF5E13'; const ORANGE = '#FF5E13';
@ -67,6 +69,7 @@ export default function MyDashboardPage(props: Props) {
const [widgetOrder, setWidgetOrder] = createSignal<string[]>([]); const [widgetOrder, setWidgetOrder] = createSignal<string[]>([]);
const [draggingIdx, setDraggingIdx] = createSignal<number | null>(null); const [draggingIdx, setDraggingIdx] = createSignal<number | null>(null);
const [visibleWidgets, setVisibleWidgets] = createSignal<Set<string>>(new Set()); const [visibleWidgets, setVisibleWidgets] = createSignal<Set<string>>(new Set());
const [profileData, setProfileData] = createSignal<Record<string, any>>({});
const getRoleType = (): string => { const getRoleType = (): string => {
if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) return 'PROFESSIONAL'; if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) return 'PROFESSIONAL';
@ -90,6 +93,56 @@ export default function MyDashboardPage(props: Props) {
onMount(() => { onMount(() => {
setTimeout(() => loadData(), 0); setTimeout(() => loadData(), 0);
initWidgetOrder(); initWidgetOrder();
loadProfileData();
});
const loadProfileData = async () => {
const prefix = ROLE_PREFIXES[props.roleKey];
if (!prefix) return;
try {
const data = await fetchProfile(prefix);
if (data) setProfileData(data);
} catch { /* ignore */ }
};
const missingBasicLabels = createMemo(() => {
const data = profileData();
const missing: string[] = [];
if (!data) return missing;
const p = data.profile || data;
if (!String(p?.first_name || '').trim()) missing.push('First Name');
if (!String(p?.last_name || '').trim()) missing.push('Last Name');
if (!String(p?.email || '').trim()) missing.push('Email Address');
if (!String(p?.phone || '').trim()) missing.push('Mobile Number');
if (!String(p?.address_line_1 || p?.address || '').trim()) missing.push('Address Line 1');
if (!String(p?.city || '').trim()) missing.push('City');
if (!String(p?.area || '').trim()) missing.push('Area');
if (!String(p?.state || '').trim()) missing.push('State');
return missing;
});
const missingDocLabels = createMemo(() => {
const data = profileData();
if (!data) return [];
const docs = data.documents || data.documents_data || [];
const missing: string[] = [];
if (!docs.some((d: any) => d?.doc_type === 'identity')) missing.push('Identity Proof');
if (!docs.some((d: any) => d?.doc_type === 'address')) missing.push('Address Proof');
if (!docs.some((d: any) => d?.doc_type === 'portfolio')) missing.push('Portfolio Ownership Proof');
return missing;
});
const missingPortfolioLabels = createMemo(() => {
const data = profileData();
if (!data) return ['About', 'Services & pricing', 'Experience / tools', 'FAQs', 'Showcase items'];
const p = data.portfolio || data.custom_data || {};
const missing: string[] = [];
if (!String(p?.about || p?.bio || '').trim()) missing.push('About');
if (!String(p?.services || p?.pricing || '').trim()) missing.push('Services & pricing');
if (!String(p?.experience || p?.tools || '').trim()) missing.push('Experience / tools');
if (!String(p?.faqs || '').trim()) missing.push('FAQs');
if (!String(p?.showcase || p?.portfolio_items || '').trim()) missing.push('Showcase items');
return missing.length > 0 ? missing : [];
}); });
const moveWidget = (fromIdx: number, toIdx: number) => { const moveWidget = (fromIdx: number, toIdx: number) => {
@ -377,64 +430,21 @@ export default function MyDashboardPage(props: Props) {
</div> </div>
<Show when={showVerificationPrompt()}> <Show when={showVerificationPrompt()}>
<div <VerificationSubmissionGuide
style={{ statusLabel={verificationStatus().replace(/_/g, ' ')}
...CARD, statusColor="#B7791F"
border: '1px solid #E5E7EB', locked={verificationStatus() === 'UNDER_REVIEW' || verificationStatus() === 'PENDING'}
background: 'white', approved={verificationStatus() === 'APPROVED'}
display: 'grid', missingBasicLabels={missingBasicLabels()}
gap: '10px', missingDocLabels={missingDocLabels()}
}} missingPortfolioLabels={missingPortfolioLabels()}
> canSubmit={missingBasicLabels().length === 0 && missingDocLabels().length === 0 && missingPortfolioLabels().length === 0}
<div style={{ display: 'grid', gap: '8px' }}> submitting={false}
<span onSubmit={() => {}}
style={{ onGoBasic={() => props.onNavigate?.('My Profile')}
display: 'inline-flex', onGoDocuments={() => props.onNavigate?.('My Profile')}
'align-items': 'center', onGoPortfolio={() => props.onNavigate?.('My Portfolio')}
width: 'fit-content', />
height: '24px',
padding: '0 10px',
'border-radius': '999px',
border: `1px solid ${verificationTone().badgeBorder}`,
background: verificationTone().badgeBackground,
color: verificationTone().badgeColor,
'font-size': '11px',
'font-weight': '700',
}}
>
{verificationStatus().replace(/_/g, ' ')}
</span>
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '800', color: '#111827' }}>{verificationTone().title}</p>
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>
{verificationTone().description}
</p>
</div>
<div style={{ display: 'flex', gap: '8px', 'flex-wrap': 'wrap' }}>
<button
type="button"
onClick={() => props.onNavigate?.('My Profile')}
style={{ ...BTN_PRIMARY, height: '32px', 'font-size': '12px', padding: '0 12px' }}
>
Go to My Profile
</button>
<Show when={showPortfolioCta()}>
<button
type="button"
onClick={() => props.onNavigate?.('My Portfolio')}
style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}
>
Go to My Portfolio
</button>
</Show>
<button
type="button"
onClick={() => props.onNavigate?.('Verification')}
style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}
>
Open Verification
</button>
</div>
</div>
</Show> </Show>
<div style={CARD}> <div style={CARD}>

View file

@ -11,7 +11,6 @@ import {
LABEL, LABEL,
BTN_PRIMARY, BTN_PRIMARY,
} from "~/components/DashboardShell"; } from "~/components/DashboardShell";
import VerificationSubmissionGuide from "~/components/dashboard/VerificationSubmissionGuide";
const API = "/api/gateway"; const API = "/api/gateway";
@ -397,6 +396,7 @@ interface Props {
roleKey: string; roleKey: string;
runtimeFields?: string[]; runtimeFields?: string[];
onVerificationStatusChange?: (status: string) => void; onVerificationStatusChange?: (status: string) => void;
onNavigate?: (key: string) => void;
} }
type Tab = "basic" | "documents"; type Tab = "basic" | "documents";
@ -627,22 +627,6 @@ export default function ProfilePage(props: Props) {
return ( return (
<div style={{ "max-width": "760px" }}> <div style={{ "max-width": "760px" }}>
<VerificationSubmissionGuide
statusLabel={statusLabel[verificationStatus()] ?? verificationStatus()}
statusColor={statusColor[verificationStatus()] ?? "#9CA3AF"}
locked={isLocked()}
approved={verificationStatus() === "APPROVED"}
docRequest={docRequest()}
missingBasicLabels={missingBasicLabels()}
missingDocLabels={missingDocLabels()}
missingPortfolioLabels={missingPortfolioLabels()}
canSubmit={canSubmitVerification()}
submitting={submitting()}
onSubmit={handleSubmitForVerification}
onGoBasic={() => setTab("basic")}
onGoDocuments={() => setTab("documents")}
/>
<Show when={submitMsg()}> <Show when={submitMsg()}>
<div <div
style={{ style={{

View file

@ -1,5 +1,5 @@
import { For, Show } from "solid-js"; import { For, Show } from "solid-js";
import { BTN_GHOST, BTN_PRIMARY, CARD } from "../DashboardShell"; import { BTN_GHOST, BTN_PRIMARY, CARD, ORANGE, NAVY } from "../DashboardShell";
type Props = { type Props = {
statusLabel: string; statusLabel: string;
@ -15,176 +15,256 @@ type Props = {
onSubmit: () => void; onSubmit: () => void;
onGoBasic: () => void; onGoBasic: () => void;
onGoDocuments: () => void; onGoDocuments: () => void;
onGoPortfolio: () => void;
}; };
export default function VerificationSubmissionGuide(props: Props) { export default function VerificationSubmissionGuide(props: Props) {
const portfolioMissing = () => props.missingPortfolioLabels ?? []; const portfolioMissing = () => props.missingPortfolioLabels ?? [];
const totalMissing = () => props.missingBasicLabels.length + props.missingDocLabels.length + portfolioMissing().length; const totalMissing = () => props.missingBasicLabels.length + props.missingDocLabels.length + portfolioMissing().length;
const allDone = () => totalMissing() === 0;
const progress = () => {
const total = 3;
const done = [
props.missingBasicLabels.length === 0,
props.missingDocLabels.length === 0,
portfolioMissing().length === 0,
].filter(Boolean).length;
return Math.round((done / total) * 100);
};
return ( return (
<div style={{ ...CARD, "margin-bottom": "16px", display: "grid", gap: "12px" }}> <div style={{ ...CARD, "margin-bottom": "16px", padding: "0", overflow: "hidden" }}>
<div <div style={{
style={{ padding: "20px 24px",
"border-bottom": "1px solid #E5E7EB",
background: "#FFFFFF",
}}>
<div style={{
display: "flex", display: "flex",
"align-items": "center", "align-items": "center",
"justify-content": "space-between", "justify-content": "space-between",
gap: "10px", gap: "16px",
"flex-wrap": "wrap", "flex-wrap": "wrap",
}} }}>
> <div style={{ display: "flex", "align-items": "center", gap: "14px" }}>
<div style={{ display: "flex", "align-items": "center", gap: "10px" }}> <div style={{
<span position: "relative",
style={{ width: "44px",
display: "inline-flex", height: "44px",
"align-items": "center", }}>
height: "24px", <svg viewBox="0 0 36 36" style={{ width: "44px", height: "44px", transform: "rotate(-90deg)" }}>
padding: "0 10px", <circle cx="18" cy="18" r="15" fill="none" stroke="#E5E7EB" stroke-width="2.5" />
"border-radius": "999px", <circle
border: "1px solid #E5E7EB", cx="18" cy="18" r="15" fill="none"
background: "#F9FAFB", stroke={NAVY}
color: "#4B5563", stroke-width="2.5"
"font-size": "11px", stroke-dasharray={`${progress()} 100`}
"font-weight": "700", stroke-linecap="round"
}} />
> </svg>
{props.statusLabel} <span style={{
</span> position: "absolute",
<span style={{ "font-size": "13px", color: "#6B7280" }}> inset: "0",
{props.approved display: "flex",
? "Your profile is approved." "align-items": "center",
: props.locked "justify-content": "center",
? "Verification in progress." "font-size": "12px",
: totalMissing() === 0 "font-weight": "700",
? "Ready to submit for verification." color: NAVY,
: `${totalMissing()} item${totalMissing() > 1 ? "s" : ""} left before submission.`} }}>
</span> {progress()}%
</span>
</div>
<div>
<p style={{
margin: "0 0 2px",
"font-size": "14px",
"font-weight": "600",
color: NAVY,
}}>
{props.approved
? "Profile Approved"
: props.locked
? "Verification In Progress"
: allDone()
? "Ready to Submit"
: `${totalMissing()} Item${totalMissing() > 1 ? "s" : ""} Left`}
</p>
<p style={{
margin: "0",
"font-size": "12px",
color: "#6B7280",
}}>
{props.approved
? "Your profile is approved"
: props.locked
? "Verification in progress"
: allDone()
? "Submit for verification"
: "Complete the steps below to submit"}
</p>
</div>
</div>
<Show when={!props.locked && !props.approved}>
<button
type="button"
onClick={props.onSubmit}
disabled={!props.canSubmit || props.submitting}
style={{
...BTN_PRIMARY,
background: ORANGE,
color: "#FFFFFF",
border: "none",
opacity: !props.canSubmit || props.submitting ? "0.5" : "1",
cursor: !props.canSubmit || props.submitting ? "not-allowed" : "pointer",
padding: "10px 20px",
"font-weight": "600",
"border-radius": "8px",
}}
>
{props.submitting ? "Submitting..." : "Submit for Verification"}
</button>
</Show>
</div> </div>
<Show when={!props.locked && !props.approved}>
<button
type="button"
onClick={props.onSubmit}
disabled={!props.canSubmit || props.submitting}
style={{
...BTN_PRIMARY,
background: "#0D0D2A",
color: "#FFFFFF",
border: "none",
opacity: "1",
cursor: !props.canSubmit || props.submitting ? "not-allowed" : "pointer",
}}
>
{props.submitting ? "Submitting..." : "Submit for Verification"}
</button>
</Show>
</div> </div>
<Show when={props.docRequest}> <Show when={props.docRequest}>
<div <div style={{
style={{ margin: "16px 24px",
border: "1px solid #FED7AA", background: "#FEF3C7",
background: "#FFF7ED", padding: "12px 16px",
padding: "10px 12px", "border-radius": "8px",
"border-radius": "10px", "font-size": "13px",
"font-size": "12px", color: "#92400E",
color: "#9A3412", }}>
}}
>
<strong>Admin request:</strong> {props.docRequest} <strong>Admin request:</strong> {props.docRequest}
</div> </div>
</Show> </Show>
<div style={{ display: "grid", gap: "8px" }}> <div style={{ padding: "16px 24px 20px", display: "grid", gap: "10px" }}>
<div <ChecklistItem
style={{ number={1}
border: "1px solid #E5E7EB", title="Complete required basic profile fields"
"border-radius": "10px", done={props.missingBasicLabels.length === 0}
padding: "10px 12px", missingLabels={props.missingBasicLabels}
background: "#FFFFFF", onGo={props.onGoBasic}
}} buttonText="Go to Basic Information"
> />
<p style={{ margin: "0", "font-size": "12px", "font-weight": "700", color: "#111827" }}> <ChecklistItem
1. Complete required basic profile fields number={2}
</p> title="Upload clear required documents"
<Show done={props.missingDocLabels.length === 0}
when={props.missingBasicLabels.length > 0} missingLabels={props.missingDocLabels}
fallback={<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>Done.</p>} onGo={props.onGoDocuments}
> buttonText="Go to Documents"
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}> />
Missing: {props.missingBasicLabels.join(", ")} <ChecklistItem
</p> number={3}
<button type="button" onClick={props.onGoBasic} style={{ ...BTN_GHOST, height: "30px", "margin-top": "8px", "font-size": "12px" }}> title="Add portfolio showcase items"
Go to Basic Information done={portfolioMissing().length === 0}
</button> missingLabels={portfolioMissing()}
</Show> onGo={props.onGoPortfolio}
</div> buttonText="Go to My Portfolio"
/>
<div
style={{
border: "1px solid #E5E7EB",
"border-radius": "10px",
padding: "10px 12px",
background: "#FFFFFF",
}}
>
<p style={{ margin: "0", "font-size": "12px", "font-weight": "700", color: "#111827" }}>
2. Upload clear required documents
</p>
<Show
when={props.missingDocLabels.length > 0}
fallback={<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>Done.</p>}
>
<div style={{ display: "flex", gap: "6px", "flex-wrap": "wrap", "margin-top": "6px" }}>
<For each={props.missingDocLabels}>
{(item) => (
<span
style={{
height: "22px",
display: "inline-flex",
"align-items": "center",
padding: "0 8px",
"border-radius": "999px",
border: "1px solid #E5E7EB",
background: "#F9FAFB",
"font-size": "11px",
color: "#6B7280",
}}
>
{item}
</span>
)}
</For>
</div>
<button type="button" onClick={props.onGoDocuments} style={{ ...BTN_GHOST, height: "30px", "margin-top": "8px", "font-size": "12px" }}>
Go to Documents
</button>
</Show>
</div>
<div
style={{
border: "1px solid #E5E7EB",
"border-radius": "10px",
padding: "10px 12px",
background: "#FFFFFF",
}}
>
<p style={{ margin: "0", "font-size": "12px", "font-weight": "700", color: "#111827" }}>
3. Add portfolio showcase items
</p>
<Show
when={portfolioMissing().length > 0}
fallback={<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>Done.</p>}
>
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
Missing: {portfolioMissing().join(", ")}. Go to <strong>My Portfolio</strong> and complete these sections.
</p>
</Show>
</div>
</div> </div>
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}> <div style={{
Document tips: use full-page, readable scans, and match name/address exactly with your profile details. padding: "12px 24px",
</p> background: "#F9FAFB",
"border-top": "1px solid #E5E7EB",
}}>
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>
💡 Use full-page, readable scans. Match name and address exactly with your profile details.
</p>
</div>
</div> </div>
); );
} }
function ChecklistItem(props: {
number: number;
title: string;
done: boolean;
missingLabels: string[];
onGo?: () => void;
buttonText?: string;
}) {
return (
<div style={{
display: "flex",
gap: "12px",
padding: "12px 14px",
background: "#FFFFFF",
border: "1px solid #E5E7EB",
"border-radius": "8px",
}}>
<div style={{
width: "24px",
height: "24px",
"border-radius": "50%",
background: props.done ? NAVY : "#E5E7EB",
color: props.done ? "#fff" : "#9CA3AF",
display: "flex",
"align-items": "center",
"justify-content": "center",
"font-size": "12px",
"font-weight": "700",
"flex-shrink": "0",
}}>
<Show when={props.done} fallback={props.number}></Show>
</div>
<div style={{ flex: "1", "min-width": "0" }}>
<p style={{ margin: "0", "font-size": "13px", "font-weight": "500", color: "#111827" }}>
{props.title}
</p>
<Show
when={props.missingLabels.length > 0}
fallback={
<p style={{ margin: "2px 0 0", "font-size": "12px", color: NAVY, "font-weight": "500" }}>
Complete
</p>
}
>
<div style={{ display: "flex", "flex-wrap": "wrap", gap: "6px", "margin-top": "6px" }}>
<For each={props.missingLabels}>
{(label) => (
<span style={{
display: "inline-flex",
"align-items": "center",
padding: "2px 8px",
"border-radius": "4px",
background: "#F3F4F6",
color: "#374151",
"font-size": "11px",
}}>
{label}
</span>
)}
</For>
</div>
<Show when={props.onGo}>
<button
type="button"
onClick={props.onGo}
style={{
...BTN_GHOST,
height: "28px",
"font-size": "11px",
"font-weight": "500",
color: ORANGE,
background: "#FFF7ED",
border: "none",
padding: "0 10px",
"border-radius": "4px",
"margin-top": "8px",
}}
>
{props.buttonText}
</button>
</Show>
</Show>
</div>
</div>
);
}

View file

@ -109,7 +109,7 @@ export default function LeadsWidget(props: Props) {
<DashboardWidget <DashboardWidget
title={isProfessional() ? 'My Lead Requests' : 'My Requirements'} title={isProfessional() ? 'My Lead Requests' : 'My Requirements'}
loading={data.loading} loading={data.loading}
error={data.error ? 'Failed to load' : undefined} error={data.error ? 'Service unavailable' : undefined}
icon={<MapPin size={16} />} icon={<MapPin size={16} />}
> >
<Show when={stats()}> <Show when={stats()}>

View file

@ -33,6 +33,7 @@ import ExploreServicesPage from "~/components/dashboard/ExploreServicesPage";
import HelpCenterDashboardPage from "~/components/dashboard/HelpCenterDashboardPage"; import HelpCenterDashboardPage from "~/components/dashboard/HelpCenterDashboardPage";
import SwitchServicesPage from "~/components/dashboard/SwitchServicesPage"; import SwitchServicesPage from "~/components/dashboard/SwitchServicesPage";
import LogoutPage from "~/components/dashboard/LogoutPage"; import LogoutPage from "~/components/dashboard/LogoutPage";
import SessionTimer from "~/components/SessionTimer";
import { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared"; import { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared";
// Sidebar items that have real data implementations (wired to backend APIs) // Sidebar items that have real data implementations (wired to backend APIs)
@ -788,6 +789,7 @@ export default function RuntimeDashboardPage() {
return ( return (
<RequireAuth> <RequireAuth>
<SessionTimer />
<main style={{ "min-height": "100vh", background: "#F3F4F6" }}> <main style={{ "min-height": "100vh", background: "#F3F4F6" }}>
<Show when={loading()}> <Show when={loading()}>
<div style={cardStyle}>Loading dashboard</div> <div style={cardStyle}>Loading dashboard</div>
@ -822,6 +824,7 @@ export default function RuntimeDashboardPage() {
roleKey={role()} roleKey={role()}
runtimeFields={bundle()?.fields || []} runtimeFields={bundle()?.fields || []}
onVerificationStatusChange={(status) => setVerificationStatusOverride(String(status || "").toUpperCase())} onVerificationStatusChange={(status) => setVerificationStatusOverride(String(status || "").toUpperCase())}
onNavigate={setActiveSidebar}
/> />
</Match> </Match>
<Match when={activeSidebarKey() === "my portfolio"}> <Match when={activeSidebarKey() === "my portfolio"}>