import { Component, Show, createEffect, For, createSignal, onMount } from 'solid-js'; import { useNavigate, A, useSearchParams } from '@solidjs/router'; import { authState, logout, switchRole, bootstrapAuth, setMockRuntimeConfig } from '~/lib/auth'; import { shouldShowRoleSwitcher } from '~/lib/auth-flow'; import { getRoleTourStorageKey, getWelcomeTourStorageKey, pickGuidedTour, readSeenRoleTours, WELCOME_TOUR_VALUE, writeSeenRoleTours, } from '~/lib/guided-tour'; import type { GuidedTourKind } from '~/lib/guided-tour'; import { type GuidedTourStep, resolveRoleApprovedTourSteps, resolveWelcomeTourSteps, } from '~/lib/guided-tour-content'; // ── Icons (inline SVGs for zero deps) ───────────────────────────────────────── const IconDashboard = () => ( ); const IconJobs = () => ( ); const IconBell = () => ( ); const IconSettings = () => ( ); const IconLogout = () => ( ); const IconCompass = () => ( ); const IconPortfolio = () => ( ); const IconServices = () => ( ); const IconWallet = () => ( ); // ── Module → nav item mapping ───────────────────────────────────────────────── const MODULE_NAV_MAP: Record = { // Next.js role-dashboard module keys dashboard: { label: 'Dashboard', href: '/users/dashboard', icon: IconDashboard, tourId: 'dashboard', order: 1 }, onboarding: { label: 'Get Started', href: '/users/onboarding', icon: IconCompass, tourId: 'onboarding', order: 2 }, role_selection: { label: 'Choose Your Role', href: '/users/onboarding/role-selection', icon: IconCompass, tourId: 'role-selection', order: 3 }, profile: { label: 'Profile', href: '/users/profile', icon: IconSettings, tourId: 'profile', order: 4 }, leads: { label: 'Leads', href: '/users/leads', icon: IconJobs, tourId: 'requests', order: 5 }, job_postings: { label: 'Job Postings', href: '/companies/job-postings', icon: IconJobs, tourId: 'jobs', order: 6 }, applications: { label: 'Applications', href: '/companies/applications', icon: IconJobs, tourId: 'applications', order: 7 }, portfolio: { label: 'Portfolio', href: '/users/professional/portfolio', icon: IconPortfolio, tourId: 'portfolio', order: 8 }, verification: { label: 'Verification Status', href: '/users/verification-status', icon: IconSettings, tourId: 'verification', order: 9 }, tracecoins: { label: 'Tracecoins', href: '/companies/tracecoins', icon: IconWallet, tourId: 'wallet', order: 10 }, feedback: { label: 'Feedback', href: '/companies/feedback', icon: IconBell, tourId: 'support', order: 11 }, notifications: { label: 'Notifications', href: '/users/notifications', icon: IconBell, tourId: 'notifications', order: 12 }, support: { label: 'Support', href: '/users/support', icon: IconBell, tourId: 'support', order: 13 }, settings: { label: 'Settings', href: '/users/settings', icon: IconSettings, tourId: 'settings', order: 14 }, // Existing Solid module keys (backward compatibility) jobs: { label: 'Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' }, my_applications: { label: 'My Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' }, browse_jobs: { label: 'Browse Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' }, requirements: { label: 'Requirements', href: '/dashboard/requirements', icon: IconJobs, tourId: 'requirements' }, marketplace: { label: 'Marketplace', href: '/dashboard/marketplace', icon: IconCompass, tourId: 'marketplace' }, services: { label: 'Services', href: '/dashboard/services', icon: IconServices, tourId: 'services' }, wallet: { label: 'Wallet', href: '/dashboard/wallet', icon: IconWallet, tourId: 'wallet' }, // Uppercase fallbacks COMPANY_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' }, JOBSEEKER_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' }, CUSTOMER_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' }, PROFESSIONAL_DASHBOARD: { label: 'Dashboard', href: '/dashboard', icon: IconDashboard, tourId: 'dashboard' }, JOBS: { label: 'Jobs', href: '/dashboard/jobs', icon: IconJobs, tourId: 'jobs' }, APPLICATIONS: { label: 'Applications', href: '/dashboard/applications', icon: IconJobs, tourId: 'applications' }, REQUIREMENTS: { label: 'Requirements', href: '/dashboard/requirements', icon: IconJobs, tourId: 'requirements' }, MARKETPLACE: { label: 'Marketplace', href: '/dashboard/marketplace', icon: IconCompass, tourId: 'marketplace' }, MY_REQUESTS: { label: 'Leads', href: '/dashboard/requests', icon: IconJobs, tourId: 'requests' }, ACCEPTED_LEADS: { label: 'Accepted Leads', href: '/dashboard/leads/accepted',icon: IconJobs, tourId: 'leads' }, PORTFOLIO: { label: 'Portfolio', href: '/dashboard/portfolio', icon: IconPortfolio, tourId: 'portfolio' }, SERVICES: { label: 'Services', href: '/dashboard/services', icon: IconServices, tourId: 'services' }, WALLET: { label: 'Wallet', href: '/dashboard/wallet', icon: IconWallet, tourId: 'wallet' }, NOTIFICATIONS: { label: 'Notifications', href: '/dashboard/notifications', icon: IconBell, tourId: 'notifications' }, SETTINGS: { label: 'Settings', href: '/dashboard/settings', icon: IconSettings, tourId: 'settings' }, EXPLORE_NXTGAUGE: { label: 'Explore', href: '/dashboard/explore', icon: IconCompass, tourId: 'explore' }, }; // ── Spotlight Tour Overlay ──────────────────────────────────────────────────── function SpotlightOverlay(props: { step: GuidedTourStep; stepIndex: number; total: number; onNext: () => void; onBack: () => void; onSkip: () => void; }) { const PAD = 10; const TW = 300; // tooltip width const TH = 230; // tooltip approx height type Rect = { x: number; y: number; w: number; h: number }; const [rect, setRect] = createSignal(null); // Re-measure spotlight target whenever the step changes createEffect(() => { const target = props.step.target; void props.stepIndex; // track index changes too requestAnimationFrame(() => { if (!target) { setRect(null); return; } const el = document.querySelector(target); if (!el) { setRect(null); return; } const r = el.getBoundingClientRect(); setRect({ x: Math.max(0, r.left - PAD), y: Math.max(0, r.top - PAD), w: r.width + PAD * 2, h: r.height + PAD * 2, }); }); }); const tooltipStyle = () => { const r = rect(); const vw = window.innerWidth; const vh = window.innerHeight; if (!r) return { top: `${(vh - TH) / 2}px`, left: `${(vw - TW) / 2}px` }; const clampY = (y: number) => Math.max(12, Math.min(y, vh - TH - 12)); const clampX = (x: number) => Math.max(12, Math.min(x, vw - TW - 12)); // Right if (r.x + r.w + TW + 20 <= vw) return { top: `${clampY(r.y + r.h / 2 - TH / 2)}px`, left: `${r.x + r.w + 20}px` }; // Below if (r.y + r.h + TH + 20 <= vh) return { top: `${r.y + r.h + 20}px`, left: `${clampX(r.x + r.w / 2 - TW / 2)}px` }; // Above if (r.y - TH - 20 >= 0) return { top: `${r.y - TH - 20}px`, left: `${clampX(r.x + r.w / 2 - TW / 2)}px` }; // Left return { top: `${clampY(r.y + r.h / 2 - TH / 2)}px`, left: `${clampX(r.x - TW - 20)}px` }; }; const progress = () => Math.round(((props.stepIndex + 1) / props.total) * 100); return ( <> {/* ── Dark overlay with spotlight cutout ── */} {/* Orange border around spotlight */} {/* ── Tooltip card ── */} {/* Header */} Guided Tour {props.stepIndex + 1} / {props.total} {/* Title */} {props.step.title} {/* Body */} {props.step.body} {/* Progress bar */} {/* Buttons */} Skip 0}> ← Back {props.stepIndex >= props.total - 1 ? '✓ Done' : 'Next →'} > ); } // ── Dashboard Layout ────────────────────────────────────────────────────────── export default function DashboardLayout(props: { children: any }) { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [switchingRole, setSwitchingRole] = createSignal(false); const [tourKind, setTourKind] = createSignal(null); const [tourStepIndex, setTourStepIndex] = createSignal(0); const [authReady, setAuthReady] = createSignal(false); // Restore session or inject mock preview config onMount(async () => { const preview = String(searchParams._preview || '').toUpperCase(); if (preview) { setMockRuntimeConfig(preview); } else { await bootstrapAuth(); } setAuthReady(true); }); createEffect(() => { if (!authReady()) return; const s = authState(); if (s.runtime_config?.onboarding_required) { navigate('/onboarding', { replace: true }); } }); const rc = () => authState().runtime_config; const roleOptions = () => rc()?.user?.roles ?? []; const activeRole = () => rc()?.user?.active_role ?? rc()?.role ?? ''; const activeTourSteps = (): GuidedTourStep[] => { const kind = tourKind(); if (!kind) return []; if (kind === 'welcome') return resolveWelcomeTourSteps(rc()?.guided_tours ?? null); return resolveRoleApprovedTourSteps(activeRole(), rc()?.guided_tours ?? null); }; const tourStep = () => activeTourSteps()[tourStepIndex()]; const navItems = () => { const role = String(activeRole() || rc()?.role || '').toUpperCase(); const moduleSet = new Set( (Array.isArray(rc()?.enabled_modules) ? rc()!.enabled_modules : []) .map((moduleKey) => String(moduleKey || '').toLowerCase()), ); if (role !== 'USER') { moduleSet.add('dashboard'); } const modules = Array.from(moduleSet); return modules .map((m) => { const base = MODULE_NAV_MAP[m]; if (!base) return null; // Match Next.js role override: customer "leads" is "Post Requirement" if (m === 'leads' && role === 'CUSTOMER') { return { ...base, label: 'Post Requirement', href: '/users/requirements/new', tourId: 'requirements' }; } // Match Next.js company route overrides. if (role === 'COMPANY') { if (m === 'profile') return { ...base, href: '/companies/profile' }; if (m === 'support') return { ...base, href: '/companies/support' }; if (m === 'settings') return { ...base, href: '/companies/settings' }; } return base; }) .filter((item): item is NonNullable => Boolean(item)) .sort((left, right) => (left.order ?? 999) - (right.order ?? 999)); }; async function handleLogout() { await logout(); navigate('/auth/login', { replace: true }); } async function handleRoleSwitch(nextRole: string) { const normalized = String(nextRole || '').trim().toUpperCase(); if (!normalized || normalized === activeRole()) return; setSwitchingRole(true); try { await switchRole(normalized); navigate('/dashboard', { replace: true }); } finally { setSwitchingRole(false); } } function finishTour() { if (typeof window === 'undefined') return; const userId = rc()?.user?.id; if (!userId || !tourKind()) { setTourKind(null); setTourStepIndex(0); return; } if (tourKind() === 'welcome') { window.localStorage.setItem(getWelcomeTourStorageKey(userId), WELCOME_TOUR_VALUE); } else { const key = getRoleTourStorageKey(userId); const seen = readSeenRoleTours(window.localStorage.getItem(key)); seen.add(activeRole()); window.localStorage.setItem(key, writeSeenRoleTours(seen)); } setTourKind(null); setTourStepIndex(0); } function nextTourStep() { if (tourStepIndex() >= activeTourSteps().length - 1) { finishTour(); return; } setTourStepIndex(i => i + 1); } // Auto-start guided tour when runtime config loads createEffect(() => { if (typeof window === 'undefined') return; const runtime = rc(); const userId = runtime?.user?.id; if (!runtime || !userId) return; const welcomeSeen = window.localStorage.getItem(getWelcomeTourStorageKey(userId)) === WELCOME_TOUR_VALUE; const seenRoles = readSeenRoleTours(window.localStorage.getItem(getRoleTourStorageKey(userId))); const next = pickGuidedTour({ userId, activeRole: runtime?.user?.active_role ?? runtime?.role, welcomeTourSeen: welcomeSeen, seenRoleTours: seenRoles, }); if (next && next !== tourKind()) { setTourKind(next); setTourStepIndex(0); } }); return ( {/* ── Sidebar ── */} {/* ── Main Content ── */} handleRoleSwitch(e.currentTarget.value)} title="Switch your dashboard view" > {(role) => {role.replaceAll('_', ' ')}} Choose What You Need {rc()?.user?.full_name ?? 'User'} Loading...}> {props.children} {/* ── Spotlight Guided Tour ── */} setTourStepIndex(i => Math.max(0, i - 1))} onSkip={finishTour} /> ); }
{props.step.body}