From bbf11b91e1945b7197b7c540cfb87357670539cb Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Mon, 6 Apr 2026 17:20:48 +0200 Subject: [PATCH] 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 --- src/components/DashboardShell.tsx | 249 +++++++++ src/components/dashboard/PortfolioPage.tsx | 364 ++++++++++++ src/components/dashboard/ProfilePage.tsx | 526 ++++++++++++++++++ .../dashboard/VerificationStatusPage.tsx | 363 ++++++++++++ src/routes/dashboard.tsx | 54 +- 5 files changed, 1549 insertions(+), 7 deletions(-) create mode 100644 src/components/DashboardShell.tsx create mode 100644 src/components/dashboard/PortfolioPage.tsx create mode 100644 src/components/dashboard/ProfilePage.tsx create mode 100644 src/components/dashboard/VerificationStatusPage.tsx diff --git a/src/components/DashboardShell.tsx b/src/components/DashboardShell.tsx new file mode 100644 index 0000000..33f0bef --- /dev/null +++ b/src/components/DashboardShell.tsx @@ -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 = { + '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 ; +} + +// ── 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 ( +
+ + {/* ── Sidebar ──────────────────────────────────────────────────────── */} + + + {/* ── Main content ─────────────────────────────────────────────────── */} +
+ {/* Top bar */} +
+

+ {props.activeSidebar} +

+
+ +
+ {(props.userName || 'U').charAt(0).toUpperCase()} +
+
+
+ + {/* Page content */} +
+ {props.children} +
+
+
+ ); +} + +// ── 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; diff --git a/src/components/dashboard/PortfolioPage.tsx b/src/components/dashboard/PortfolioPage.tsx new file mode 100644 index 0000000..e2ff986 --- /dev/null +++ b/src/components/dashboard/PortfolioPage.tsx @@ -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 = { + 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([]); + const [loading, setLoading] = createSignal(true); + const [showForm, setShowForm] = createSignal(false); + const [editId, setEditId] = createSignal(null); + const [form, setForm] = createSignal({ ...EMPTY_FORM }); + const [saving, setSaving] = createSignal(false); + const [deleting, setDeleting] = createSignal(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 ( +
+

+ Portfolio is available for professional roles only. +

+
+ ); + } + + return ( +
+ + {/* ── Header ────────────────────────────────────────────────────── */} +
+
+

My Portfolio

+

+ Showcase your work to attract clients. +

+
+ +
+ + {/* ── Create / Edit form ─────────────────────────────────────────── */} + +
+

+ {editId() ? 'Edit Portfolio Item' : 'New Portfolio Item'} +

+ +
+
+ + setField('title', e.currentTarget.value)} + style={INPUT} + /> +
+
+ +