feat(dashboard): real My Profile, My Portfolio, Verification pages

- 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>
This commit is contained in:
Ashwin Kumar 2026-04-06 17:20:48 +02:00
parent ed68a636f5
commit bbf11b91e1
5 changed files with 1549 additions and 7 deletions

View file

@ -0,0 +1,249 @@
/**
* DashboardShell real sidebar + header layout wrapper.
* Used for pages that need actual backend connectivity
* (My Profile, My Portfolio, Verification) instead of the preview mock.
*/
import { For, JSX, Show, createMemo } from 'solid-js';
import {
User, Briefcase, LayoutDashboard, FolderOpen, MapPin, Star,
CreditCard, Globe, ShieldCheck, HelpCircle, Settings,
RefreshCw, LogOut, Bell, ChevronRight,
} from 'lucide-solid';
// ── Icon map (matches DashboardDesignPreview sidebar keys) ────────────────────
const ICON_MAP: Record<string, any> = {
'my dashboard': LayoutDashboard,
'my profile': User,
'my portfolio': FolderOpen,
'leads': MapPin,
'my responses': Star,
'credits': CreditCard,
'explore nxtgauge': Globe,
'verification': ShieldCheck,
'help center': HelpCircle,
'settings': Settings,
'switch services': RefreshCw,
'jobs': Briefcase,
'applications': Briefcase,
'shortlisted candidates': User,
'my applications': FolderOpen,
'saved jobs': Star,
'my requirements': FolderOpen,
'received responses': Bell,
'shortlisted responses': Star,
'logout': LogOut,
};
function SidebarIcon(props: { label: string }) {
const key = props.label.toLowerCase();
const Icon = ICON_MAP[key] || ChevronRight;
return <Icon size={16} />;
}
// ── Types ─────────────────────────────────────────────────────────────────────
interface Props {
sidebarItems: string[];
activeSidebar: string;
onSidebarSelect: (item: string) => void;
roleKey: string;
userName?: string;
children: JSX.Element;
}
// ── Brand colours ─────────────────────────────────────────────────────────────
const ORANGE = '#FF5E13';
const NAVY = '#0D0D2A';
// ── Component ─────────────────────────────────────────────────────────────────
export default function DashboardShell(props: Props) {
const roleLabel = createMemo(() => {
const k = String(props.roleKey || '').replace(/_/g, ' ');
return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase();
});
return (
<div style={{
display: 'flex',
'min-height': '100vh',
background: '#F3F4F6',
'font-family': "'Inter', sans-serif",
}}>
{/* ── Sidebar ──────────────────────────────────────────────────────── */}
<aside style={{
width: '220px',
'flex-shrink': '0',
background: NAVY,
display: 'flex',
'flex-direction': 'column',
'padding': '0',
'min-height': '100vh',
position: 'sticky',
top: '0',
height: '100vh',
'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>
{/* 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>
{/* Nav items */}
<nav style={{ flex: '1', padding: '8px 8px' }}>
<For each={props.sidebarItems}>
{(item) => {
const isActive = () => item.toLowerCase() === props.activeSidebar.toLowerCase();
const isLogout = item.toLowerCase() === 'logout';
return (
<button
type="button"
onClick={() => props.onSidebarSelect(item)}
style={{
display: 'flex',
'align-items': 'center',
gap: '10px',
width: '100%',
'text-align': 'left',
padding: '9px 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)',
transition: 'background 0.15s, color 0.15s',
}}
>
<span style={{ 'flex-shrink': '0', opacity: isActive() ? '1' : '0.7' }}>
<SidebarIcon label={item} />
</span>
{item}
</button>
);
}}
</For>
</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' }}>
{props.userName || 'User'}
</p>
</div>
</aside>
{/* ── Main content ─────────────────────────────────────────────────── */}
<div style={{ flex: '1', display: 'flex', 'flex-direction': 'column', 'min-width': '0' }}>
{/* Top bar */}
<header style={{
height: '56px',
background: '#fff',
'border-bottom': '1px solid #E5E7EB',
display: 'flex',
'align-items': 'center',
'justify-content': 'space-between',
padding: '0 24px',
'flex-shrink': '0',
}}>
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '700', color: NAVY }}>
{props.activeSidebar}
</p>
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>
<Bell size={18} style={{ color: '#9CA3AF' }} />
<div style={{
width: '32px', height: '32px', 'border-radius': '999px',
background: ORANGE, color: '#fff', display: 'flex',
'align-items': 'center', 'justify-content': 'center',
'font-size': '13px', 'font-weight': '700',
}}>
{(props.userName || 'U').charAt(0).toUpperCase()}
</div>
</div>
</header>
{/* Page content */}
<main style={{ flex: '1', padding: '24px', 'overflow-y': 'auto' }}>
{props.children}
</main>
</div>
</div>
);
}
// ── Shared UI primitives ──────────────────────────────────────────────────────
export const CARD = {
background: '#fff',
border: '1px solid #E5E7EB',
'border-radius': '14px',
padding: '20px',
'box-shadow': '0 1px 4px rgba(0,0,0,0.06)',
} as const;
export const BTN_PRIMARY = {
height: '38px',
'border-radius': '10px',
border: 'none',
background: NAVY,
color: '#fff',
padding: '0 18px',
'font-size': '13px',
'font-weight': '700',
cursor: 'pointer',
} as const;
export const BTN_ORANGE = {
height: '38px',
'border-radius': '10px',
border: 'none',
background: ORANGE,
color: '#fff',
padding: '0 18px',
'font-size': '13px',
'font-weight': '700',
cursor: 'pointer',
} as const;
export const BTN_GHOST = {
height: '38px',
'border-radius': '10px',
border: '1px solid #E5E7EB',
background: '#fff',
color: '#374151',
padding: '0 18px',
'font-size': '13px',
'font-weight': '600',
cursor: 'pointer',
} as const;
export const INPUT = {
height: '40px',
width: '100%',
'border-radius': '8px',
border: '1px solid #E5E7EB',
padding: '0 12px',
'font-size': '14px',
color: '#111827',
background: '#fff',
'box-sizing': 'border-box',
} as const;
export const LABEL = {
display: 'block',
'font-size': '12px',
'font-weight': '600',
color: '#374151',
'margin-bottom': '6px',
} as const;

