- DashboardShell: sticky sidebar + header wrapper with shared style tokens - ProfilePage: 3-tab form (Basic, Documents, Settings) per role, save/submit-for-verification - PortfolioPage: full CRUD wired to /api/:prefix/portfolio/me endpoints - VerificationStatusPage: 7-state status display with progress timeline and resubmit flow - dashboard.tsx: REAL_PAGES routing intercepts these three sidebar items and renders real components instead of DashboardDesignPreview mock Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
361 lines
14 KiB
TypeScript
361 lines
14 KiB
TypeScript
import { Match, Show, Switch, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
|
|
import { useNavigate } from '@solidjs/router';
|
|
import { useAuth, RequireAuth } from '~/lib/auth';
|
|
import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
|
|
import DashboardShell from '~/components/DashboardShell';
|
|
import ProfilePage from '~/components/dashboard/ProfilePage';
|
|
import PortfolioPage from '~/components/dashboard/PortfolioPage';
|
|
import VerificationStatusPage from '~/components/dashboard/VerificationStatusPage';
|
|
|
|
// Sidebar items that have real implementations (not the preview mock)
|
|
const REAL_PAGES = ['my profile', 'my portfolio', 'verification'];
|
|
|
|
type RoleKey =
|
|
| 'COMPANY'
|
|
| 'CUSTOMER'
|
|
| 'JOB_SEEKER'
|
|
| 'PHOTOGRAPHER'
|
|
| 'MAKEUP_ARTIST'
|
|
| 'TUTOR'
|
|
| 'DEVELOPER'
|
|
| 'VIDEO_EDITOR'
|
|
| 'UGC_CONTENT_CREATOR'
|
|
| 'GRAPHIC_DESIGNER'
|
|
| 'SOCIAL_MEDIA_MANAGER'
|
|
| 'FITNESS_TRAINER'
|
|
| 'CATERING_SERVICES';
|
|
|
|
type RuntimeBundle = {
|
|
role: RoleKey;
|
|
status: 'ACTIVE' | 'INACTIVE';
|
|
sidebarItems: string[];
|
|
tabs: string[];
|
|
widgets: string[];
|
|
fields: string[];
|
|
source: 'dashboard-config';
|
|
};
|
|
|
|
const API_GATEWAY = '/api/gateway';
|
|
const SERVER_API_BASE = (process.env.PUBLIC_API_URL || 'http://localhost:8080/api').replace(/\/+$/, '');
|
|
|
|
const ROLE_OPTIONS: RoleKey[] = [
|
|
'COMPANY',
|
|
'CUSTOMER',
|
|
'JOB_SEEKER',
|
|
'PHOTOGRAPHER',
|
|
'MAKEUP_ARTIST',
|
|
'TUTOR',
|
|
'DEVELOPER',
|
|
'VIDEO_EDITOR',
|
|
'UGC_CONTENT_CREATOR',
|
|
'GRAPHIC_DESIGNER',
|
|
'SOCIAL_MEDIA_MANAGER',
|
|
'FITNESS_TRAINER',
|
|
'CATERING_SERVICES',
|
|
];
|
|
|
|
const ROLE_BASED_SIDEBAR: Record<RoleKey, string[]> = {
|
|
PHOTOGRAPHER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
MAKEUP_ARTIST: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
TUTOR: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
DEVELOPER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
VIDEO_EDITOR: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
UGC_CONTENT_CREATOR: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
GRAPHIC_DESIGNER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
SOCIAL_MEDIA_MANAGER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
FITNESS_TRAINER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
CATERING_SERVICES: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
COMPANY: ['My Dashboard', 'My Profile', 'Jobs', 'Applications', 'Shortlisted Candidates', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
JOB_SEEKER: ['My Dashboard', 'My Profile', 'Jobs', 'My Applications', 'Saved Jobs', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
CUSTOMER: ['My Dashboard', 'My Profile', 'My Requirements', 'Received Responses', 'Shortlisted Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
|
|
};
|
|
|
|
const ROLE_PREFIXES: Record<string, string> = {
|
|
PHOTOGRAPHER: 'photographers',
|
|
MAKEUP_ARTIST: 'makeup-artists',
|
|
TUTOR: 'tutors',
|
|
DEVELOPER: 'developers',
|
|
VIDEO_EDITOR: 'video-editors',
|
|
GRAPHIC_DESIGNER: 'graphic-designers',
|
|
SOCIAL_MEDIA_MANAGER: 'social-media-managers',
|
|
FITNESS_TRAINER: 'fitness-trainers',
|
|
CATERING_SERVICES: 'catering-services',
|
|
UGC_CONTENT_CREATOR: 'ugc-content-creators',
|
|
CUSTOMER: 'customers',
|
|
COMPANY: 'companies',
|
|
JOB_SEEKER: 'jobseeker',
|
|
};
|
|
|
|
function getNameFromStorage(): string {
|
|
if (typeof window === 'undefined') return 'User';
|
|
const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user'];
|
|
for (const key of keys) {
|
|
try {
|
|
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
|
|
if (!raw) continue;
|
|
const parsed = JSON.parse(raw);
|
|
const name = parsed?.name
|
|
|| parsed?.first_name
|
|
|| parsed?.user?.name
|
|
|| parsed?.user?.first_name;
|
|
if (name) return String(name);
|
|
} catch { /* ignore */ }
|
|
}
|
|
return 'User';
|
|
}
|
|
|
|
const EXPLORE_ROLES = [
|
|
{ key: 'PHOTOGRAPHER', name: 'Photographer' },
|
|
{ key: 'MAKEUP_ARTIST', name: 'Makeup Artist' },
|
|
{ key: 'TUTOR', name: 'Tutor' },
|
|
{ key: 'DEVELOPER', name: 'Developer' },
|
|
{ key: 'VIDEO_EDITOR', name: 'Video Editor' },
|
|
{ key: 'UGC_CONTENT_CREATOR', name: 'UGC Content Creator' },
|
|
{ key: 'GRAPHIC_DESIGNER', name: 'Graphic Designer' },
|
|
{ key: 'SOCIAL_MEDIA_MANAGER', name: 'Social Media Manager' },
|
|
{ key: 'FITNESS_TRAINER', name: 'Fitness Trainer' },
|
|
{ key: 'CATERING_SERVICES', name: 'Catering Services' },
|
|
];
|
|
|
|
function normalizeRole(value: string): RoleKey {
|
|
const up = String(value || '').trim().toUpperCase().replace(/\s+/g, '_');
|
|
return (ROLE_OPTIONS.find((r) => r === up) || 'JOB_SEEKER') as RoleKey;
|
|
}
|
|
|
|
function asStringArray(value: unknown): string[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value
|
|
.map((item) => String(item || '').trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function getInitialRoleFromStorage(): RoleKey {
|
|
if (typeof window === 'undefined') return 'JOB_SEEKER';
|
|
const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user'];
|
|
for (const key of keys) {
|
|
try {
|
|
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
|
|
if (!raw) continue;
|
|
const parsed = JSON.parse(raw);
|
|
const found = normalizeRole(
|
|
String(
|
|
parsed?.roleKey
|
|
|| parsed?.role
|
|
|| parsed?.active_role
|
|
|| parsed?.user?.active_role
|
|
|| parsed?.user?.roles?.[0]
|
|
|| '',
|
|
),
|
|
);
|
|
if (ROLE_OPTIONS.includes(found)) return found;
|
|
} catch {
|
|
// ignore invalid payload
|
|
}
|
|
}
|
|
return 'JOB_SEEKER';
|
|
}
|
|
|
|
async function fetchJson(path: string): Promise<any | null> {
|
|
try {
|
|
const isServer = typeof window === 'undefined';
|
|
const target = isServer
|
|
? `${SERVER_API_BASE}${path}`
|
|
: `${API_GATEWAY}${path}`;
|
|
const res = await fetch(target, { credentials: 'include' });
|
|
if (!res.ok) return null;
|
|
return await res.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
|
|
if (typeof window === 'undefined') {
|
|
return null;
|
|
}
|
|
let payload = await fetchJson(`/api/config/dashboard/by-key/${encodeURIComponent(role)}?audience=EXTERNAL`);
|
|
if (!payload) {
|
|
const listPayload = await fetchJson('/api/admin/dashboard-config?audience=EXTERNAL');
|
|
const rows = Array.isArray(listPayload) ? listPayload : (Array.isArray(listPayload?.items) ? listPayload.items : []);
|
|
const matched = rows.find((row: any) => String(row?.role_key || row?.config_json?.role_key || '').toUpperCase() === role);
|
|
if (matched) payload = matched;
|
|
}
|
|
const config = (payload?.config_json || payload || null) as Record<string, unknown> | null;
|
|
if (!config) return null;
|
|
const sidebarItems = asStringArray((config as any)?.sidebar_items ?? (config as any)?.sidebarItems);
|
|
const tabs = asStringArray((config as any)?.tabs);
|
|
const widgetsRaw = Array.isArray((config as any)?.widgets) ? (config as any).widgets : [];
|
|
const widgets = widgetsRaw
|
|
.map((item: any) => String(typeof item === 'string' ? item : (item?.key || item?.id || '')).trim())
|
|
.filter(Boolean);
|
|
const fields = asStringArray((config as any)?.fields);
|
|
return {
|
|
role,
|
|
status: payload?.is_active === false ? 'INACTIVE' : 'ACTIVE',
|
|
sidebarItems,
|
|
tabs,
|
|
widgets,
|
|
fields,
|
|
source: 'dashboard-config',
|
|
};
|
|
}
|
|
|
|
function mergeSidebar(role: RoleKey, runtimeSidebar: string[]): string[] {
|
|
const base = ROLE_BASED_SIDEBAR[role] || ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
|
const fromRuntime = runtimeSidebar.filter(Boolean);
|
|
const map = new Map<string, string>();
|
|
for (const item of [...fromRuntime, ...base]) {
|
|
const key = item.trim().toLowerCase();
|
|
if (!map.has(key)) map.set(key, item);
|
|
}
|
|
let merged = Array.from(map.values());
|
|
if (role === 'JOB_SEEKER') {
|
|
merged = merged.filter((item) => item.trim().toLowerCase() !== 'credits');
|
|
}
|
|
if (!merged.some((item) => item.trim().toLowerCase() === 'explore nxtgauge')) {
|
|
const insertBefore = merged.findIndex((item) => item.trim().toLowerCase() === 'verification');
|
|
if (insertBefore >= 0) merged.splice(insertBefore, 0, 'Explore Nxtgauge');
|
|
else merged.push('Explore Nxtgauge');
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
export default function RuntimeDashboardPage() {
|
|
const navigate = useNavigate();
|
|
const auth = useAuth();
|
|
const [hydrated, setHydrated] = createSignal(false);
|
|
const [role, setRole] = createSignal<RoleKey>('JOB_SEEKER');
|
|
const [activeSidebar, setActiveSidebar] = createSignal('My Dashboard');
|
|
const [activeTab, setActiveTab] = createSignal('overview');
|
|
const [userName, setUserName] = createSignal('User');
|
|
const [userId, setUserId] = createSignal('');
|
|
|
|
onMount(() => {
|
|
setHydrated(true);
|
|
const storedRole = getInitialRoleFromStorage();
|
|
setRole(storedRole);
|
|
setUserName(getNameFromStorage());
|
|
if (auth.user()) {
|
|
const u = auth.user()!;
|
|
if (u.full_name) setUserName(u.full_name);
|
|
if (u.id) setUserId(u.id);
|
|
if (u.active_role) setRole(normalizeRole(u.active_role));
|
|
}
|
|
});
|
|
|
|
createEffect(() => {
|
|
const u = auth.user();
|
|
if (u) {
|
|
if (u.full_name && userName() === 'User') setUserName(u.full_name);
|
|
if (u.id && !userId()) setUserId(u.id);
|
|
if (u.active_role) setRole(normalizeRole(u.active_role));
|
|
}
|
|
});
|
|
|
|
const [bundle] = createResource(
|
|
() => role(),
|
|
loadRoleBundle,
|
|
);
|
|
|
|
const sidebarItems = createMemo(() => mergeSidebar(role(), bundle()?.sidebarItems || []));
|
|
|
|
createEffect(() => {
|
|
const first = sidebarItems()[0] || 'My Dashboard';
|
|
const current = activeSidebar();
|
|
const exists = sidebarItems().some((item) => item.toLowerCase() === current.toLowerCase());
|
|
if (!exists) setActiveSidebar(first);
|
|
});
|
|
|
|
const tabs = createMemo(() => {
|
|
const fromRuntime = bundle()?.tabs || [];
|
|
return fromRuntime.length > 0 ? fromRuntime : ['overview'];
|
|
});
|
|
|
|
createEffect(() => {
|
|
role();
|
|
const firstTab = tabs()[0] || 'overview';
|
|
setActiveTab((prev) => (prev ? prev : firstTab));
|
|
});
|
|
|
|
const loading = createMemo(() => !hydrated() || bundle.loading);
|
|
const ready = createMemo(() => hydrated() && !bundle.loading);
|
|
|
|
const liveData = createMemo(() => {
|
|
const prefix = ROLE_PREFIXES[role()];
|
|
if (!prefix) return undefined;
|
|
return { userName: userName(), userId: userId(), rolePrefix: prefix };
|
|
});
|
|
|
|
const isRealPage = createMemo(() =>
|
|
REAL_PAGES.includes(activeSidebar().toLowerCase()),
|
|
);
|
|
|
|
return (
|
|
<RequireAuth>
|
|
<main style={{ 'min-height': '100vh', background: '#F3F4F6' }}>
|
|
|
|
<Show when={loading()}>
|
|
<div style={cardStyle}>Loading dashboard…</div>
|
|
</Show>
|
|
|
|
<Show when={ready()}>
|
|
|
|
{/* ── Real pages: DashboardShell + actual components ── */}
|
|
<Show when={isRealPage()}>
|
|
<DashboardShell
|
|
sidebarItems={sidebarItems()}
|
|
activeSidebar={activeSidebar()}
|
|
onSidebarSelect={setActiveSidebar}
|
|
roleKey={role()}
|
|
userName={userName()}
|
|
>
|
|
<Switch>
|
|
<Match when={activeSidebar().toLowerCase() === 'my profile'}>
|
|
<ProfilePage roleKey={role()} />
|
|
</Match>
|
|
<Match when={activeSidebar().toLowerCase() === 'my portfolio'}>
|
|
<PortfolioPage roleKey={role()} />
|
|
</Match>
|
|
<Match when={activeSidebar().toLowerCase() === 'verification'}>
|
|
<VerificationStatusPage
|
|
roleKey={role()}
|
|
onNavigate={setActiveSidebar}
|
|
/>
|
|
</Match>
|
|
</Switch>
|
|
</DashboardShell>
|
|
</Show>
|
|
|
|
{/* ── All other views: DashboardDesignPreview mock ── */}
|
|
<Show when={!isRealPage()}>
|
|
<DashboardDesignPreview
|
|
status={bundle()?.status ?? 'ACTIVE'}
|
|
sidebarItems={sidebarItems()}
|
|
activeSidebar={activeSidebar()}
|
|
onSidebarSelect={setActiveSidebar}
|
|
tabs={tabs()}
|
|
activeTab={activeTab()}
|
|
onTabSelect={setActiveTab}
|
|
widgets={bundle()?.widgets || []}
|
|
fields={bundle()?.fields || []}
|
|
mode="customer_external"
|
|
roleKey={role()}
|
|
exploreRoles={EXPLORE_ROLES}
|
|
hidePreviewHeader
|
|
liveData={liveData()}
|
|
/>
|
|
</Show>
|
|
|
|
</Show>
|
|
</main>
|
|
</RequireAuth>
|
|
);
|
|
}
|
|
|
|
const cardStyle = {
|
|
border: '1px solid #E5E7EB',
|
|
'border-radius': '12px',
|
|
padding: '16px',
|
|
background: '#fff',
|
|
color: '#111827',
|
|
} as const;
|