diff --git a/src/components/SessionTimer.tsx b/src/components/SessionTimer.tsx new file mode 100644 index 0000000..259d5c9 --- /dev/null +++ b/src/components/SessionTimer.tsx @@ -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 | null = null; + let logoutTimer: ReturnType | 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 ( + +
+
+

+ Session Expiring +

+

+ Your session will expire soon due to inactivity. + Click continue to stay signed in. +

+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/dashboard/MyDashboardPage.tsx b/src/components/dashboard/MyDashboardPage.tsx index 2da9f22..999e86c 100644 --- a/src/components/dashboard/MyDashboardPage.tsx +++ b/src/components/dashboard/MyDashboardPage.tsx @@ -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([]); const [draggingIdx, setDraggingIdx] = createSignal(null); const [visibleWidgets, setVisibleWidgets] = createSignal>(new Set()); + const [profileData, setProfileData] = createSignal>({}); 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) { -
-
- - {verificationStatus().replace(/_/g, ' ')} - -

{verificationTone().title}

-

- {verificationTone().description} -

-
-
- - - - - -
-
+ {}} + onGoBasic={() => props.onNavigate?.('My Profile')} + onGoDocuments={() => props.onNavigate?.('My Profile')} + onGoPortfolio={() => props.onNavigate?.('My Portfolio')} + />
diff --git a/src/components/dashboard/ProfilePage.tsx b/src/components/dashboard/ProfilePage.tsx index 12ba081..154483c 100644 --- a/src/components/dashboard/ProfilePage.tsx +++ b/src/components/dashboard/ProfilePage.tsx @@ -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 (
- setTab("basic")} - onGoDocuments={() => setTab("documents")} - /> -
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 ( -
-
+
+
-
- - {props.statusLabel} - - - {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.`} - + }}> +
+
+ + + + + + {progress()}% + +
+
+

+ {props.approved + ? "Profile Approved" + : props.locked + ? "Verification In Progress" + : allDone() + ? "Ready to Submit" + : `${totalMissing()} Item${totalMissing() > 1 ? "s" : ""} Left`} +

+

+ {props.approved + ? "Your profile is approved" + : props.locked + ? "Verification in progress" + : allDone() + ? "Submit for verification" + : "Complete the steps below to submit"} +

+
+
+ + +
- - -
-
+
Admin request: {props.docRequest}
-
-
-

- 1. Complete required basic profile fields -

- 0} - fallback={

Done.

} - > -

- Missing: {props.missingBasicLabels.join(", ")} -

- -
-
- -
-

- 2. Upload clear required documents -

- 0} - fallback={

Done.

} - > -
- - {(item) => ( - - {item} - - )} - -
- -
-
- -
-

- 3. Add portfolio showcase items -

- 0} - fallback={

Done.

} - > -

- Missing: {portfolioMissing().join(", ")}. Go to My Portfolio and complete these sections. -

-
-
+
+ + +
-

- Document tips: use full-page, readable scans, and match name/address exactly with your profile details. -

+
+

+ 💡 Use full-page, readable scans. Match name and address exactly with your profile details. +

+
); } + +function ChecklistItem(props: { + number: number; + title: string; + done: boolean; + missingLabels: string[]; + onGo?: () => void; + buttonText?: string; +}) { + return ( +
+
+ ✓ +
+
+

+ {props.title} +

+ 0} + fallback={ +

+ ✓ Complete +

+ } + > +
+ + {(label) => ( + + {label} + + )} + +
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/dashboard/widgets/LeadsWidget.tsx b/src/components/dashboard/widgets/LeadsWidget.tsx index 8aa94c5..f3d9005 100644 --- a/src/components/dashboard/widgets/LeadsWidget.tsx +++ b/src/components/dashboard/widgets/LeadsWidget.tsx @@ -109,7 +109,7 @@ export default function LeadsWidget(props: Props) { } > diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 19246b5..666a780 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -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 ( +
Loading dashboard…
@@ -822,6 +824,7 @@ export default function RuntimeDashboardPage() { roleKey={role()} runtimeFields={bundle()?.fields || []} onVerificationStatusChange={(status) => setVerificationStatusOverride(String(status || "").toUpperCase())} + onNavigate={setActiveSidebar} />