View file

@ -0,0 +1,364 @@
/**
* PortfolioPage real My Portfolio CRUD, wired to backend.
* Uses existing /api/:rolePrefix/portfolio/me endpoints.
* Professionals only.
*/
import { For, Show, createSignal, onMount } from 'solid-js';
import { CARD, BTN_ORANGE, BTN_GHOST, BTN_PRIMARY, INPUT, LABEL } from '~/components/DashboardShell';
const API = '/api/gateway';
// ── Role prefix map ───────────────────────────────────────────────────────────
const ROLE_PREFIX: 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',
};
// ── Types ─────────────────────────────────────────────────────────────────────
interface PortfolioItem {
id: string;
title: string;
description?: string;
tags?: string[];
created_at?: string;
}
interface FormState {
title: string;
description: string;
tags: string;
}
const EMPTY_FORM: FormState = { title: '', description: '', tags: '' };
// ── Helpers ───────────────────────────────────────────────────────────────────
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
// ── Component ─────────────────────────────────────────────────────────────────
interface Props {
roleKey: string;
}
export default function PortfolioPage(props: Props) {
const prefix = () => ROLE_PREFIX[props.roleKey] ?? '';
const isProfessional = () => Boolean(prefix());
const [items, setItems] = createSignal<PortfolioItem[]>([]);
const [loading, setLoading] = createSignal(true);
const [showForm, setShowForm] = createSignal(false);
const [editId, setEditId] = createSignal<string | null>(null);
const [form, setForm] = createSignal<FormState>({ ...EMPTY_FORM });
const [saving, setSaving] = createSignal(false);
const [deleting, setDeleting] = createSignal<string | null>(null);
const [error, setError] = createSignal('');
const loadItems = async () => {
if (!isProfessional()) { setLoading(false); return; }
setLoading(true);
try {
const res = await apiFetch(`/api/${prefix()}/portfolio/me`);
if (res.ok) {
const data = await res.json();
setItems(Array.isArray(data) ? data : (data.items ?? []));
}
} finally {
setLoading(false);
}
};
onMount(loadItems);
const openCreate = () => {
setEditId(null);
setForm({ ...EMPTY_FORM });
setError('');
setShowForm(true);
};
const openEdit = (item: PortfolioItem) => {
setEditId(item.id);
setForm({
title: item.title,
description: item.description ?? '',
tags: (item.tags ?? []).join(', '),
});
setError('');
setShowForm(true);
};
const cancelForm = () => {
setShowForm(false);
setEditId(null);
setForm({ ...EMPTY_FORM });
setError('');
};
const setField = (key: keyof FormState, val: string) =>
setForm((prev) => ({ ...prev, [key]: val }));
const handleSave = async () => {
if (!form().title.trim()) { setError('Title is required.'); return; }
setSaving(true);
setError('');
const payload = {
title: form().title.trim(),
description: form().description.trim() || undefined,
tags: form().tags
.split(',')
.map((t) => t.trim())
.filter(Boolean),
};
try {
const id = editId();
const res = id
? await apiFetch(`/api/${prefix()}/portfolio/me/${id}`, { method: 'PATCH', body: JSON.stringify(payload) })
: await apiFetch(`/api/${prefix()}/portfolio/me`, { method: 'POST', body: JSON.stringify(payload) });
if (res.ok) {
await loadItems();
cancelForm();
} else {
const d = await res.json().catch(() => ({}));
setError(d.error ?? d.message ?? 'Failed to save. Please try again.');
}
} catch {
setError('Network error. Please try again.');
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm('Delete this portfolio item?')) return;
setDeleting(id);
try {
await apiFetch(`/api/${prefix()}/portfolio/me/${id}`, { method: 'DELETE' });
await loadItems();
} finally {
setDeleting(null);
}
};
// ── Not a professional role ─────────────────────────────────────────────
if (!isProfessional()) {
return (
<div style={{ ...CARD, 'text-align': 'center', padding: '40px' }}>
<p style={{ margin: '0', 'font-size': '15px', color: '#9CA3AF' }}>
Portfolio is available for professional roles only.
</p>
</div>
);
}
return (
<div style={{ 'max-width': '800px' }}>
{/* ── Header ────────────────────────────────────────────────────── */}
<div style={{ display: 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'margin-bottom': '16px' }}>
<div>
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>My Portfolio</p>
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
Showcase your work to attract clients.
</p>
</div>
<button type="button" onClick={openCreate} style={BTN_ORANGE}>
+ Add Item
</button>
</div>
{/* ── Create / Edit form ─────────────────────────────────────────── */}
<Show when={showForm()}>
<div style={{ ...CARD, 'margin-bottom': '16px', border: '1px solid #FF5E13' }}>
<p style={{ margin: '0 0 16px', 'font-size': '16px', 'font-weight': '800', color: '#0D0D2A' }}>
{editId() ? 'Edit Portfolio Item' : 'New Portfolio Item'}
</p>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '14px' }}>
<div style={{ 'grid-column': 'span 2' }}>
<label style={LABEL}>Title <span style={{ color: '#EF4444' }}>*</span></label>
<input
type="text"
placeholder="e.g. Wedding shoot at Udaipur"
value={form().title}
onInput={(e) => setField('title', e.currentTarget.value)}
style={INPUT}
/>
</div>
<div style={{ 'grid-column': 'span 2' }}>
<label style={LABEL}>Description</label>
<textarea
rows={3}
placeholder="Brief description of the work…"
value={form().description}
onInput={(e) => setField('description', e.currentTarget.value)}
style={{ ...INPUT, height: 'auto', padding: '10px 12px', resize: 'vertical' }}
/>
</div>
<div style={{ 'grid-column': 'span 2' }}>
<label style={LABEL}>Tags (comma separated)</label>
<input
type="text"
placeholder="e.g. wedding, outdoor, portrait"
value={form().tags}
onInput={(e) => setField('tags', e.currentTarget.value)}
style={INPUT}
/>
</div>
</div>
<Show when={error()}>
<p style={{ margin: '12px 0 0', 'font-size': '13px', color: '#EF4444', 'font-weight': '600' }}>
{error()}
</p>
</Show>
<div style={{ display: 'flex', gap: '10px', 'margin-top': '16px' }}>
<button
type="button"
onClick={handleSave}
disabled={saving()}
style={{ ...BTN_PRIMARY, opacity: saving() ? '0.6' : '1' }}
>
{saving() ? 'Saving…' : editId() ? 'Update Item' : 'Add Item'}
</button>
<button type="button" onClick={cancelForm} style={BTN_GHOST}>
Cancel
</button>
</div>
</div>
</Show>
{/* ── Loading ─────────────────────────────────────────────────────── */}
<Show when={loading()}>
<div style={{ ...CARD, 'text-align': 'center', padding: '32px', color: '#9CA3AF', 'font-size': '14px' }}>
Loading portfolio
</div>
</Show>
{/* ── Empty state ─────────────────────────────────────────────────── */}
<Show when={!loading() && items().length === 0 && !showForm()}>
<div style={{ ...CARD, 'text-align': 'center', padding: '48px 24px' }}>
<p style={{ margin: '0', 'font-size': '40px' }}>🗂</p>
<p style={{ margin: '12px 0 4px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>
No portfolio items yet
</p>
<p style={{ margin: '0 0 16px', 'font-size': '13px', color: '#6B7280' }}>
Add your first work sample to attract clients.
</p>
<button type="button" onClick={openCreate} style={BTN_ORANGE}>
+ Add First Item
</button>
</div>
</Show>
{/* ── Portfolio grid ─────────────────────────────────────────────── */}
<Show when={!loading() && items().length > 0}>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '12px' }}>
<For each={items()}>
{(item) => (
<div style={{
...CARD,
padding: '16px',
display: 'flex',
'flex-direction': 'column',
gap: '8px',
}}>
{/* Placeholder image area */}
<div style={{
height: '120px',
'border-radius': '8px',
background: '#F3F4F6',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
color: '#D1D5DB',
'font-size': '28px',
}}>
🖼
</div>
<p style={{ margin: '0', 'font-size': '14px', 'font-weight': '700', color: '#111827' }}>
{item.title}
</p>
<Show when={item.description}>
<p style={{ margin: '0', 'font-size': '12px', color: '#6B7280', 'line-height': '1.5' }}>
{item.description}
</p>
</Show>
<Show when={item.tags && item.tags.length > 0}>
<div style={{ display: 'flex', 'flex-wrap': 'wrap', gap: '4px' }}>
<For each={item.tags}>
{(tag) => (
<span style={{
'font-size': '10px',
'font-weight': '700',
color: '#6B7280',
background: '#F3F4F6',
border: '1px solid #E5E7EB',
'border-radius': '6px',
padding: '2px 8px',
}}>
{tag}
</span>
)}
</For>
</div>
</Show>
<div style={{ display: 'flex', gap: '8px', 'margin-top': '4px' }}>
<button
type="button"
onClick={() => openEdit(item)}
style={{ ...BTN_GHOST, height: '30px', 'font-size': '11px', padding: '0 12px', flex: '1' }}
>
Edit
</button>
<button
type="button"
onClick={() => handleDelete(item.id)}
disabled={deleting() === item.id}
style={{
height: '30px',
'border-radius': '8px',
border: '1px solid #FECACA',
background: '#fff',
color: '#EF4444',
'font-size': '11px',
'font-weight': '700',
padding: '0 12px',
cursor: 'pointer',
flex: '1',
opacity: deleting() === item.id ? '0.6' : '1',
}}
>
{deleting() === item.id ? '…' : 'Delete'}
</button>
</div>
</div>
)}
</For>
</div>
</Show>
</div>
);
}

