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:
parent
a3cc407207
commit
eb61206810
6 changed files with 410 additions and 227 deletions
106
src/components/SessionTimer.tsx
Normal file
106
src/components/SessionTimer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { For, Show, createMemo, createSignal, onMount, Switch, Match } from 'solid-js';
|
||||
import { LayoutDashboard } from 'lucide-solid';
|
||||
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 LeadsWidget from './widgets/LeadsWidget';
|
||||
import JobsWidget from './widgets/JobsWidget';
|
||||
|
|
@ -11,6 +11,8 @@ import ShortlistedWidget from './widgets/ShortlistedWidget';
|
|||
import PortfolioWidget from './widgets/PortfolioWidget';
|
||||
import ProfileCompletionWidget from './widgets/ProfileCompletionWidget';
|
||||
import VerificationWidget from './widgets/VerificationWidget';
|
||||
import VerificationSubmissionGuide from './VerificationSubmissionGuide';
|
||||
import { fetchProfile } from '~/lib/api';
|
||||
|
||||
const NAVY = '#0D0D2A';
|
||||
const ORANGE = '#FF5E13';
|
||||
|
|
@ -67,6 +69,7 @@ export default function MyDashboardPage(props: Props) {
|
|||
const [widgetOrder, setWidgetOrder] = createSignal<string[]>([]);
|
||||
const [draggingIdx, setDraggingIdx] = createSignal<number | null>(null);
|
||||
const [visibleWidgets, setVisibleWidgets] = createSignal<Set<string>>(new Set());
|
||||
const [profileData, setProfileData] = createSignal<Record<string, any>>({});
|
||||
|
||||
const getRoleType = (): string => {
|
||||
if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) return 'PROFESSIONAL';
|
||||
|
|
@ -90,6 +93,56 @@ export default function MyDashboardPage(props: Props) {
|
|||
onMount(() => {
|
||||
setTimeout(() => loadData(), 0);
|
||||
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) => {
|
||||
|
|
@ -377,64 +430,21 @@ export default function MyDashboardPage(props: Props) {
|
|||
</div>
|
||||
|
||||
<Show when={showVerificationPrompt()}>
|
||||
<div
|
||||
style={{
|
||||
...CARD,
|
||||
border: '1px solid #E5E7EB',
|
||||
background: 'white',
|
||||
display: 'grid',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: '8px' }}>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
'align-items': 'center',
|
||||
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>
|
||||
<VerificationSubmissionGuide
|
||||
statusLabel={verificationStatus().replace(/_/g, ' ')}
|
||||
statusColor="#B7791F"
|
||||
locked={verificationStatus() === 'UNDER_REVIEW' || verificationStatus() === 'PENDING'}
|
||||
approved={verificationStatus() === 'APPROVED'}
|
||||
missingBasicLabels={missingBasicLabels()}
|
||||
missingDocLabels={missingDocLabels()}
|
||||
missingPortfolioLabels={missingPortfolioLabels()}
|
||||
canSubmit={missingBasicLabels().length === 0 && missingDocLabels().length === 0 && missingPortfolioLabels().length === 0}
|
||||
submitting={false}
|
||||
onSubmit={() => {}}
|
||||
onGoBasic={() => props.onNavigate?.('My Profile')}
|
||||
onGoDocuments={() => props.onNavigate?.('My Profile')}
|
||||
onGoPortfolio={() => props.onNavigate?.('My Portfolio')}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div style={CARD}>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
LABEL,
|
||||
BTN_PRIMARY,
|
||||
} from "~/components/DashboardShell";
|
||||
import VerificationSubmissionGuide from "~/components/dashboard/VerificationSubmissionGuide";
|
||||
|
||||
const API = "/api/gateway";
|
||||
|
||||
|
|
@ -397,6 +396,7 @@ interface Props {
|
|||
roleKey: string;
|
||||
runtimeFields?: string[];
|
||||
onVerificationStatusChange?: (status: string) => void;
|
||||
onNavigate?: (key: string) => void;
|
||||
}
|
||||
|
||||
type Tab = "basic" | "documents";
|
||||
|
|
@ -627,22 +627,6 @@ export default function ProfilePage(props: Props) {
|
|||
|
||||
return (
|
||||
<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()}>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 = {
|
||||
statusLabel: string;
|
||||
|
|
@ -15,176 +15,256 @@ type Props = {
|
|||
onSubmit: () => void;
|
||||
onGoBasic: () => void;
|
||||
onGoDocuments: () => void;
|
||||
onGoPortfolio: () => void;
|
||||
};
|
||||
|
||||
export default function VerificationSubmissionGuide(props: Props) {
|
||||
const portfolioMissing = () => props.missingPortfolioLabels ?? [];
|
||||
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 (
|
||||
<div style={{ ...CARD, "margin-bottom": "16px", display: "grid", gap: "12px" }}>
|
||||
<div
|
||||
style={{
|
||||
<div style={{ ...CARD, "margin-bottom": "16px", padding: "0", overflow: "hidden" }}>
|
||||
<div style={{
|
||||
padding: "20px 24px",
|
||||
"border-bottom": "1px solid #E5E7EB",
|
||||
background: "#FFFFFF",
|
||||
}}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "space-between",
|
||||
gap: "10px",
|
||||
gap: "16px",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "10px" }}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
"align-items": "center",
|
||||
height: "24px",
|
||||
padding: "0 10px",
|
||||
"border-radius": "999px",
|
||||
border: "1px solid #E5E7EB",
|
||||
background: "#F9FAFB",
|
||||
color: "#4B5563",
|
||||
"font-size": "11px",
|
||||
"font-weight": "700",
|
||||
}}
|
||||
>
|
||||
{props.statusLabel}
|
||||
</span>
|
||||
<span style={{ "font-size": "13px", color: "#6B7280" }}>
|
||||
{props.approved
|
||||
? "Your profile is approved."
|
||||
: props.locked
|
||||
? "Verification in progress."
|
||||
: totalMissing() === 0
|
||||
? "Ready to submit for verification."
|
||||
: `${totalMissing()} item${totalMissing() > 1 ? "s" : ""} left before submission.`}
|
||||
</span>
|
||||
}}>
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "14px" }}>
|
||||
<div style={{
|
||||
position: "relative",
|
||||
width: "44px",
|
||||
height: "44px",
|
||||
}}>
|
||||
<svg viewBox="0 0 36 36" style={{ width: "44px", height: "44px", transform: "rotate(-90deg)" }}>
|
||||
<circle cx="18" cy="18" r="15" fill="none" stroke="#E5E7EB" stroke-width="2.5" />
|
||||
<circle
|
||||
cx="18" cy="18" r="15" fill="none"
|
||||
stroke={NAVY}
|
||||
stroke-width="2.5"
|
||||
stroke-dasharray={`${progress()} 100`}
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span style={{
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
"font-size": "12px",
|
||||
"font-weight": "700",
|
||||
color: NAVY,
|
||||
}}>
|
||||
{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>
|
||||
<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>
|
||||
|
||||
<Show when={props.docRequest}>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #FED7AA",
|
||||
background: "#FFF7ED",
|
||||
padding: "10px 12px",
|
||||
"border-radius": "10px",
|
||||
"font-size": "12px",
|
||||
color: "#9A3412",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
margin: "16px 24px",
|
||||
background: "#FEF3C7",
|
||||
padding: "12px 16px",
|
||||
"border-radius": "8px",
|
||||
"font-size": "13px",
|
||||
color: "#92400E",
|
||||
}}>
|
||||
<strong>Admin request:</strong> {props.docRequest}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<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" }}>
|
||||
1. Complete required basic profile fields
|
||||
</p>
|
||||
<Show
|
||||
when={props.missingBasicLabels.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: {props.missingBasicLabels.join(", ")}
|
||||
</p>
|
||||
<button type="button" onClick={props.onGoBasic} style={{ ...BTN_GHOST, height: "30px", "margin-top": "8px", "font-size": "12px" }}>
|
||||
Go to Basic Information
|
||||
</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" }}>
|
||||
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 style={{ padding: "16px 24px 20px", display: "grid", gap: "10px" }}>
|
||||
<ChecklistItem
|
||||
number={1}
|
||||
title="Complete required basic profile fields"
|
||||
done={props.missingBasicLabels.length === 0}
|
||||
missingLabels={props.missingBasicLabels}
|
||||
onGo={props.onGoBasic}
|
||||
buttonText="Go to Basic Information"
|
||||
/>
|
||||
<ChecklistItem
|
||||
number={2}
|
||||
title="Upload clear required documents"
|
||||
done={props.missingDocLabels.length === 0}
|
||||
missingLabels={props.missingDocLabels}
|
||||
onGo={props.onGoDocuments}
|
||||
buttonText="Go to Documents"
|
||||
/>
|
||||
<ChecklistItem
|
||||
number={3}
|
||||
title="Add portfolio showcase items"
|
||||
done={portfolioMissing().length === 0}
|
||||
missingLabels={portfolioMissing()}
|
||||
onGo={props.onGoPortfolio}
|
||||
buttonText="Go to My Portfolio"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>
|
||||
Document tips: use full-page, readable scans, and match name/address exactly with your profile details.
|
||||
</p>
|
||||
<div style={{
|
||||
padding: "12px 24px",
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ export default function LeadsWidget(props: Props) {
|
|||
<DashboardWidget
|
||||
title={isProfessional() ? 'My Lead Requests' : 'My Requirements'}
|
||||
loading={data.loading}
|
||||
error={data.error ? 'Failed to load' : undefined}
|
||||
error={data.error ? 'Service unavailable' : undefined}
|
||||
icon={<MapPin size={16} />}
|
||||
>
|
||||
<Show when={stats()}>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import ExploreServicesPage from "~/components/dashboard/ExploreServicesPage";
|
|||
import HelpCenterDashboardPage from "~/components/dashboard/HelpCenterDashboardPage";
|
||||
import SwitchServicesPage from "~/components/dashboard/SwitchServicesPage";
|
||||
import LogoutPage from "~/components/dashboard/LogoutPage";
|
||||
import SessionTimer from "~/components/SessionTimer";
|
||||
import { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared";
|
||||
|
||||
// Sidebar items that have real data implementations (wired to backend APIs)
|
||||
|
|
@ -788,6 +789,7 @@ export default function RuntimeDashboardPage() {
|
|||
|
||||
return (
|
||||
<RequireAuth>
|
||||
<SessionTimer />
|
||||
<main style={{ "min-height": "100vh", background: "#F3F4F6" }}>
|
||||
<Show when={loading()}>
|
||||
<div style={cardStyle}>Loading dashboard…</div>
|
||||
|
|
@ -822,6 +824,7 @@ export default function RuntimeDashboardPage() {
|
|||
roleKey={role()}
|
||||
runtimeFields={bundle()?.fields || []}
|
||||
onVerificationStatusChange={(status) => setVerificationStatusOverride(String(status || "").toUpperCase())}
|
||||
onNavigate={setActiveSidebar}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={activeSidebarKey() === "my portfolio"}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue