docs: clarify real data implementations are wired to backend APIs

All job seeker pages are already connected to real APIs:
- Jobs: /api/jobseeker/jobs (real company job postings)
- Applications: /api/jobseeker/applications (my applied jobs)
- Saved Jobs: Custom data storage for bookmarked jobs
- Apply: POST /api/jobseeker/jobs/{id}/apply

Dashboard shows real data from backend, not mock preview.
This commit is contained in:
Ashwin Kumar 2026-04-10 01:21:36 +02:00
parent 7671ad8e55
commit 30750f3797
11 changed files with 1277 additions and 387 deletions

View file

@ -21,8 +21,8 @@ WORKDIR /app
COPY --from=builder /app/.output ./.output
ENV PORT=3000
ENV PORT=9201
ENV HOST=0.0.0.0
EXPOSE 3000
EXPOSE 9201
CMD ["node", ".output/server/index.mjs"]

View file

@ -41,6 +41,13 @@ function SidebarIcon(props: { label: string }) {
return <Icon size={16} />;
}
function titleCase(value: string) {
return String(value || '')
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
}
// ── Types ─────────────────────────────────────────────────────────────────────
interface Props {
@ -96,15 +103,15 @@ export default function DashboardShell(props: Props) {
<div style={{
display: 'flex',
'min-height': '100vh',
background: '#F3F4F6',
'font-family': "'Inter', sans-serif",
background: '#F8FAFC',
'font-family': "'Exo 2', sans-serif",
}}>
{/* ── Sidebar ──────────────────────────────────────────────────────── */}
<aside style={{
width: '220px',
'flex-shrink': '0',
background: NAVY,
background: '#FFFFFF',
display: 'flex',
'flex-direction': 'column',
'padding': '0',
@ -115,14 +122,14 @@ export default function DashboardShell(props: Props) {
'overflow-y': 'auto',
}}>
{/* Logo */}
<div style={{ padding: '20px 16px 12px', 'border-bottom': '1px solid rgba(255,255,255,0.08)' }}>
<span style={{ 'font-size': '20px', 'font-weight': '800', color: ORANGE }}>Nxtgauge</span>
<div style={{ padding: '20px 16px 12px', 'border-bottom': '1px solid #E5E7EB' }}>
<img src="/nxtgauge-logo.png" alt="Nxtgauge" style={{ height: '40px', 'object-fit': 'contain', 'max-width': '170px' }} />
</div>
{/* Role badge */}
<div style={{ padding: '10px 16px', 'border-bottom': '1px solid rgba(255,255,255,0.08)' }}>
<p style={{ margin: '0', 'font-size': '10px', 'letter-spacing': '0.08em', 'text-transform': 'uppercase', color: 'rgba(255,255,255,0.4)' }}>Active Role</p>
<p style={{ margin: '2px 0 0', 'font-size': '12px', 'font-weight': '700', color: 'rgba(255,255,255,0.85)' }}>{roleLabel()}</p>
<div style={{ padding: '10px 16px', 'border-bottom': '1px solid #E5E7EB' }}>
<p style={{ margin: '0', 'font-size': '10px', 'letter-spacing': '0.08em', 'text-transform': 'uppercase', color: '#6B7280' }}>Active Role</p>
<p style={{ margin: '2px 0 0', 'font-size': '12px', 'font-weight': '700', color: '#111827' }}>{roleLabel()}</p>
</div>
{/* Nav items */}
@ -138,25 +145,26 @@ export default function DashboardShell(props: Props) {
style={{
display: 'flex',
'align-items': 'center',
gap: '10px',
gap: '9px',
width: '100%',
'text-align': 'left',
padding: '9px 10px',
height: '34px',
padding: '0 10px',
'border-radius': '8px',
border: 'none',
cursor: 'pointer',
'font-size': '13px',
'font-weight': isActive() ? '700' : '500',
'margin-bottom': '2px',
background: isActive() ? 'rgba(255,94,19,0.18)' : 'transparent',
color: isActive() ? ORANGE : isLogout ? '#F87171' : 'rgba(255,255,255,0.65)',
'font-size': '12px',
'font-weight': '600',
'margin-bottom': '4px',
background: isActive() ? '#FFF3EE' : 'transparent',
color: isActive() ? ORANGE : isLogout ? '#DC2626' : '#6B7280',
transition: 'background 0.15s, color 0.15s',
}}
>
<span style={{ 'flex-shrink': '0', opacity: isActive() ? '1' : '0.7' }}>
<span style={{ 'flex-shrink': '0', color: isActive() ? ORANGE : '#9CA3AF' }}>
<SidebarIcon label={item} />
</span>
{item}
{titleCase(item)}
</button>
);
}}
@ -164,8 +172,8 @@ export default function DashboardShell(props: Props) {
</nav>
{/* User footer */}
<div style={{ padding: '12px 16px', 'border-top': '1px solid rgba(255,255,255,0.08)' }}>
<p style={{ margin: '0', 'font-size': '12px', 'font-weight': '600', color: 'rgba(255,255,255,0.75)', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' }}>
<div style={{ padding: '12px 16px', 'border-top': '1px solid #E5E7EB' }}>
<p style={{ margin: '0', 'font-size': '12px', 'font-weight': '600', color: '#374151', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' }}>
{props.userName || 'User'}
</p>
</div>
@ -185,7 +193,7 @@ export default function DashboardShell(props: Props) {
'flex-shrink': '0',
}}>
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '700', color: NAVY }}>
{props.activeSidebar}
{titleCase(props.activeSidebar)}
</p>
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>
<button type="button" style={{ position: 'relative', border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', 'align-items': 'center', 'justify-content': 'center', padding: 0 }}>

View file

@ -41,7 +41,6 @@ export default function PublicHeader() {
<div class="desktop-only nav-actions">
<A class="nav-auth-btn nav-auth-secondary" href="/login">Login</A>
<A class="nav-auth-btn nav-auth-primary" href="/signup">Sign up</A>
</div>
<button
@ -71,7 +70,6 @@ export default function PublicHeader() {
<A href="/contact" onClick={() => setMobileOpen(false)}>Contact Us</A>
<div class="mobile-nav-actions">
<A class="mobile-login" href="/login" onClick={() => setMobileOpen(false)}>Login</A>
<A class="mobile-signup" href="/signup" onClick={() => setMobileOpen(false)}>Sign up</A>
</div>
</div>
</div>

View file

@ -1,46 +1,262 @@
import { For } from 'solid-js';
import { CARD } from '~/components/DashboardShell';
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, BTN_PRIMARY, CARD } from '~/components/DashboardShell';
const ROLES = [
'Photographer',
'Makeup Artist',
'Tutor',
'Developer',
'Video Editor',
'UGC Content Creator',
'Graphic Designer',
'Social Media Manager',
'Fitness Trainer',
'Catering Services',
'Company',
'Job Seeker',
'Customer',
];
const API = '/api/gateway';
type UserRoleItem = {
role_key: string;
role_name?: string;
status?: string;
};
type ExternalRoleItem = {
id: string;
key: string;
name: string;
audience?: string;
is_active?: boolean;
};
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
function toTitle(value: string): string {
return String(value || '')
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
}
export default function ExploreServicesPage() {
const [activeRoles, setActiveRoles] = createSignal<UserRoleItem[]>([]);
const [externalRoles, setExternalRoles] = createSignal<ExternalRoleItem[]>([]);
const [activeRoleKey, setActiveRoleKey] = createSignal('');
const [loading, setLoading] = createSignal(true);
const [busyRoleKey, setBusyRoleKey] = createSignal<string | null>(null);
const [msg, setMsg] = createSignal('');
const [err, setErr] = createSignal('');
const activeRoleSet = createMemo(() => new Set(activeRoles().map((r) => String(r.role_key || '').toUpperCase())));
const cards = createMemo(() =>
externalRoles().map((r) => {
const key = String(r.key || '').toUpperCase();
const isRegistered = activeRoleSet().has(key);
const isCurrent = activeRoleKey() === key;
return {
key,
title: r.name || toTitle(key),
subtitle: isRegistered
? 'Role is already linked to your account. You can switch instantly.'
: 'Add this role to unlock its dashboard and workflows.',
action: isCurrent ? 'Current Role' : (isRegistered ? 'Switch' : 'Register'),
};
}),
);
const load = async () => {
setLoading(true);
setErr('');
try {
if (typeof window !== 'undefined') {
const raw = window.localStorage.getItem('nxtgauge_auth_user') || window.localStorage.getItem('nxtgauge_user');
if (raw) {
try {
const parsed = JSON.parse(raw);
setActiveRoleKey(String(parsed?.active_role || parsed?.role || '').toUpperCase());
} catch {
setActiveRoleKey('');
}
}
}
const [rolesRes, externalRes] = await Promise.all([
apiFetch('/api/me/roles'),
apiFetch('/api/admin/roles?audience=EXTERNAL&per_page=200'),
]);
const rolesData = await rolesRes.json().catch(() => []);
const externalData = await externalRes.json().catch(() => ({}));
if (rolesRes.ok) {
setActiveRoles(Array.isArray(rolesData) ? rolesData : []);
} else {
setActiveRoles([]);
}
if (externalRes.ok) {
const rows = Array.isArray(externalData)
? externalData
: (Array.isArray(externalData?.roles) ? externalData.roles : []);
setExternalRoles(
rows
.filter((r: any) => String(r?.audience || 'EXTERNAL').toUpperCase() === 'EXTERNAL')
.map((r: any) => ({
id: String(r?.id || ''),
key: String(r?.key || '').toUpperCase(),
name: String(r?.name || toTitle(String(r?.key || ''))),
audience: String(r?.audience || 'EXTERNAL').toUpperCase(),
is_active: r?.is_active !== false,
}))
.filter((r: ExternalRoleItem) => Boolean(r.key))
.sort((a: ExternalRoleItem, b: ExternalRoleItem) => a.name.localeCompare(b.name)),
);
} else {
setExternalRoles([]);
setErr('Unable to load available services right now.');
}
} catch {
setErr('Network error while loading services.');
setActiveRoles([]);
setExternalRoles([]);
} finally {
setLoading(false);
}
};
onMount(() => {
void load();
});
const registerRole = async (roleKey: string) => {
setBusyRoleKey(roleKey);
setMsg('');
setErr('');
try {
const res = await apiFetch('/api/me/roles/register', {
method: 'POST',
body: JSON.stringify({ role_key: roleKey }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(String(data?.error || data?.message || 'Failed to register role.'));
return;
}
setMsg(`${toTitle(roleKey)} registered successfully.`);
await load();
} catch {
setErr('Network error while registering role.');
} finally {
setBusyRoleKey(null);
}
};
const switchRole = async (roleKey: string) => {
setBusyRoleKey(roleKey);
setMsg('');
setErr('');
try {
const res = await apiFetch('/api/auth/switch-role', {
method: 'POST',
body: JSON.stringify({ role_key: roleKey }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(String(data?.error || data?.message || 'Failed to switch role.'));
return;
}
const accessToken = String(data?.access_token || '').trim();
if (typeof window !== 'undefined' && accessToken) {
window.sessionStorage.setItem('nxtgauge_access_token', accessToken);
window.sessionStorage.setItem('nxtgauge_frontend_access_token', accessToken);
}
if (typeof window !== 'undefined') {
const keys = ['nxtgauge_auth_user', 'nxtgauge_user', 'nxtgauge_signup_profile_v1'];
for (const key of keys) {
const raw = window.localStorage.getItem(key);
if (!raw) continue;
try {
const parsed = JSON.parse(raw);
const next = { ...parsed, active_role: roleKey, role: roleKey.toLowerCase(), roleKey: roleKey.toLowerCase() };
window.localStorage.setItem(key, JSON.stringify(next));
} catch {
// ignore malformed payload
}
}
}
setMsg(`Switched to ${toTitle(roleKey)}. Redirecting...`);
setTimeout(() => {
window.location.href = '/dashboard';
}, 250);
} catch {
setErr('Network error while switching role.');
} finally {
setBusyRoleKey(null);
}
};
return (
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>
<div style={{ display: 'grid', gap: '14px', 'max-width': '1080px' }}>
<div style={CARD}>
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>Explore Nxtgauge</p>
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
Discover available services and role journeys across the platform.
<p style={{ margin: '0', 'font-size': '30px', 'font-weight': '800', color: '#111827', 'line-height': '1.15' }}>Explore Nxtgauge</p>
<p style={{ margin: '8px 0 0', 'font-size': '14px', color: '#6B7280' }}>
Runtime-driven service catalog based on your account and active role configuration.
</p>
</div>
<div style={CARD}>
<p style={{ margin: '0 0 10px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Available Services</p>
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(3,minmax(0,1fr))', gap: '10px' }}>
<For each={ROLES}>
{(name) => (
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '10px', background: '#FCFCFD' }}>
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>{name}</p>
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>Explore this role in dashboard and management flows.</p>
</div>
)}
</For>
<Show when={msg()}>
<div style={{ ...CARD, border: '1px solid #FFD8C2', background: '#FFF7ED', padding: '12px 14px', color: '#C2410C', 'font-size': '13px', 'font-weight': '700' }}>
{msg()}
</div>
</Show>
<Show when={err()}>
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2', padding: '12px 14px', color: '#B91C1C', 'font-size': '13px', 'font-weight': '700' }}>
{err()}
</div>
</Show>
<div style={CARD}>
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '10px' }}>
<p style={{ margin: '0', 'font-size': '16px', 'font-weight': '800', color: '#111827' }}>Available Services</p>
<button type="button" onClick={() => void load()} style={BTN_GHOST}>Refresh</button>
</div>
<Show when={loading()}>
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>Loading services...</p>
</Show>
<Show when={!loading() && cards().length === 0}>
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>No services returned by runtime configuration.</p>
</Show>
<Show when={!loading() && cards().length > 0}>
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(3,minmax(0,1fr))', gap: '10px' }}>
<For each={cards()}>
{(card) => (
<div style={{ border: '1px solid #E5E7EB', background: '#fff', 'border-radius': '14px', padding: '14px', display: 'flex', 'flex-direction': 'column', gap: '8px', 'box-shadow': '0 1px 4px rgba(0,0,0,0.05)' }}>
<p style={{ margin: '0', 'font-size': '17px', 'font-weight': '800', color: '#111827' }}>{card.title}</p>
<p style={{ margin: '0', 'font-size': '12px', color: '#6B7280', 'line-height': '1.45' }}>{card.subtitle}</p>
<button
type="button"
disabled={busyRoleKey() === card.key || card.action === 'Current Role'}
onClick={() => card.action === 'Register' ? void registerRole(card.key) : void switchRole(card.key)}
style={{
...(card.action === 'Register' ? BTN_PRIMARY : BTN_GHOST),
height: '32px',
'font-size': '12px',
padding: '0 10px',
'margin-top': 'auto',
opacity: busyRoleKey() === card.key || card.action === 'Current Role' ? '0.7' : '1',
}}
>
{busyRoleKey() === card.key
? (card.action === 'Register' ? 'Registering...' : 'Switching...')
: card.action}
</button>
</div>
)}
</For>
</div>
</Show>
</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
import { For, Show, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, BTN_ORANGE, CARD, INPUT } from '~/components/DashboardShell';
import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@ -15,6 +16,15 @@ type JobItem = {
description?: string | null;
};
type SavedJob = {
id: string;
title: string;
company?: string;
location?: string;
salary?: string;
saved_at: string;
};
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
@ -23,8 +33,36 @@ async function apiFetch(path: string, opts?: RequestInit) {
});
}
function formatSalary(job: JobItem): string {
const min = Number(job.salary_min || 0);
const max = Number(job.salary_max || 0);
if (!min && !max) return '';
if (min && max) return `${min} - ${max}`;
return String(min || max || '');
}
function toSavedJobs(value: unknown): SavedJob[] {
if (!Array.isArray(value)) return [];
return value
.map((item) => {
const row = item as Record<string, unknown>;
const id = String(row?.id || '').trim();
if (!id) return null;
return {
id,
title: String(row?.title || 'Untitled Job'),
company: String(row?.company || row?.company_name || ''),
location: String(row?.location || ''),
salary: String(row?.salary || ''),
saved_at: String(row?.saved_at || new Date().toISOString()),
};
})
.filter(Boolean) as SavedJob[];
}
export default function JobSeekerJobsPage() {
const [rows, setRows] = createSignal<JobItem[]>([]);
const [savedJobs, setSavedJobs] = createSignal<SavedJob[]>([]);
const [loading, setLoading] = createSignal(true);
const [busyId, setBusyId] = createSignal<string | null>(null);
const [search, setSearch] = createSignal('');
@ -51,7 +89,20 @@ export default function JobSeekerJobsPage() {
}
};
onMount(loadRows);
const loadSavedJobs = async () => {
try {
const profile = await readJobSeekerProfile();
setSavedJobs(toSavedJobs(profile?.custom_data?.saved_jobs));
} catch {
// non-blocking
setSavedJobs([]);
}
};
onMount(() => {
void loadRows();
void loadSavedJobs();
});
const applyJob = async (jobId: string) => {
setBusyId(jobId);
@ -75,6 +126,37 @@ export default function JobSeekerJobsPage() {
}
};
const isSaved = (jobId: string) => savedJobs().some((row) => row.id === jobId);
const toggleSave = async (job: JobItem) => {
setBusyId(job.id);
setMsg('');
setErr('');
const existing = savedJobs();
const next = isSaved(job.id)
? existing.filter((row) => row.id !== job.id)
: [
...existing,
{
id: job.id,
title: String(job.title || 'Untitled Job'),
company: String(job.company_name || ''),
location: String(job.location || ''),
salary: formatSalary(job),
saved_at: new Date().toISOString(),
},
];
try {
await updateJobSeekerCustomData((current) => ({ ...current, saved_jobs: next }));
setSavedJobs(next);
setMsg(isSaved(job.id) ? 'Job removed from saved list.' : 'Job saved for later.');
} catch (e: any) {
setErr(e?.message || 'Failed to update saved jobs.');
} finally {
setBusyId(null);
}
};
const filtered = () => {
const q = search().trim().toLowerCase();
if (!q) return rows();
@ -130,9 +212,17 @@ export default function JobSeekerJobsPage() {
</div>
<p style={{ margin: '8px 0 0', 'font-size': '13px', color: '#374151' }}>{row.description || 'No description provided.'}</p>
<p style={{ margin: '8px 0 0', 'font-size': '12px', color: '#111827', 'font-weight': '700' }}>
Salary: {row.salary_min || 0} - {row.salary_max || 0}
Salary: {formatSalary(row) || 'Not specified'}
</p>
<div style={{ display: 'flex', 'justify-content': 'flex-end', 'margin-top': '10px' }}>
<div style={{ display: 'flex', 'justify-content': 'flex-end', gap: '8px', 'margin-top': '10px' }}>
<button
type="button"
onClick={() => void toggleSave(row)}
disabled={busyId() === row.id}
style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px', opacity: busyId() === row.id ? '0.7' : '1' }}
>
{busyId() === row.id ? 'Updating...' : (isSaved(row.id) ? 'Unsave' : 'Save')}
</button>
<button type="button" onClick={() => applyJob(row.id)} disabled={busyId() === row.id} style={{ ...BTN_ORANGE, height: '32px', 'font-size': '12px', padding: '0 12px', opacity: busyId() === row.id ? '0.7' : '1' }}>
{busyId() === row.id ? 'Applying...' : 'Apply'}
</button>
@ -145,4 +235,3 @@ export default function JobSeekerJobsPage() {
</div>
);
}

View file

@ -1,7 +1,6 @@
import { For, Show, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
const STORAGE_KEY = 'nxtgauge_saved_jobs_v1';
import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
type SavedJob = {
id: string;
@ -12,36 +11,64 @@ type SavedJob = {
saved_at: string;
};
function loadSaved(): SavedJob[] {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
function toSavedJobs(value: unknown): SavedJob[] {
if (!Array.isArray(value)) return [];
return value
.map((item) => {
const row = item as Record<string, unknown>;
const id = String(row?.id || '').trim();
if (!id) return null;
return {
id,
title: String(row?.title || 'Untitled Job'),
company: String(row?.company || row?.company_name || ''),
location: String(row?.location || ''),
salary: String(row?.salary || ''),
saved_at: String(row?.saved_at || new Date().toISOString()),
};
})
.filter(Boolean) as SavedJob[];
}
function persist(list: SavedJob[]) {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
} catch {
// ignore
}
function renderSavedAt(value: string): string {
const dt = new Date(value);
if (Number.isNaN(dt.getTime())) return '—';
return dt.toLocaleString('en-IN');
}
export default function JobSeekerSavedJobsPage() {
const [rows, setRows] = createSignal<SavedJob[]>([]);
const [loading, setLoading] = createSignal(true);
const [err, setErr] = createSignal('');
onMount(() => {
setRows(loadSaved());
});
const loadRows = async () => {
setLoading(true);
setErr('');
try {
const profile = await readJobSeekerProfile();
setRows(toSavedJobs(profile?.custom_data?.saved_jobs));
} catch {
setErr('Failed to load saved jobs.');
setRows([]);
} finally {
setLoading(false);
}
};
const removeRow = (id: string) => {
onMount(() => void loadRows());
const removeRow = async (id: string) => {
const next = rows().filter((row) => row.id !== id);
setRows(next);
persist(next);
setLoading(true);
setErr('');
try {
await updateJobSeekerCustomData((current) => ({ ...current, saved_jobs: next }));
setRows(next);
} catch (e: any) {
setErr(e?.message || 'Failed to remove saved job.');
} finally {
setLoading(false);
}
};
return (
@ -49,19 +76,27 @@ export default function JobSeekerSavedJobsPage() {
<div style={CARD}>
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>Saved Jobs</p>
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
Jobs bookmarked for later. Saved locally on this device.
Jobs bookmarked for later.
</p>
</div>
<Show when={err()}>
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2', padding: '12px 14px', color: '#B91C1C', 'font-size': '13px', 'font-weight': '600' }}>{err()}</div>
</Show>
<div style={CARD}>
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '10px' }}>
<p style={{ margin: '0', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Bookmarked Jobs</p>
<button type="button" onClick={() => setRows(loadSaved())} style={BTN_GHOST}>Refresh</button>
<button type="button" onClick={() => void loadRows()} style={BTN_GHOST}>Refresh</button>
</div>
<Show when={rows().length === 0}>
<Show when={loading()}>
<p style={{ margin: '0', color: '#9CA3AF', 'font-size': '13px' }}>Loading saved jobs...</p>
</Show>
<Show when={!loading() && rows().length === 0}>
<p style={{ margin: '0', color: '#6B7280', 'font-size': '13px' }}>No saved jobs yet.</p>
</Show>
<Show when={rows().length > 0}>
<Show when={!loading() && rows().length > 0}>
<div style={{ display: 'grid', gap: '10px' }}>
<For each={rows()}>
{(row) => (
@ -73,10 +108,10 @@ export default function JobSeekerSavedJobsPage() {
{row.company || '—'} {row.location ? `${row.location}` : ''} {row.salary ? `${row.salary}` : ''}
</p>
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>
Saved on {new Date(row.saved_at).toLocaleString('en-IN')}
Saved on {renderSavedAt(row.saved_at)}
</p>
</div>
<button type="button" onClick={() => removeRow(row.id)} style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}>
<button type="button" onClick={() => void removeRow(row.id)} style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}>
Remove
</button>
</div>
@ -89,4 +124,3 @@ export default function JobSeekerSavedJobsPage() {
</div>
);
}

View file

@ -1,6 +1,7 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
import { readJobSeekerProfile } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@ -63,21 +64,38 @@ export default function MyDashboardPage(props: Props) {
);
if (!reqRes.ok) setErr('Some customer metrics could not be loaded.');
} else if (props.roleKey === 'JOB_SEEKER') {
const [jobsRes, appsRes] = await Promise.all([
const [jobsRes, appsRes, profile] = await Promise.all([
apiFetch('/api/jobseeker/jobs?page=1&limit=100'),
apiFetch('/api/jobseeker/applications?page=1&limit=100'),
readJobSeekerProfile(),
]);
const jobsJson = await jobsRes.json().catch(() => ({}));
const appsJson = await appsRes.json().catch(() => ({}));
const jobs = Array.isArray(jobsJson?.data) ? jobsJson.data : [];
const apps = Array.isArray(appsJson?.data) ? appsJson.data : [];
const customData = (profile?.custom_data && typeof profile.custom_data === 'object')
? (profile.custom_data as Record<string, unknown>)
: {};
const savedJobs = Array.isArray(customData.saved_jobs) ? customData.saved_jobs : [];
const portfolio = (customData.job_seeker_portfolio && typeof customData.job_seeker_portfolio === 'object')
? (customData.job_seeker_portfolio as Record<string, unknown>)
: {};
const profileStatus = String(profile?.status || 'NOT_SUBMITTED').replace(/_/g, ' ');
const portfolioDone = Boolean(
String(portfolio.headline || '').trim()
&& String(portfolio.education || '').trim()
&& String(portfolio.workExperience || '').trim()
&& String(portfolio.skills || '').trim(),
);
next.push(
{ title: 'Available Jobs', value: String(jobs.length), hint: 'Open approved jobs' },
{ title: 'My Applications', value: String(apps.length), hint: 'Total applications submitted' },
{ title: 'Shortlisted', value: String(apps.filter((a: any) => String(a.status || '').toUpperCase() === 'SHORTLISTED').length), hint: 'Moved ahead in process' },
{ title: 'Under Review', value: String(apps.filter((a: any) => String(a.status || '').toUpperCase().includes('REVIEW')).length), hint: 'Awaiting decision' },
{ title: 'Saved Jobs', value: String(savedJobs.length), hint: 'Bookmarked for later' },
{ title: 'Profile Status', value: profileStatus, hint: 'Verification state' },
{ title: 'Portfolio', value: portfolioDone ? 'Complete' : 'Incomplete', hint: 'Education/work/skills sections' },
);
if (!jobsRes.ok && !appsRes.ok) setErr('Some job seeker metrics could not be loaded.');
if (!jobsRes.ok && !appsRes.ok && !profile) setErr('Some job seeker metrics could not be loaded.');
} else if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) {
const prefix = ROLE_PREFIXES[props.roleKey];
const [marketRes, reqRes, walletRes] = await Promise.all([
@ -159,4 +177,3 @@ export default function MyDashboardPage(props: Props) {
</div>
);
}

View file

@ -1,10 +1,11 @@
/**
* PortfolioPage real My Portfolio CRUD, wired to backend.
* Uses existing /api/:rolePrefix/portfolio/me endpoints.
* Professionals only.
* Uses existing /api/:rolePrefix/portfolio/me endpoints for professionals.
* Job seekers get a dedicated portfolio editor (education/work experience/skills).
*/
import { For, Show, createSignal, onMount } from 'solid-js';
import { CARD, BTN_ORANGE, BTN_GHOST, BTN_PRIMARY, INPUT, LABEL } from '~/components/DashboardShell';
import { readJobSeekerProfile, updateJobSeekerCustomData } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@ -39,7 +40,23 @@ interface FormState {
tags: string;
}
interface JobSeekerPortfolioState {
headline: string;
summary: string;
education: string;
workExperience: string;
skills: string;
}
const EMPTY_FORM: FormState = { title: '', description: '', tags: '' };
const EMPTY_JOB_SEEKER_FORM: JobSeekerPortfolioState = {
headline: '',
summary: '',
education: '',
workExperience: '',
skills: '',
};
const JOB_SEEKER_FALLBACK_TABS = ['About', 'Education', 'Work Experience', 'Skills'];
// ── Helpers ───────────────────────────────────────────────────────────────────
@ -55,11 +72,14 @@ async function apiFetch(path: string, opts?: RequestInit) {
interface Props {
roleKey: string;
runtimeTabs?: string[];
runtimeFields?: string[];
}
export default function PortfolioPage(props: Props) {
const prefix = () => ROLE_PREFIX[props.roleKey] ?? '';
const isProfessional = () => Boolean(prefix());
const isJobSeeker = () => String(props.roleKey || '').toUpperCase() === 'JOB_SEEKER';
const [items, setItems] = createSignal<PortfolioItem[]>([]);
const [loading, setLoading] = createSignal(true);
@ -69,6 +89,12 @@ export default function PortfolioPage(props: Props) {
const [saving, setSaving] = createSignal(false);
const [deleting, setDeleting] = createSignal<string | null>(null);
const [error, setError] = createSignal('');
const [jobSeekerForm, setJobSeekerForm] = createSignal<JobSeekerPortfolioState>({ ...EMPTY_JOB_SEEKER_FORM });
const [jobSeekerSavedAt, setJobSeekerSavedAt] = createSignal('');
const [jobSeekerTab, setJobSeekerTab] = createSignal('About');
const [jobSeekerSaving, setJobSeekerSaving] = createSignal(false);
const [jobSeekerMsg, setJobSeekerMsg] = createSignal('');
const [jobSeekerErr, setJobSeekerErr] = createSignal('');
const loadItems = async () => {
if (!isProfessional()) { setLoading(false); return; }
@ -84,7 +110,102 @@ export default function PortfolioPage(props: Props) {
}
};
onMount(loadItems);
const loadJobSeekerPortfolio = () => {
readJobSeekerProfile()
.then((profile) => {
const parsed = profile?.custom_data?.job_seeker_portfolio as Record<string, unknown> | undefined;
if (!parsed || typeof parsed !== 'object') return;
setJobSeekerForm({
headline: String(parsed?.headline || ''),
summary: String(parsed?.summary || ''),
education: String(parsed?.education || ''),
workExperience: String(parsed?.workExperience || ''),
skills: String(parsed?.skills || ''),
});
setJobSeekerSavedAt(String(parsed?.savedAt || ''));
})
.catch(() => {
// ignore non-blocking load errors
});
};
const saveJobSeekerPortfolio = () => {
setJobSeekerSaving(true);
setJobSeekerMsg('');
setJobSeekerErr('');
const savedAt = new Date().toISOString();
const payload = { ...jobSeekerForm(), savedAt };
updateJobSeekerCustomData((current) => ({ ...current, job_seeker_portfolio: payload }))
.then(() => {
setJobSeekerSavedAt(savedAt);
setJobSeekerMsg('Portfolio saved successfully.');
})
.catch((e: any) => {
setJobSeekerErr(e?.message || 'Failed to save portfolio.');
})
.finally(() => {
setJobSeekerSaving(false);
});
};
const normalizeToken = (value: string) => String(value || '').trim().toLowerCase().replace(/[_-]+/g, ' ');
const toLabel = (value: string) =>
String(value || '')
.trim()
.replace(/[_-]+/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
const runtimePortfolioTabs = () => {
const raw = Array.isArray(props.runtimeTabs) ? props.runtimeTabs : [];
const allowed = new Set(['about', 'education', 'work experience', 'skills']);
const mapped = raw
.map((tab) => toLabel(tab))
.filter(Boolean)
.map((tab) => {
const t = normalizeToken(tab);
if (t.includes('education')) return 'Education';
if (t.includes('work') || t.includes('experience')) return 'Work Experience';
if (t.includes('skill')) return 'Skills';
if (t.includes('about') || t.includes('overview') || t.includes('profile')) return 'About';
return '';
})
.filter((tab) => Boolean(tab) && allowed.has(normalizeToken(tab)));
const unique = Array.from(new Set(mapped));
return unique.length ? unique : JOB_SEEKER_FALLBACK_TABS;
};
const runtimeFieldsByTab = () => {
const fromRuntime = Array.isArray(props.runtimeFields) ? props.runtimeFields.map((f) => toLabel(String(f || ''))).filter(Boolean) : [];
const grouped: Record<string, string[]> = {
About: [],
Education: [],
'Work Experience': [],
Skills: [],
};
for (const field of fromRuntime) {
const key = normalizeToken(field);
if (key.includes('education') || key.includes('college') || key.includes('degree')) grouped.Education.push(field);
else if (key.includes('work') || key.includes('experience') || key.includes('employment')) grouped['Work Experience'].push(field);
else if (key.includes('skill') || key.includes('tool') || key.includes('technology')) grouped.Skills.push(field);
else grouped.About.push(field);
}
if (!grouped.About.length) grouped.About = ['Professional Headline', 'Career Summary'];
if (!grouped.Education.length) grouped.Education = ['Education'];
if (!grouped['Work Experience'].length) grouped['Work Experience'] = ['Work Experience'];
if (!grouped.Skills.length) grouped.Skills = ['Skills'];
return grouped;
};
onMount(() => {
if (isJobSeeker()) {
loadJobSeekerPortfolio();
const tabs = runtimePortfolioTabs();
setJobSeekerTab(tabs[0] || 'About');
setLoading(false);
return;
}
void loadItems();
});
const openCreate = () => {
setEditId(null);
@ -157,6 +278,135 @@ export default function PortfolioPage(props: Props) {
}
};
// ── Job seeker portfolio editor ─────────────────────────────────────────
if (isJobSeeker()) {
const setField = (fieldKey: string, value: string) => {
const key = normalizeToken(fieldKey);
setJobSeekerForm((prev) => {
if (key.includes('headline')) return { ...prev, headline: value };
if (key.includes('summary') || key.includes('about')) return { ...prev, summary: value };
if (key.includes('education') || key.includes('degree') || key.includes('college')) return { ...prev, education: value };
if (key.includes('work') || key.includes('experience') || key.includes('employment')) return { ...prev, workExperience: value };
if (key.includes('skill') || key.includes('tool') || key.includes('technology')) return { ...prev, skills: value };
return { ...prev, summary: value };
});
};
const readField = (fieldKey: string) => {
const key = normalizeToken(fieldKey);
if (key.includes('headline')) return jobSeekerForm().headline;
if (key.includes('summary') || key.includes('about')) return jobSeekerForm().summary;
if (key.includes('education') || key.includes('degree') || key.includes('college')) return jobSeekerForm().education;
if (key.includes('work') || key.includes('experience') || key.includes('employment')) return jobSeekerForm().workExperience;
if (key.includes('skill') || key.includes('tool') || key.includes('technology')) return jobSeekerForm().skills;
return '';
};
const tabs = runtimePortfolioTabs();
const fieldsByTab = runtimeFieldsByTab();
const activeTab = () => tabs.includes(jobSeekerTab()) ? jobSeekerTab() : (tabs[0] || 'About');
const activeFields = () => fieldsByTab[activeTab()] || [];
const isLongField = (field: string) => {
const key = normalizeToken(field);
return key.includes('summary')
|| key.includes('about')
|| key.includes('experience')
|| key.includes('education')
|| key.includes('skills');
};
return (
<div style={{ 'max-width': '900px' }}>
<div style={{ ...CARD, 'margin-bottom': '14px', padding: '0 16px' }}>
<div style={{ display: 'flex', gap: '20px', 'border-bottom': '1px solid #E5E7EB', padding: '12px 0 0' }}>
<For each={tabs}>
{(tab) => (
<button
type="button"
onClick={() => setJobSeekerTab(tab)}
style={{
padding: '0 0 10px',
border: 'none',
background: 'none',
cursor: 'pointer',
'font-size': '13px',
'font-weight': jobSeekerTab() === tab ? '700' : '500',
color: jobSeekerTab() === tab ? '#FF5E13' : '#6B7280',
'border-bottom': jobSeekerTab() === tab ? '2px solid #FF5E13' : '2px solid transparent',
'margin-bottom': '-1px',
}}
>
{tab}
</button>
)}
</For>
</div>
<div style={{ padding: '14px 0' }}>
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>My Portfolio</p>
<p style={{ margin: '6px 0 0', 'font-size': '13px', color: '#6B7280' }}>
Runtime-config driven form using configured tabs and fields.
</p>
<Show when={jobSeekerSavedAt()}>
<p style={{ margin: '8px 0 0', 'font-size': '12px', color: '#059669', 'font-weight': '600' }}>
Saved to profile at {new Date(jobSeekerSavedAt()).toLocaleString()}.
</p>
</Show>
</div>
</div>
<div style={{ ...CARD, display: 'grid', gap: '12px' }}>
<Show when={jobSeekerMsg()}>
<div style={{ border: '1px solid #BBF7D0', background: '#ECFDF5', color: '#065F46', 'border-radius': '10px', padding: '10px 12px', 'font-size': '12px', 'font-weight': '600' }}>
{jobSeekerMsg()}
</div>
</Show>
<Show when={jobSeekerErr()}>
<div style={{ border: '1px solid #FECACA', background: '#FEF2F2', color: '#B91C1C', 'border-radius': '10px', padding: '10px 12px', 'font-size': '12px', 'font-weight': '600' }}>
{jobSeekerErr()}
</div>
</Show>
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '700', color: '#111827' }}>{activeTab()}</p>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '12px' }}>
<For each={activeFields()}>
{(field) => (
<div style={{ 'grid-column': isLongField(field) ? '1 / -1' : 'auto' }}>
<label style={LABEL}>{field}</label>
<Show
when={!isLongField(field)}
fallback={
<textarea
rows={4}
value={readField(field)}
onInput={(e) => setField(field, e.currentTarget.value)}
placeholder={`Enter ${field.toLowerCase()}`}
style={{ ...INPUT, height: 'auto', padding: '10px 12px', resize: 'vertical' }}
/>
}
>
<input
type="text"
value={readField(field)}
onInput={(e) => setField(field, e.currentTarget.value)}
placeholder={`Enter ${field.toLowerCase()}`}
style={INPUT}
/>
</Show>
</div>
)}
</For>
</div>
<div style={{ display: 'flex', gap: '10px', 'justify-content': 'flex-end' }}>
<button type="button" onClick={() => setJobSeekerForm({ ...EMPTY_JOB_SEEKER_FORM })} style={BTN_GHOST}>
Clear
</button>
<button type="button" onClick={saveJobSeekerPortfolio} disabled={jobSeekerSaving()} style={{ ...BTN_ORANGE, opacity: jobSeekerSaving() ? '0.7' : '1' }}>
{jobSeekerSaving() ? 'Saving...' : 'Save Portfolio'}
</button>
</div>
</div>
</div>
);
}
// ── Not a professional role ─────────────────────────────────────────────
if (!isProfessional()) {
return (

View file

@ -0,0 +1,68 @@
const API = '/api/gateway';
export type JobSeekerProfile = {
id: string;
user_id: string;
full_name?: string | null;
location?: string | null;
summary?: string | null;
experience_years?: number | null;
skills?: string[] | null;
resume_url?: string | null;
status?: string | null;
bio?: string | null;
custom_data?: Record<string, unknown> | null;
};
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
function asObject(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
export async function readJobSeekerProfile(): Promise<JobSeekerProfile | null> {
try {
const res = await apiFetch('/api/jobseeker/profile/me');
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
export async function updateJobSeekerCustomData(
mutator: (currentCustomData: Record<string, unknown>) => Record<string, unknown>,
): Promise<JobSeekerProfile> {
const existing = await readJobSeekerProfile();
const currentCustomData = asObject(existing?.custom_data);
const nextCustomData = mutator(currentCustomData);
const payload = {
full_name: existing?.full_name ?? null,
location: existing?.location ?? null,
summary: existing?.summary ?? null,
experience_years: typeof existing?.experience_years === 'number' ? existing.experience_years : 0,
skills: Array.isArray(existing?.skills) ? existing.skills : [],
resume_url: existing?.resume_url ?? null,
bio: existing?.bio ?? null,
custom_data: nextCustomData,
};
const res = await apiFetch('/api/jobseeker/profile/me', {
method: 'PATCH',
body: JSON.stringify(payload),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(String(data?.error || data?.message || 'Failed to update job seeker data.'));
}
return await res.json();
}

View file

@ -1,161 +1,350 @@
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';
import CompanyJobsPage from '~/components/dashboard/CompanyJobsPage';
import CompanyApplicationsPage from '~/components/dashboard/CompanyApplicationsPage';
import SettingsPage from '~/components/dashboard/SettingsPage';
import MyDashboardPage from '~/components/dashboard/MyDashboardPage';
import CustomerRequirementsPage from '~/components/dashboard/CustomerRequirementsPage';
import CustomerResponsesPage from '~/components/dashboard/CustomerResponsesPage';
import CompanyShortlistedCandidatesPage from '~/components/dashboard/CompanyShortlistedCandidatesPage';
import JobSeekerApplicationsPage from '~/components/dashboard/JobSeekerApplicationsPage';
import JobSeekerSavedJobsPage from '~/components/dashboard/JobSeekerSavedJobsPage';
import JobSeekerJobsPage from '~/components/dashboard/JobSeekerJobsPage';
import ProfessionalLeadsPage from '~/components/dashboard/ProfessionalLeadsPage';
import ProfessionalResponsesPage from '~/components/dashboard/ProfessionalResponsesPage';
import CreditsPage from '~/components/dashboard/CreditsPage';
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 { PROFESSIONAL_ROLE_SET } from '~/components/dashboard/RoleDashboardShared';
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";
import CompanyJobsPage from "~/components/dashboard/CompanyJobsPage";
import CompanyApplicationsPage from "~/components/dashboard/CompanyApplicationsPage";
import SettingsPage from "~/components/dashboard/SettingsPage";
import MyDashboardPage from "~/components/dashboard/MyDashboardPage";
import CustomerRequirementsPage from "~/components/dashboard/CustomerRequirementsPage";
import CustomerResponsesPage from "~/components/dashboard/CustomerResponsesPage";
import CompanyShortlistedCandidatesPage from "~/components/dashboard/CompanyShortlistedCandidatesPage";
import JobSeekerApplicationsPage from "~/components/dashboard/JobSeekerApplicationsPage";
import JobSeekerSavedJobsPage from "~/components/dashboard/JobSeekerSavedJobsPage";
import JobSeekerJobsPage from "~/components/dashboard/JobSeekerJobsPage";
import ProfessionalLeadsPage from "~/components/dashboard/ProfessionalLeadsPage";
import ProfessionalResponsesPage from "~/components/dashboard/ProfessionalResponsesPage";
import CreditsPage from "~/components/dashboard/CreditsPage";
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 { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared";
// Sidebar items that have real implementations (not the preview mock)
const BASE_REAL_PAGES = ['my dashboard', 'my profile', 'my portfolio', 'verification', 'settings'];
const COMMON_REAL_PAGES = ['credits', 'explore nxtgauge', 'help center', 'switch services', 'logout'];
const COMPANY_REAL_PAGES = ['jobs', 'applications', 'shortlisted candidates'];
const CUSTOMER_REAL_PAGES = ['my requirements', 'received responses', 'shortlisted responses'];
const JOB_SEEKER_REAL_PAGES = ['jobs', 'my applications', 'saved jobs'];
const PROFESSIONAL_REAL_PAGES = ['leads', 'my responses'];
// Sidebar items that have real data implementations (wired to backend APIs)
// These show real components instead of DashboardDesignPreview mock
const BASE_REAL_PAGES = ["my dashboard", "my profile", "my portfolio", "verification", "settings"];
const COMMON_REAL_PAGES = [
"credits",
"explore nxtgauge",
"help center",
"switch services",
"logout",
];
const COMPANY_REAL_PAGES = ["jobs", "applications", "shortlisted candidates"];
const CUSTOMER_REAL_PAGES = ["my requirements", "received responses", "shortlisted responses"];
const JOB_SEEKER_REAL_PAGES = ["jobs", "my applications", "saved jobs", "my portfolio"];
const PROFESSIONAL_REAL_PAGES = ["leads", "my responses"];
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';
| "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';
status: "ACTIVE" | "INACTIVE";
sidebarItems: string[];
tabs: string[];
widgets: string[];
fields: string[];
verificationStatus?: string;
source: 'dashboard-config';
source: "dashboard-config";
};
const API_GATEWAY = '/api/gateway';
const SERVER_API_BASE = (process.env.PUBLIC_API_URL || 'http://localhost:8080/api').replace(/\/+$/, '');
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',
"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'],
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",
"My Portfolio",
"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',
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'];
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;
const name =
parsed?.name || parsed?.first_name || parsed?.user?.name || parsed?.user?.first_name;
if (name) return String(name);
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
return 'User';
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' },
{ 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;
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);
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'];
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);
@ -163,29 +352,27 @@ function getInitialRoleFromStorage(): RoleKey {
const parsed = JSON.parse(raw);
const found = normalizeRole(
String(
parsed?.roleKey
|| parsed?.role
|| parsed?.active_role
|| parsed?.user?.active_role
|| parsed?.user?.roles?.[0]
|| '',
),
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';
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' });
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 {
@ -194,102 +381,117 @@ async function fetchJson(path: string): Promise<any | null> {
}
async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
if (typeof window === 'undefined') {
if (typeof window === "undefined") {
return null;
}
const runtime = await fetchJson('/api/runtime-config');
const runtime = await fetchJson("/api/runtime-config");
if (runtime) {
const runtimeRole = normalizeRole(
String(runtime?.role || runtime?.user?.active_role || role),
);
const runtimeRole = normalizeRole(String(runtime?.role || runtime?.user?.active_role || role));
const runtimeSidebar = asStringArray(
runtime?.dashboard_config?.sidebar_items
?? runtime?.dashboard_config?.sidebarItems
?? runtime?.sidebar_items
?? runtime?.sidebarItems,
);
const runtimeTabs = asStringArray(
runtime?.dashboard_config?.tabs ?? runtime?.tabs,
runtime?.dashboard_config?.sidebar_items ??
runtime?.dashboard_config?.sidebarItems ??
runtime?.sidebar_items ??
runtime?.sidebarItems
);
const runtimeTabs = asStringArray(runtime?.dashboard_config?.tabs ?? runtime?.tabs);
const runtimeWidgetsRaw = Array.isArray(runtime?.dashboard_config?.widgets)
? runtime.dashboard_config.widgets
: (Array.isArray(runtime?.widgets) ? runtime.widgets : []);
: Array.isArray(runtime?.widgets)
? runtime.widgets
: [];
const runtimeWidgets = runtimeWidgetsRaw
.map((item: any) => String(typeof item === 'string' ? item : (item?.key || item?.id || '')).trim())
.map((item: any) =>
String(typeof item === "string" ? item : item?.key || item?.id || "").trim()
)
.filter(Boolean);
const runtimeFields = asStringArray(
runtime?.dashboard_config?.fields ?? runtime?.fields,
);
const runtimeFields = asStringArray(runtime?.dashboard_config?.fields ?? runtime?.fields);
return {
role: runtimeRole,
status: 'ACTIVE',
status: "ACTIVE",
sidebarItems: runtimeSidebar,
tabs: runtimeTabs,
widgets: runtimeWidgets,
fields: runtimeFields,
verificationStatus: String(runtime?.verification_status || runtime?.user?.verification_status || '').toUpperCase() || undefined,
source: 'dashboard-config',
verificationStatus:
String(
runtime?.verification_status || runtime?.user?.verification_status || ""
).toUpperCase() || undefined,
source: "dashboard-config",
};
}
let payload = await fetchJson(`/api/config/dashboard/by-key/${encodeURIComponent(role)}?audience=EXTERNAL`);
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);
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 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())
.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',
status: payload?.is_active === false ? "INACTIVE" : "ACTIVE",
sidebarItems,
tabs,
widgets,
fields,
source: 'dashboard-config',
source: "dashboard-config",
};
}
function mergeSidebar(role: RoleKey, runtimeSidebar: string[], verificationStatus?: string): string[] {
const base = ROLE_BASED_SIDEBAR[role] || ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
function mergeSidebar(
role: RoleKey,
runtimeSidebar: string[],
verificationStatus?: string
): string[] {
const base = ROLE_BASED_SIDEBAR[role] || [
"My Dashboard",
"My Profile",
"Switch Services",
"Logout",
];
const fromRuntime = runtimeSidebar.filter(Boolean);
const source = fromRuntime.length > 0 ? fromRuntime : base;
const map = new Map<string, string>();
for (const item of [...fromRuntime, ...base]) {
for (const item of source) {
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');
}
const status = String(verificationStatus || '').toUpperCase();
const approved = status === 'APPROVED';
const status = String(verificationStatus || "").toUpperCase();
const approved = status === "APPROVED";
if (!approved && status) {
const restricted = new Set(
[
'my profile',
'help center',
'settings',
'verification',
'logout',
...(PROFESSIONAL_ROLE_SET.has(role) ? ['my portfolio', 'credits'] : []),
],
);
const restricted = new Set([
"my profile",
"help center",
"settings",
"verification",
"logout",
...(PROFESSIONAL_ROLE_SET.has(role) || role === "JOB_SEEKER"
? ["my portfolio", "credits"]
: []),
]);
merged = merged.filter((item) => restricted.has(item.trim().toLowerCase()));
}
return merged;
@ -299,11 +501,11 @@ 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('');
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);
@ -321,18 +523,17 @@ export default function RuntimeDashboardPage() {
createEffect(() => {
const u = auth.user();
if (u) {
if (u.full_name && userName() === 'User') setUserName(u.full_name);
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 [bundle] = createResource(() => role(), loadRoleBundle);
const sidebarItems = createMemo(() => mergeSidebar(role(), bundle()?.sidebarItems || [], bundle()?.verificationStatus));
const sidebarItems = createMemo(() =>
mergeSidebar(role(), bundle()?.sidebarItems || [], bundle()?.verificationStatus)
);
createEffect(() => {
const runtimeRole = bundle()?.role;
@ -340,7 +541,7 @@ export default function RuntimeDashboardPage() {
});
createEffect(() => {
const first = sidebarItems()[0] || 'My Dashboard';
const first = sidebarItems()[0] || "My Dashboard";
const current = activeSidebar();
const exists = sidebarItems().some((item) => item.toLowerCase() === current.toLowerCase());
if (!exists) setActiveSidebar(first);
@ -348,12 +549,12 @@ export default function RuntimeDashboardPage() {
const tabs = createMemo(() => {
const fromRuntime = bundle()?.tabs || [];
return fromRuntime.length > 0 ? fromRuntime : ['overview'];
return fromRuntime.length > 0 ? fromRuntime : ["overview"];
});
createEffect(() => {
role();
const firstTab = tabs()[0] || 'overview';
const firstTab = tabs()[0] || "overview";
setActiveTab((prev) => (prev ? prev : firstTab));
});
@ -370,23 +571,21 @@ export default function RuntimeDashboardPage() {
const key = activeSidebar().toLowerCase();
if (BASE_REAL_PAGES.includes(key)) return true;
if (COMMON_REAL_PAGES.includes(key)) return true;
if (role() === 'COMPANY' && COMPANY_REAL_PAGES.includes(key)) return true;
if (role() === 'CUSTOMER' && CUSTOMER_REAL_PAGES.includes(key)) return true;
if (role() === 'JOB_SEEKER' && JOB_SEEKER_REAL_PAGES.includes(key)) return true;
if (role() === "COMPANY" && COMPANY_REAL_PAGES.includes(key)) return true;
if (role() === "CUSTOMER" && CUSTOMER_REAL_PAGES.includes(key)) return true;
if (role() === "JOB_SEEKER" && JOB_SEEKER_REAL_PAGES.includes(key)) return true;
if (PROFESSIONAL_ROLE_SET.has(role()) && PROFESSIONAL_REAL_PAGES.includes(key)) return true;
return false;
});
return (
<RequireAuth>
<main style={{ 'min-height': '100vh', background: '#F3F4F6' }}>
<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
@ -397,70 +596,106 @@ export default function RuntimeDashboardPage() {
userName={userName()}
>
<Switch>
<Match when={activeSidebar().toLowerCase() === 'my dashboard'}>
<Match when={activeSidebar().toLowerCase() === "my dashboard"}>
<MyDashboardPage roleKey={role()} userName={userName()} />
</Match>
<Match when={activeSidebar().toLowerCase() === 'my profile'}>
<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
<Match when={activeSidebar().toLowerCase() === "my portfolio"}>
<PortfolioPage
roleKey={role()}
onNavigate={setActiveSidebar}
runtimeTabs={bundle()?.tabs || []}
runtimeFields={bundle()?.fields || []}
/>
</Match>
<Match when={activeSidebar().toLowerCase() === 'settings'}>
<Match when={activeSidebar().toLowerCase() === "verification"}>
<VerificationStatusPage roleKey={role()} onNavigate={setActiveSidebar} />
</Match>
<Match when={activeSidebar().toLowerCase() === "settings"}>
<SettingsPage />
</Match>
<Match when={activeSidebar().toLowerCase() === 'credits'}>
<Match when={activeSidebar().toLowerCase() === "credits"}>
<CreditsPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === 'explore nxtgauge'}>
<Match when={activeSidebar().toLowerCase() === "explore nxtgauge"}>
<ExploreServicesPage />
</Match>
<Match when={activeSidebar().toLowerCase() === 'help center'}>
<Match when={activeSidebar().toLowerCase() === "help center"}>
<HelpCenterDashboardPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === 'switch services'}>
<Match when={activeSidebar().toLowerCase() === "switch services"}>
<SwitchServicesPage />
</Match>
<Match when={activeSidebar().toLowerCase() === 'logout'}>
<Match when={activeSidebar().toLowerCase() === "logout"}>
<LogoutPage />
</Match>
<Match when={role() === 'COMPANY' && activeSidebar().toLowerCase() === 'jobs'}>
<Match when={role() === "COMPANY" && activeSidebar().toLowerCase() === "jobs"}>
<CompanyJobsPage />
</Match>
<Match when={role() === 'COMPANY' && activeSidebar().toLowerCase() === 'applications'}>
<Match
when={role() === "COMPANY" && activeSidebar().toLowerCase() === "applications"}
>
<CompanyApplicationsPage />
</Match>
<Match when={role() === 'COMPANY' && activeSidebar().toLowerCase() === 'shortlisted candidates'}>
<Match
when={
role() === "COMPANY" &&
activeSidebar().toLowerCase() === "shortlisted candidates"
}
>
<CompanyShortlistedCandidatesPage />
</Match>
<Match when={role() === 'CUSTOMER' && activeSidebar().toLowerCase() === 'my requirements'}>
<Match
when={
role() === "CUSTOMER" && activeSidebar().toLowerCase() === "my requirements"
}
>
<CustomerRequirementsPage />
</Match>
<Match when={role() === 'CUSTOMER' && activeSidebar().toLowerCase() === 'received responses'}>
<Match
when={
role() === "CUSTOMER" && activeSidebar().toLowerCase() === "received responses"
}
>
<CustomerResponsesPage mode="received" />
</Match>
<Match when={role() === 'CUSTOMER' && activeSidebar().toLowerCase() === 'shortlisted responses'}>
<Match
when={
role() === "CUSTOMER" &&
activeSidebar().toLowerCase() === "shortlisted responses"
}
>
<CustomerResponsesPage mode="shortlisted" />
</Match>
<Match when={role() === 'JOB_SEEKER' && activeSidebar().toLowerCase() === 'my applications'}>
<Match
when={
role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "my applications"
}
>
<JobSeekerApplicationsPage />
</Match>
<Match when={role() === 'JOB_SEEKER' && activeSidebar().toLowerCase() === 'jobs'}>
<Match when={role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "jobs"}>
<JobSeekerJobsPage />
</Match>
<Match when={role() === 'JOB_SEEKER' && activeSidebar().toLowerCase() === 'saved jobs'}>
<Match
when={role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "saved jobs"}
>
<JobSeekerSavedJobsPage />
</Match>
<Match when={PROFESSIONAL_ROLE_SET.has(role()) && activeSidebar().toLowerCase() === 'leads'}>
<Match
when={
PROFESSIONAL_ROLE_SET.has(role()) && activeSidebar().toLowerCase() === "leads"
}
>
<ProfessionalLeadsPage roleKey={role()} />
</Match>
<Match when={PROFESSIONAL_ROLE_SET.has(role()) && activeSidebar().toLowerCase() === 'my responses'}>
<Match
when={
PROFESSIONAL_ROLE_SET.has(role()) &&
activeSidebar().toLowerCase() === "my responses"
}
>
<ProfessionalResponsesPage roleKey={role()} />
</Match>
</Switch>
@ -470,7 +705,7 @@ export default function RuntimeDashboardPage() {
{/* ── All other views: DashboardDesignPreview mock ── */}
<Show when={!isRealPage()}>
<DashboardDesignPreview
status={bundle()?.status ?? 'ACTIVE'}
status={bundle()?.status ?? "ACTIVE"}
sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar}
@ -486,7 +721,6 @@ export default function RuntimeDashboardPage() {
liveData={liveData()}
/>
</Show>
</Show>
</main>
</RequireAuth>
@ -494,9 +728,9 @@ export default function RuntimeDashboardPage() {
}
const cardStyle = {
border: '1px solid #E5E7EB',
'border-radius': '12px',
padding: '16px',
background: '#fff',
color: '#111827',
border: "1px solid #E5E7EB",
"border-radius": "12px",
padding: "16px",
background: "#fff",
color: "#111827",
} as const;

View file

@ -78,7 +78,7 @@ export default function SignupRoute() {
const [emailExists, setEmailExists] = createSignal(false);
const [submitting, setSubmitting] = createSignal(false);
const [pendingEmail, setPendingEmail] = createSignal('');
const [pendingPassword, setPendingPassword] = createSignal('');
const [verifiedSuccess, setVerifiedSuccess] = createSignal(false);
const [showPassword, setShowPassword] = createSignal(false);
const [showConfirmPassword, setShowConfirmPassword] = createSignal(false);
@ -164,14 +164,6 @@ export default function SignupRoute() {
}
};
const storeAuth = (payload: any) => {
const accessToken = String(payload?.access_token || '').trim();
if (typeof window !== 'undefined' && accessToken) {
window.sessionStorage.setItem('nxtgauge_access_token', accessToken);
window.sessionStorage.setItem('nxtgauge_frontend_access_token', accessToken);
}
};
const register = async () => {
setServerError('');
const validation = validateRegisterForm({
@ -212,7 +204,7 @@ export default function SignupRoute() {
const cleanEmail = email().trim().toLowerCase();
setPendingEmail(cleanEmail);
setPendingPassword(password());
setVerifiedSuccess(false);
saveUserForDashboard({
firstName: firstName().trim(),
lastName: lastName().trim(),
@ -245,32 +237,8 @@ export default function SignupRoute() {
setServerError(String(verifyData?.error || verifyData?.message || 'Verification failed.'));
return;
}
const loginRes = await fetch('/api/gateway/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
credentials: 'include',
body: JSON.stringify({
email: pendingEmail() || email().trim().toLowerCase(),
password: pendingPassword() || password(),
}),
});
const loginData = await loginRes.json().catch(() => ({}));
if (!loginRes.ok) {
setServerError(String(loginData?.error || loginData?.message || 'Email verified. Please login manually.'));
navigate('/login', { replace: true });
return;
}
storeAuth(loginData);
saveUserForDashboard({
firstName: firstName().trim(),
lastName: lastName().trim(),
email: pendingEmail() || email().trim().toLowerCase(),
roleKey: role(),
user: loginData?.user,
});
navigate('/dashboard', { replace: true });
setVerifiedSuccess(true);
setTimeout(() => navigate('/login?verified=1', { replace: true }), 1400);
} finally {
setSubmitting(false);
}
@ -316,31 +284,39 @@ export default function SignupRoute() {
<h2 class="title">Verify Email</h2>
<p class="subtitle">Enter the 6-digit code sent to <strong>{pendingEmail() || email()}</strong>.</p>
<div class="otp-row">
<For each={Array.from({ length: 6 }, (_, index) => index)}>
{(index) => (
<input
id={`otp-${index}`}
class="otp-input"
inputMode="numeric"
maxlength={1}
value={otp()[index]}
onInput={(e) => setOtpDigit(index, e.currentTarget.value)}
/>
)}
</For>
</div>
<Show when={!verifiedSuccess()} fallback={
<div style={{ 'margin-top': '12px', 'border-radius': '12px', border: '1px solid #FED7AA', background: '#FFF7ED', padding: '14px 16px', color: '#C2410C', 'text-align': 'center' }}>
<div style={{ 'font-size': '30px', 'line-height': '1' }}></div>
<p style={{ margin: '8px 0 0', 'font-weight': '700', 'font-size': '14px' }}>Your email has been verified.</p>
<p style={{ margin: '6px 0 0', 'font-size': '13px' }}>Redirecting to login...</p>
</div>
}>
<div class="otp-row">
<For each={Array.from({ length: 6 }, (_, index) => index)}>
{(index) => (
<input
id={`otp-${index}`}
class="otp-input"
inputMode="numeric"
maxlength={1}
value={otp()[index]}
onInput={(e) => setOtpDigit(index, e.currentTarget.value)}
/>
)}
</For>
</div>
<button class="auth-submit-btn" type="button" disabled={submitting()} onClick={() => void verifyOtp()}>
{submitting() ? 'Verifying...' : 'Verify and Continue'}
</button>
<div class="auth-footer-row">
<p class="note">Didnt receive code?</p>
<button class="auth-forgot-link" type="button" onClick={() => void resendOtp()} disabled={submitting()}>
Resend OTP
<button class="auth-submit-btn" type="button" disabled={submitting()} onClick={() => void verifyOtp()}>
{submitting() ? 'Verifying...' : 'Verify and Continue'}
</button>
</div>
<div class="auth-footer-row">
<p class="note">Didnt receive code?</p>
<button class="auth-forgot-link" type="button" onClick={() => void resendOtp()} disabled={submitting()}>
Resend OTP
</button>
</div>
</Show>
</>
}>
<h2 class="title">Create Your Account</h2>