View file

@ -0,0 +1,526 @@
/**
* ProfilePage real My Profile form, wired to backend.
* Supports all 13 roles. Tabs: Basic Info · Documents · Settings.
* User fills and saves freely; "Submit for Verification" locks and queues for admin.
*/
import {
For, Match, Show, Switch, createEffect, createSignal, onMount,
} from 'solid-js';
import { CARD, BTN_ORANGE, BTN_GHOST, INPUT, LABEL, BTN_PRIMARY } from '~/components/DashboardShell';
const API = '/api/gateway';
// ── Role-specific field definitions ──────────────────────────────────────────
const BASIC_FIELDS: Record<string, Array<{ key: string; label: string; type?: string; required?: boolean; options?: string[] }>> = {
default: [
{ key: 'first_name', label: 'First Name', required: true },
{ key: 'last_name', label: 'Last Name', required: true },
{ key: 'phone', label: 'Mobile Number', required: true },
{ key: 'gender', label: 'Gender', type: 'select', options: ['Male', 'Female', 'Other', 'Prefer not to say'] },
{ key: 'city', label: 'City', required: true },
{ key: 'state', label: 'State', required: true },
{ key: 'pin_code', label: 'PIN Code' },
{ key: 'address', label: 'Address', type: 'textarea' },
],
COMPANY: [
{ key: 'company_name', label: 'Company Name', required: true },
{ key: 'company_email', label: 'Company Email', type: 'email', required: true },
{ key: 'company_phone', label: 'Company Phone' },
{ key: 'website', label: 'Website URL', type: 'url' },
{ key: 'city', label: 'City', required: true },
{ key: 'state', label: 'State', required: true },
{ key: 'pin_code', label: 'PIN Code' },
{ key: 'address', label: 'Registered Address', type: 'textarea' },
{ key: 'gst_number', label: 'GST Number (optional)' },
],
PHOTOGRAPHER: [
{ key: 'first_name', label: 'First Name', required: true },
{ key: 'last_name', label: 'Last Name', required: true },
{ key: 'phone', label: 'Mobile Number', required: true },
{ key: 'city', label: 'City', required: true },
{ key: 'state', label: 'State', required: true },
{ key: 'pin_code', label: 'PIN Code' },
{ key: 'speciality', label: 'Photography Speciality', type: 'select',
options: ['Wedding', 'Portrait', 'Commercial', 'Event', 'Wildlife', 'Fashion', 'Product', 'Other'] },
{ key: 'experience_years', label: 'Years of Experience', type: 'number' },
{ key: 'bio', label: 'Short Bio', type: 'textarea' },
],
FITNESS_TRAINER: [
{ key: 'first_name', label: 'First Name', required: true },
{ key: 'last_name', label: 'Last Name', required: true },
{ key: 'phone', label: 'Mobile Number', required: true },
{ key: 'city', label: 'City', required: true },
{ key: 'state', label: 'State', required: true },
{ key: 'training_type', label: 'Training Type', type: 'select',
options: ['Personal Training', 'Group Fitness', 'Yoga', 'CrossFit', 'Zumba', 'Pilates', 'Other'] },
{ key: 'experience_years', label: 'Years of Experience', type: 'number' },
{ key: 'bio', label: 'Short Bio', type: 'textarea' },
],
TUTOR: [
{ key: 'first_name', label: 'First Name', required: true },
{ key: 'last_name', label: 'Last Name', required: true },
{ key: 'phone', label: 'Mobile Number', required: true },
{ key: 'city', label: 'City', required: true },
{ key: 'state', label: 'State', required: true },
{ key: 'subjects', label: 'Subjects Taught (comma separated)' },
{ key: 'experience_years', label: 'Years of Experience', type: 'number' },
{ key: 'bio', label: 'Short Bio', type: 'textarea' },
],
CATERING_SERVICES: [
{ key: 'business_name', label: 'Business Name', required: true },
{ key: 'owner_name', label: 'Owner Name', required: true },
{ key: 'phone', label: 'Contact Number', required: true },
{ key: 'city', label: 'City', required: true },
{ key: 'state', label: 'State', required: true },
{ key: 'cuisine_types', label: 'Cuisine Types (comma separated)' },
{ key: 'bio', label: 'About Your Service', type: 'textarea' },
],
};
const DOC_FIELDS: Record<string, Array<{ key: string; label: string; required?: boolean; hint?: string }>> = {
default: [
{ key: 'aadhar_doc', label: 'Aadhar / Government ID', required: true,
hint: 'JPG, PNG or PDF · Max 10MB' },
],
COMPANY: [
{ key: 'registration_doc', label: 'Company Registration Certificate', required: true,
hint: 'JPG, PNG or PDF · Max 10MB' },
{ key: 'gst_doc', label: 'GST Certificate (optional)',
hint: 'JPG, PNG or PDF · Max 10MB' },
],
PHOTOGRAPHER: [
{ key: 'aadhar_doc', label: 'Aadhar / Government ID', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
{ key: 'sample_work', label: 'Sample Work Photos (23 images)', required: true, hint: 'JPG or PNG · Max 5MB each' },
],
MAKEUP_ARTIST: [
{ key: 'aadhar_doc', label: 'Aadhar / Government ID', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
{ key: 'sample_work', label: 'Sample Work Photos (23 images)', required: true, hint: 'JPG or PNG · Max 5MB each' },
],
TUTOR: [
{ key: 'aadhar_doc', label: 'Aadhar / Government ID', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
{ key: 'degree_certificate', label: 'Degree Certificate', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
],
FITNESS_TRAINER: [
{ key: 'aadhar_doc', label: 'Aadhar / Government ID', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
{ key: 'certification_doc', label: 'Fitness Certification', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
],
CATERING_SERVICES: [
{ key: 'aadhar_doc', label: 'Aadhar / Government ID', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
{ key: 'fssai_license', label: 'FSSAI License', required: true, hint: 'JPG, PNG or PDF · Max 10MB' },
],
};
function getBasicFields(roleKey: string) {
return BASIC_FIELDS[roleKey] ?? BASIC_FIELDS.default;
}
function getDocFields(roleKey: string) {
return DOC_FIELDS[roleKey] ?? DOC_FIELDS.default;
}
// ── Helpers ───────────────────────────────────────────────────────────────────
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
// ── Component ─────────────────────────────────────────────────────────────────
interface Props {
roleKey: string;
}
type Tab = 'basic' | 'documents' | 'settings';
export default function ProfilePage(props: Props) {
const [tab, setTab] = createSignal<Tab>('basic');
const [form, setForm] = createSignal<Record<string, string>>({});
const [saving, setSaving] = createSignal(false);
const [saveMsg, setSaveMsg] = createSignal('');
const [verificationStatus, setVerificationStatus] = createSignal('NOT_SUBMITTED');
const [docRequest, setDocRequest] = createSignal<string | null>(null);
const [submitting, setSubmitting] = createSignal(false);
const [submitMsg, setSubmitMsg] = createSignal('');
// Load saved profile + verification status on mount
onMount(async () => {
const [profileRes, statusRes] = await Promise.all([
apiFetch(`/api/profile?roleKey=${props.roleKey}`),
apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`),
]);
if (profileRes.ok) {
const data = await profileRes.json();
if (data.profile_data && typeof data.profile_data === 'object') {
const flat: Record<string, string> = {};
for (const [k, v] of Object.entries(data.profile_data)) {
flat[k] = String(v ?? '');
}
setForm(flat);
}
}
if (statusRes.ok) {
const s = await statusRes.json();
setVerificationStatus(s.status ?? 'NOT_SUBMITTED');
setDocRequest(s.document_request ?? null);
}
});
const isLocked = () =>
['PENDING', 'UNDER_REVIEW'].includes(verificationStatus());
const setField = (key: string, val: string) =>
setForm((prev) => ({ ...prev, [key]: val }));
const handleSave = async () => {
setSaving(true);
setSaveMsg('');
try {
const res = await apiFetch('/api/profile', {
method: 'PATCH',
body: JSON.stringify({ roleKey: props.roleKey, profile_data: form() }),
});
setSaveMsg(res.ok ? 'Saved successfully.' : 'Failed to save. Please try again.');
} catch {
setSaveMsg('Network error. Please try again.');
} finally {
setSaving(false);
setTimeout(() => setSaveMsg(''), 3000);
}
};
const handleSubmitForVerification = async () => {
setSubmitting(true);
setSubmitMsg('');
try {
const res = await apiFetch('/api/profile/submit-for-verification', {
method: 'POST',
body: JSON.stringify({ roleKey: props.roleKey }),
});
const data = await res.json();
if (res.ok) {
setVerificationStatus('PENDING');
setSubmitMsg('Submitted! We will review your profile and notify you.');
} else if (res.status === 409) {
setSubmitMsg(data.error ?? 'A verification is already in progress.');
} else {
setSubmitMsg(data.error ?? 'Submission failed. Please try again.');
}
} catch {
setSubmitMsg('Network error. Please try again.');
} finally {
setSubmitting(false);
}
};
const statusColor: Record<string, string> = {
PENDING: '#F59E0B',
UNDER_REVIEW: '#3B82F6',
DOCUMENTS_REQUESTED: '#FF5E13',
REVISION_REQUESTED: '#FF5E13',
APPROVED: '#10B981',
REJECTED: '#EF4444',
NOT_SUBMITTED: '#9CA3AF',
};
const statusLabel: Record<string, string> = {
PENDING: 'Pending Review',
UNDER_REVIEW: 'Under Review',
DOCUMENTS_REQUESTED: 'Documents Requested',
REVISION_REQUESTED: 'Revision Requested',
APPROVED: 'Approved',
REJECTED: 'Rejected',
NOT_SUBMITTED: 'Not Submitted',
};
return (
<div style={{ 'max-width': '760px' }}>
{/* ── Verification status banner ─────────────────────────────────── */}
<div style={{
...CARD,
'margin-bottom': '16px',
display: 'flex',
'align-items': 'center',
'justify-content': 'space-between',
gap: '12px',
'flex-wrap': 'wrap',
}}>
<div style={{ display: 'flex', 'align-items': 'center', gap: '10px' }}>
<span style={{
display: 'inline-flex',
'align-items': 'center',
height: '22px',
padding: '0 10px',
'border-radius': '999px',
background: `${statusColor[verificationStatus()] ?? '#9CA3AF'}22`,
color: statusColor[verificationStatus()] ?? '#9CA3AF',
'font-size': '11px',
'font-weight': '700',
}}>
{statusLabel[verificationStatus()] ?? verificationStatus()}
</span>
<Show when={docRequest()}>
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>
<strong style={{ color: '#FF5E13' }}>Action needed:</strong> {docRequest()}
</p>
</Show>
</div>
<Show when={!isLocked() && verificationStatus() !== 'APPROVED'}>
<button
type="button"
onClick={handleSubmitForVerification}
disabled={submitting()}
style={{ ...BTN_ORANGE, opacity: submitting() ? '0.7' : '1' }}
>
{submitting() ? 'Submitting…' : 'Submit for Verification'}
</button>
</Show>
</div>
<Show when={submitMsg()}>
<div style={{
...CARD,
'margin-bottom': '16px',
padding: '12px 16px',
background: submitMsg().includes('Submitted') ? '#ECFDF5' : '#FEF2F2',
border: `1px solid ${submitMsg().includes('Submitted') ? '#6EE7B7' : '#FECACA'}`,
color: submitMsg().includes('Submitted') ? '#065F46' : '#B91C1C',
'font-size': '13px',
'font-weight': '600',
}}>
{submitMsg()}
</div>
</Show>
{/* ── Tabs ──────────────────────────────────────────────────────── */}
<div style={{ display: 'flex', gap: '4px', 'margin-bottom': '16px' }}>
<For each={[
{ key: 'basic', label: 'Basic Information' },
{ key: 'documents', label: 'Documents' },
{ key: 'settings', label: 'Settings' },
] as Array<{ key: Tab; label: string }>}>
{(t) => (
<button
type="button"
onClick={() => setTab(t.key)}
style={{
height: '36px',
padding: '0 16px',
'border-radius': '8px',
border: tab() === t.key ? '1px solid #FF5E13' : '1px solid #E5E7EB',
background: tab() === t.key ? '#FFF3EE' : '#fff',
color: tab() === t.key ? '#FF5E13' : '#6B7280',
'font-size': '13px',
'font-weight': tab() === t.key ? '700' : '500',
cursor: 'pointer',
}}
>
{t.label}
</button>
)}
</For>
</div>
{/* ── Tab content ───────────────────────────────────────────────── */}
<div style={CARD}>
<Switch>
{/* Basic Info */}
<Match when={tab() === 'basic'}>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '16px' }}>
<For each={getBasicFields(props.roleKey)}>
{(field) => (
<div style={{ 'grid-column': field.type === 'textarea' ? 'span 2' : 'span 1' }}>
<label style={LABEL}>
{field.label}
<Show when={field.required}>
<span style={{ color: '#EF4444' }}> *</span>
</Show>
</label>
<Switch>
<Match when={field.type === 'textarea'}>
<textarea
rows={3}
disabled={isLocked()}
value={form()[field.key] ?? ''}
onInput={(e) => setField(field.key, e.currentTarget.value)}
style={{
...INPUT,
height: 'auto',
padding: '10px 12px',
resize: 'vertical',
opacity: isLocked() ? '0.6' : '1',
}}
/>
</Match>
<Match when={field.type === 'select'}>
<select
disabled={isLocked()}
value={form()[field.key] ?? ''}
onChange={(e) => setField(field.key, e.currentTarget.value)}
style={{ ...INPUT, opacity: isLocked() ? '0.6' : '1' }}
>
<option value="">Select</option>
<For each={field.options ?? []}>
{(opt) => <option value={opt}>{opt}</option>}
</For>
</select>
</Match>
<Match when={true}>
<input
type={field.type ?? 'text'}
disabled={isLocked()}
value={form()[field.key] ?? ''}
onInput={(e) => setField(field.key, e.currentTarget.value)}
style={{ ...INPUT, opacity: isLocked() ? '0.6' : '1' }}
/>
</Match>
</Switch>
</div>
)}
</For>
</div>
<Show when={isLocked()}>
<p style={{ margin: '16px 0 0', 'font-size': '12px', color: '#9CA3AF' }}>
Profile is locked while verification is in progress.
</p>
</Show>
</Match>
{/* Documents */}
<Match when={tab() === 'documents'}>
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '16px' }}>
<For each={getDocFields(props.roleKey)}>
{(doc) => (
<div style={{ 'border': '1px dashed #E5E7EB', 'border-radius': '10px', padding: '16px' }}>
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>
{doc.label}
<Show when={doc.required}>
<span style={{ color: '#EF4444' }}> *</span>
</Show>
</p>
<Show when={doc.hint}>
<p style={{ margin: '2px 0 10px', 'font-size': '11px', color: '#9CA3AF' }}>{doc.hint}</p>
</Show>
<Show
when={form()[doc.key]}
fallback={
<div style={{ display: 'flex', 'align-items': 'center', gap: '10px' }}>
<input
type="file"
id={`file-${doc.key}`}
style={{ display: 'none' }}
disabled={isLocked()}
onChange={(e) => {
const file = e.currentTarget.files?.[0];
if (file) setField(doc.key, file.name);
}}
/>
<label
for={`file-${doc.key}`}
style={{
...BTN_GHOST,
display: 'inline-flex',
'align-items': 'center',
'line-height': '1',
opacity: isLocked() ? '0.5' : '1',
cursor: isLocked() ? 'not-allowed' : 'pointer',
}}
>
Choose File
</label>
<span style={{ 'font-size': '12px', color: '#9CA3AF' }}>No file chosen</span>
</div>
}
>
<div style={{ display: 'flex', 'align-items': 'center', gap: '10px' }}>
<span style={{
'font-size': '12px', 'font-weight': '600', color: '#10B981',
background: '#ECFDF5', padding: '4px 10px', 'border-radius': '6px',
}}>
{form()[doc.key]}
</span>
<Show when={!isLocked()}>
<button
type="button"
onClick={() => setField(doc.key, '')}
style={{ ...BTN_GHOST, height: '28px', 'font-size': '11px', padding: '0 10px' }}
>
Remove
</button>
</Show>
</div>
</Show>
</div>
)}
</For>
</div>
</Match>
{/* Settings */}
<Match when={tab() === 'settings'}>
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '20px' }}>
<div>
<p style={{ margin: '0 0 4px', 'font-size': '15px', 'font-weight': '700', color: '#111827' }}>Password & Login</p>
<p style={{ margin: '0 0 12px', 'font-size': '13px', color: '#6B7280' }}>Change your account password.</p>
<div style={{ display: 'grid', 'grid-template-columns': '1fr 1fr', gap: '12px' }}>
<div>
<label style={LABEL}>Current Password</label>
<input type="password" style={INPUT} />
</div>
<div>
<label style={LABEL}>New Password</label>
<input type="password" style={INPUT} />
</div>
</div>
</div>
<div style={{ 'border-top': '1px solid #F3F4F6', 'padding-top': '16px' }}>
<p style={{ margin: '0 0 4px', 'font-size': '15px', 'font-weight': '700', color: '#111827' }}>Notification Preferences</p>
<p style={{ margin: '0 0 12px', 'font-size': '13px', color: '#6B7280' }}>Choose how you receive notifications.</p>
<For each={['Email Notifications', 'In-App Notifications', 'SMS Alerts']}>
{(label) => (
<div style={{ display: 'flex', 'align-items': 'center', 'justify-content': 'space-between', padding: '10px 0', 'border-bottom': '1px solid #F3F4F6' }}>
<span style={{ 'font-size': '13px', color: '#374151', 'font-weight': '500' }}>{label}</span>
<input type="checkbox" checked style={{ width: '16px', height: '16px', cursor: 'pointer' }} />
</div>
)}
</For>
</div>
</div>
</Match>
</Switch>
</div>
{/* ── Save button (not on settings) ─────────────────────────────── */}
<Show when={tab() !== 'settings'}>
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px', 'margin-top': '16px' }}>
<button
type="button"
onClick={handleSave}
disabled={saving() || isLocked()}
style={{ ...BTN_PRIMARY, opacity: saving() || isLocked() ? '0.6' : '1' }}
>
{saving() ? 'Saving…' : 'Save Changes'}
</button>
<Show when={saveMsg()}>
<span style={{
'font-size': '13px',
'font-weight': '600',
color: saveMsg().includes('success') ? '#10B981' : '#EF4444',
}}>
{saveMsg()}
</span>
</Show>
</div>
</Show>
</div>
);
}

View file

@ -0,0 +1,363 @@
/**
* VerificationStatusPage shows the user their current verification state.
* Handles: NOT_SUBMITTED, PENDING, UNDER_REVIEW, DOCUMENTS_REQUESTED,
* REVISION_REQUESTED, APPROVED, REJECTED.
*/
import { Show, createSignal, onMount } from 'solid-js';
import { CARD, BTN_ORANGE, BTN_GHOST } from '~/components/DashboardShell';
const API = '/api/gateway';
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
});
}
// ── Status config ─────────────────────────────────────────────────────────────
const STATUS_CONFIG: Record<string, {
emoji: string;
label: string;
color: string;
bg: string;
border: string;
description: string;
}> = {
NOT_SUBMITTED: {
emoji: '📋',
label: 'Not Submitted',
color: '#6B7280',
bg: '#F9FAFB',
border: '#E5E7EB',
description: 'Complete your My Profile and My Portfolio, then submit for verification to start using the platform.',
},
PENDING: {
emoji: '⏳',
label: 'Pending Review',
color: '#92400E',
bg: '#FFFBEB',
border: '#FDE68A',
description: 'Your profile has been submitted and is in the review queue. We typically respond within 2448 hours.',
},
UNDER_REVIEW: {
emoji: '🔍',
label: 'Under Review',
color: '#1E40AF',
bg: '#EEF2FF',
border: '#BFDBFE',
description: 'Our team is actively reviewing your submission. You will be notified once a decision is made.',
},
DOCUMENTS_REQUESTED: {
emoji: '📎',
label: 'Documents Requested',
color: '#C2410C',
bg: '#FFF7ED',
border: '#FED7AA',
description: 'Admin has requested additional or clearer documents. Please review the message below and resubmit.',
},
REVISION_REQUESTED: {
emoji: '✏️',
label: 'Revision Requested',
color: '#C2410C',
bg: '#FFF7ED',
border: '#FED7AA',
description: 'Admin has requested changes to your profile information. Please update and resubmit.',
},
APPROVED: {
emoji: '✅',
label: 'Approved',
color: '#065F46',
bg: '#ECFDF5',
border: '#6EE7B7',
description: 'Your profile has been verified and approved. You now have full access to the platform.',
},
REJECTED: {
emoji: '❌',
label: 'Rejected',
color: '#991B1B',
bg: '#FEF2F2',
border: '#FECACA',
description: 'Your verification was rejected. Please review the reason below, update your profile, and resubmit.',
},
};
const FLOW_STEPS = [
{ key: 'submit', label: 'Submit Profile' },
{ key: 'review', label: 'Under Review' },
{ key: 'verify', label: 'Verified' },
{ key: 'approved', label: 'Approved' },
];
function stepIndex(status: string): number {
switch (status) {
case 'NOT_SUBMITTED': return 0;
case 'PENDING': return 1;
case 'UNDER_REVIEW': return 2;
case 'DOCUMENTS_REQUESTED':
case 'REVISION_REQUESTED': return 2; // still at review stage
case 'APPROVED': return 4;
default: return 1;
}
}
// ── Component ─────────────────────────────────────────────────────────────────
interface Props {
roleKey: string;
onNavigate?: (sidebar: string) => void;
}
export default function VerificationStatusPage(props: Props) {
const [status, setStatus] = createSignal('NOT_SUBMITTED');
const [docRequest, setDocRequest] = createSignal<string | null>(null);
const [rejectionReason, setRejectionReason] = createSignal<string | null>(null);
const [updatedAt, setUpdatedAt] = createSignal<string | null>(null);
const [loading, setLoading] = createSignal(true);
const [resubmitting, setResubmitting] = createSignal(false);
const [resubmitMsg, setResubmitMsg] = createSignal('');
onMount(async () => {
try {
const res = await apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`);
if (res.ok) {
const d = await res.json();
setStatus(d.status ?? 'NOT_SUBMITTED');
setDocRequest(d.document_request ?? null);
setRejectionReason(d.rejection_reason ?? null);
setUpdatedAt(d.updated_at ?? null);
}
} finally {
setLoading(false);
}
});
const cfg = () => STATUS_CONFIG[status()] ?? STATUS_CONFIG.NOT_SUBMITTED;
const currentStep = () => stepIndex(status());
const canResubmit = () => ['DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status());
const handleResubmit = async () => {
setResubmitting(true);
setResubmitMsg('');
try {
const res = await apiFetch('/api/profile/submit-for-verification', {
method: 'POST',
body: JSON.stringify({ roleKey: props.roleKey }),
});
const d = await res.json().catch(() => ({}));
if (res.ok) {
setStatus('PENDING');
setDocRequest(null);
setRejectionReason(null);
setResubmitMsg('Resubmitted successfully! We will review your profile.');
} else if (res.status === 409) {
setResubmitMsg(d.error ?? 'A verification is already in progress.');
} else {
setResubmitMsg(d.error ?? 'Resubmission failed. Please try again.');
}
} catch {
setResubmitMsg('Network error. Please try again.');
} finally {
setResubmitting(false);
}
};
return (
<div style={{ 'max-width': '640px' }}>
<Show when={loading()}>
<div style={{ ...CARD, 'text-align': 'center', padding: '32px', color: '#9CA3AF' }}>
Loading verification status
</div>
</Show>
<Show when={!loading()}>
{/* ── Main status card ─────────────────────────────────────────── */}
<div style={{
...CARD,
background: cfg().bg,
border: `1px solid ${cfg().border}`,
'margin-bottom': '16px',
display: 'flex',
'flex-direction': 'column',
gap: '12px',
}}>
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>
<span style={{ 'font-size': '36px', 'line-height': '1' }}>{cfg().emoji}</span>
<div>
<p style={{ margin: '0', 'font-size': '11px', 'text-transform': 'uppercase', 'letter-spacing': '0.08em', color: cfg().color, 'font-weight': '700' }}>
Verification Status
</p>
<p style={{ margin: '2px 0 0', 'font-size': '22px', 'font-weight': '800', color: cfg().color }}>
{cfg().label}
</p>
</div>
</div>
<p style={{ margin: '0', 'font-size': '13px', color: '#374151', 'line-height': '1.6' }}>
{cfg().description}
</p>
<Show when={updatedAt()}>
<p style={{ margin: '0', 'font-size': '11px', color: '#9CA3AF' }}>
Last updated: {new Date(updatedAt()!).toLocaleString('en-IN')}
</p>
</Show>
</div>
{/* ── Doc request / rejection reason ──────────────────────────── */}
<Show when={docRequest()}>
<div style={{
...CARD,
background: '#FFF7ED',
border: '1px solid #FED7AA',
'margin-bottom': '16px',
}}>
<p style={{ margin: '0 0 6px', 'font-size': '12px', 'font-weight': '700', 'text-transform': 'uppercase', 'letter-spacing': '0.06em', color: '#C2410C' }}>
Document Request from Admin
</p>
<p style={{ margin: '0', 'font-size': '14px', color: '#374151', 'line-height': '1.6' }}>
{docRequest()}
</p>
</div>
</Show>
<Show when={rejectionReason()}>
<div style={{
...CARD,
background: '#FEF2F2',
border: '1px solid #FECACA',
'margin-bottom': '16px',
}}>
<p style={{ margin: '0 0 6px', 'font-size': '12px', 'font-weight': '700', 'text-transform': 'uppercase', 'letter-spacing': '0.06em', color: '#B91C1C' }}>
Rejection Reason
</p>
<p style={{ margin: '0', 'font-size': '14px', color: '#374151', 'line-height': '1.6' }}>
{rejectionReason()}
</p>
</div>
</Show>
{/* ── Progress timeline ──────────────────────────────────────── */}
<Show when={status() !== 'APPROVED'}>
<div style={{ ...CARD, 'margin-bottom': '16px' }}>
<p style={{ margin: '0 0 14px', 'font-size': '14px', 'font-weight': '700', color: '#111827' }}>
Verification Progress
</p>
<div style={{ display: 'flex', 'align-items': 'center', gap: '0' }}>
{FLOW_STEPS.map((step, idx) => {
const done = currentStep() > idx;
const active = currentStep() === idx + 1;
return (
<>
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', 'flex-shrink': '0' }}>
<div style={{
width: '28px',
height: '28px',
'border-radius': '999px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'font-size': '11px',
'font-weight': '800',
background: done ? '#FF5E13' : active ? '#FFF3EE' : '#F3F4F6',
color: done ? '#fff' : active ? '#FF5E13' : '#9CA3AF',
border: active ? '2px solid #FF5E13' : '2px solid transparent',
}}>
{done ? '✓' : idx + 1}
</div>
<p style={{
margin: '4px 0 0',
'font-size': '10px',
'font-weight': '600',
color: done || active ? '#374151' : '#9CA3AF',
'white-space': 'nowrap',
'text-align': 'center',
}}>
{step.label}
</p>
</div>
{idx < FLOW_STEPS.length - 1 && (
<div style={{
flex: '1',
height: '2px',
background: done ? '#FF5E13' : '#E5E7EB',
'margin-bottom': '18px',
}} />
)}
</>
);
})}
</div>
</div>
</Show>
{/* ── Actions ───────────────────────────────────────────────── */}
<div style={{ display: 'flex', gap: '10px', 'flex-wrap': 'wrap' }}>
<Show when={status() === 'NOT_SUBMITTED'}>
<button
type="button"
onClick={() => props.onNavigate?.('My Profile')}
style={BTN_ORANGE}
>
Fill My Profile
</button>
<button
type="button"
onClick={() => props.onNavigate?.('My Portfolio')}
style={BTN_GHOST}
>
Fill My Portfolio
</button>
</Show>
<Show when={canResubmit()}>
<button
type="button"
onClick={() => props.onNavigate?.('My Profile')}
style={BTN_GHOST}
>
Update My Profile
</button>
<button
type="button"
onClick={handleResubmit}
disabled={resubmitting()}
style={{ ...BTN_ORANGE, opacity: resubmitting() ? '0.7' : '1' }}
>
{resubmitting() ? 'Resubmitting…' : 'Resubmit for Verification'}
</button>
</Show>
</div>
<Show when={resubmitMsg()}>
<p style={{
margin: '12px 0 0',
'font-size': '13px',
'font-weight': '600',
color: resubmitMsg().includes('successfully') ? '#10B981' : '#EF4444',
}}>
{resubmitMsg()}
</p>
</Show>
{/* ── Approved state: success message ───────────────────────── */}
<Show when={status() === 'APPROVED'}>
<div style={{ ...CARD, background: '#ECFDF5', border: '1px solid #6EE7B7', 'text-align': 'center', padding: '32px' }}>
<p style={{ margin: '0', 'font-size': '48px' }}>🎉</p>
<p style={{ margin: '12px 0 4px', 'font-size': '20px', 'font-weight': '800', color: '#065F46' }}>
You're Verified!
</p>
<p style={{ margin: '0', 'font-size': '14px', color: '#047857', 'line-height': '1.6' }}>
Your profile is approved. Start exploring opportunities on Nxtgauge.
</p>
</div>
</Show>
</Show>
</div>
);
}

View file

@ -1,7 +1,14 @@
import { Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
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'
@ -279,16 +286,48 @@ export default function RuntimeDashboardPage() {
return { userName: userName(), userId: userId(), rolePrefix: prefix };
});
const isRealPage = createMemo(() =>
REAL_PAGES.includes(activeSidebar().toLowerCase()),
);
return (
<RequireAuth>
<main style={{ 'min-height': '100vh', background: '#f3f4f6' }}>
<div>
<main style={{ 'min-height': '100vh', background: '#F3F4F6' }}>
<Show when={loading()}>
<div style={cardStyle}>Loading dashboard...</div>
<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>
<Show when={ready()}>
{/* ── All other views: DashboardDesignPreview mock ── */}
<Show when={!isRealPage()}>
<DashboardDesignPreview
status={bundle()?.status ?? 'ACTIVE'}
sidebarItems={sidebarItems()}
@ -306,7 +345,8 @@ export default function RuntimeDashboardPage() {
liveData={liveData()}
/>
</Show>
</div>
</Show>
</main>
</RequireAuth>
);