diff --git a/src/components/dashboard/ExploreServicesPage.tsx b/src/components/dashboard/ExploreServicesPage.tsx index ab87aa7..bc4e6b0 100644 --- a/src/components/dashboard/ExploreServicesPage.tsx +++ b/src/components/dashboard/ExploreServicesPage.tsx @@ -1,6 +1,8 @@ import { For, Show, createMemo, createSignal, onMount } from "solid-js"; -import { BTN_GHOST, BTN_PRIMARY, CARD } from "~/components/DashboardShell"; -import { Camera, Scissors, GraduationCap, Code2, Clapperboard, PenTool, Megaphone, Dumbbell, UtensilsCrossed, Briefcase, Globe, Users, UserCircle, FileText, TrendingUp, Award, BarChart3, ShieldCheck } from "lucide-solid"; +import { ArrowLeft, Camera, Scissors, GraduationCap, Code2, Clapperboard, PenTool, Megaphone, Dumbbell, UtensilsCrossed, Globe, Users, UserCircle, FileText, TrendingUp, Award, BarChart3, ShieldCheck } from "lucide-solid"; +import { BTN_GHOST, CARD } from "~/components/DashboardShell"; +import RoleWizard from "~/components/dashboard/RoleWizard"; +import { roleKeyToPrefix } from "~/lib/role-utils"; const NAVY = "#0D0D2A"; const ORANGE = "#FF5E13"; @@ -8,31 +10,37 @@ const ORANGE = "#FF5E13"; const API = ""; const MAIN_ROLES = [ - { key: "COMPANY", name: "Company", Icon: Users, subtitle: "Hire talent, post jobs, and manage applications in one path." }, - { key: "JOB_SEEKER", name: "Job Seeker", Icon: UserCircle, subtitle: "Explore opportunities and apply to roles with your profile." }, - { key: "CUSTOMER", name: "Service Seeker", Icon: FileText, subtitle: "Post requirements and connect with verified professionals." }, + { key: "COMPANY", name: "Company", Icon: Users, subtitle: "Hire talent, post jobs, and manage applications in one path." }, + { key: "JOB_SEEKER", name: "Job Seeker", Icon: UserCircle, subtitle: "Explore opportunities and apply to roles with your profile." }, + { key: "CUSTOMER", name: "Service Seeker", Icon: FileText, subtitle: "Post requirements and connect with verified professionals." }, ]; const PROFESSIONAL_ROLES = [ - { key: "PHOTOGRAPHER", name: "Photographer", Icon: Camera }, - { key: "MAKEUP_ARTIST", name: "Makeup Artist", Icon: Scissors }, - { key: "TUTOR", name: "Tutor", Icon: GraduationCap }, - { key: "DEVELOPER", name: "Developer", Icon: Code2 }, - { key: "VIDEO_EDITOR", name: "Video Editor", Icon: Clapperboard }, - { key: "UGC_CONTENT_CREATOR", name: "UGC Content Creator", Icon: Clapperboard }, - { key: "GRAPHIC_DESIGNER", name: "Graphic Designer", Icon: PenTool }, - { key: "SOCIAL_MEDIA_MANAGER", name: "Social Media Manager", Icon: Megaphone }, - { key: "FITNESS_TRAINER", name: "Fitness Trainer", Icon: Dumbbell }, - { key: "CATERING_SERVICES", name: "Catering Services", Icon: UtensilsCrossed }, + { key: "PHOTOGRAPHER", name: "Photographer", Icon: Camera }, + { key: "MAKEUP_ARTIST", name: "Makeup Artist", Icon: Scissors }, + { key: "TUTOR", name: "Tutor", Icon: GraduationCap }, + { key: "DEVELOPER", name: "Developer", Icon: Code2 }, + { key: "VIDEO_EDITOR", name: "Video Editor", Icon: Clapperboard }, + { key: "UGC_CONTENT_CREATOR", name: "UGC Content Creator", Icon: Clapperboard }, + { key: "GRAPHIC_DESIGNER", name: "Graphic Designer", Icon: PenTool }, + { key: "SOCIAL_MEDIA_MANAGER", name: "Social Media Manager", Icon: Megaphone }, + { key: "FITNESS_TRAINER", name: "Fitness Trainer", Icon: Dumbbell }, + { key: "CATERING_SERVICES", name: "Catering Services", Icon: UtensilsCrossed }, ]; type RoleCard = { key: string; title: string; subtitle: string; + /** "Register" | "Switch" | "Current Role" | "Under Review" */ action: string; - Icon: any; - status: "Active" | "Registered" | "Available"; + Icon: (props: { size: number; color: string; strokeWidth: number }) => unknown; + status: "Active" | "Pending" | "Registered" | "Available"; +}; + +type UserRoleItem = { + role_key: string; + status?: string; }; async function apiFetch(path: string, opts?: RequestInit) { @@ -58,53 +66,93 @@ function toTitle(value: string): string { .replace(/\b\w/g, (c) => c.toUpperCase()); } -function StatusBadge(props: { status: "Active" | "Registered" | "Available" }) { - const { status } = props; - const isActive = status === "Active"; - const isRegistered = status === "Registered"; +function StatusBadge(props: { status: "Active" | "Pending" | "Registered" | "Available" }) { + const dotColor = () => props.status === "Active" ? ORANGE : props.status === "Pending" ? "#F59E0B" : "#9CA3AF"; + const bg = () => props.status === "Active" ? "#FFF1EB" : props.status === "Pending" ? "#FFFBEB" : "#F3F4F6"; + const border = () => props.status === "Active" ? "#FFD8C2" : props.status === "Pending" ? "#FDE68A" : "#D1D5DB"; + const color = () => props.status === "Active" ? ORANGE : props.status === "Pending" ? "#A16207" : "#4B5563"; + const label = () => props.status === "Active" ? "Active" : props.status === "Pending" ? "Under Review" : props.status; return ( - - - {status} + + + {label()} ); } +/** Fetch the user's existing profile data to pass as pre-fill candidates to RoleWizard. */ +async function fetchExistingProfile(activeRoleKey: string): Promise> { + if (!activeRoleKey) return {}; + try { + const res = await apiFetch(`/api/profile?roleKey=${activeRoleKey}`); + if (!res.ok) return {}; + const data = await res.json().catch(() => ({})); + const profile: Record = data?.profile_data ?? data ?? {}; + const result: Record = {}; + for (const [key, val] of Object.entries(profile)) { + if (typeof val === "string" && val) result[key] = val; + } + return result; + } catch { + return {}; + } +} + export default function ExploreServicesPage() { - const [activeRoles, setActiveRoles] = createSignal([]); + const [roleItems, setRoleItems] = createSignal([]); const [currentRole, setCurrentRole] = createSignal(""); const [loading, setLoading] = createSignal(true); const [busyRoleKey, setBusyRoleKey] = createSignal(null); const [msg, setMsg] = createSignal(""); const [err, setErr] = createSignal(""); - const activeRoleSet = createMemo(() => new Set(activeRoles())); + // ── wizard state ────────────────────────────────────────────────────────── + const [wizardRole, setWizardRole] = createSignal(null); + const [wizardLabel, setWizardLabel] = createSignal(""); + const [sharedDocs, setSharedDocs] = createSignal>({}); + + const approvedRoleSet = createMemo(() => + new Set( + roleItems() + .filter((r) => { + const s = String(r.status || "").toUpperCase(); + return s === "APPROVED" || s === "ACTIVE" || s === ""; + }) + .map((r) => String(r.role_key || "").toUpperCase()), + ), + ); + + const pendingRoleSet = createMemo(() => + new Set( + roleItems() + .filter((r) => String(r.status || "").toUpperCase() === "PENDING") + .map((r) => String(r.role_key || "").toUpperCase()), + ), + ); const cards = createMemo((): RoleCard[] => PROFESSIONAL_ROLES.map(({ key, name, Icon }) => { - const upperKey = key; - const isRegistered = activeRoleSet().has(upperKey); - const isCurrent = currentRole() === upperKey; - let status: "Active" | "Registered" | "Available" = "Available"; - if (isCurrent) status = "Active"; - else if (isRegistered) status = "Registered"; - return { - key: upperKey, - title: name, - subtitle: isCurrent - ? "This is your current active role." - : isRegistered - ? "Role is linked to your account. Switch instantly." - : "Add this role to unlock its dashboard and workflows.", - action: isCurrent ? "Current Role" : isRegistered ? "Switch" : "Register", - Icon, - status, - }; - }) + const isCurrent = currentRole() === key; + const isApproved = approvedRoleSet().has(key); + const isPending = pendingRoleSet().has(key); + + let status: RoleCard["status"] = "Available"; + if (isCurrent) status = "Active"; + else if (isPending) status = "Pending"; + else if (isApproved) status = "Registered"; + + const action = isCurrent ? "Current Role" + : isPending ? "Under Review" + : isApproved ? "Switch" + : "Register"; + + const subtitle = isCurrent ? "This is your current active role." + : isPending ? "Submitted for verification. Awaiting admin approval." + : isApproved ? "Role is approved. Switch instantly." + : "Add this role to unlock its dashboard and workflows."; + + return { key, title: name, subtitle, action, Icon, status }; + }), ); const load = async () => { @@ -121,37 +169,36 @@ export default function ExploreServicesPage() { try { const parsed = JSON.parse(raw); setCurrentRole(String(parsed?.active_role || parsed?.role || "").toUpperCase()); - } catch { - setCurrentRole(""); - } + } catch { setCurrentRole(""); } } } - - const res = await apiFetch("/api/users/roles"); - const data = await res.json().catch(() => ({})); + // Use /api/me/roles which returns status info + const res = await apiFetch("/api/me/roles"); + const data = await res.json().catch(() => []); if (res.ok) { - const roles: string[] = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : []; - setActiveRoles(roles.map((r) => String(typeof r === "string" ? r : (r as any).role_key || (r as any).key || "").toUpperCase())); + const items: UserRoleItem[] = Array.isArray(data) ? data : []; + setRoleItems(items.map((r) => ({ + role_key: String(r.role_key || "").toUpperCase(), + status: String(r.status || "").toUpperCase(), + }))); } else { - setActiveRoles([]); + setRoleItems([]); } } catch { - setActiveRoles([]); + setRoleItems([]); } finally { setLoading(false); } }; - onMount(() => { - void load(); - }); + onMount(() => { void load(); }); - const registerRole = async (roleKey: string) => { + const registerRole = async (roleKey: string, label: string) => { setBusyRoleKey(roleKey); setMsg(""); setErr(""); try { - const res = await apiFetch("/api/users/roles/register", { + const res = await apiFetch("/api/me/roles/register", { method: "POST", body: JSON.stringify({ role_key: roleKey }), }); @@ -160,8 +207,12 @@ export default function ExploreServicesPage() { setErr(String(data?.error || data?.message || "Failed to register role.")); return; } - setMsg(`${toTitle(roleKey)} registered successfully.`); + // Fetch existing profile for identity-doc pre-fill in the wizard + const existing = await fetchExistingProfile(currentRole()); + setSharedDocs(existing); await load(); + setWizardLabel(label); + setWizardRole(roleKey); } catch { setErr("Network error while registering role."); } finally { @@ -195,22 +246,12 @@ export default function ExploreServicesPage() { 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 - } + window.localStorage.setItem(key, JSON.stringify({ ...parsed, active_role: roleKey, role: roleKey.toLowerCase(), roleKey: roleKey.toLowerCase() })); + } catch { /* ignore */ } } } setMsg(`Switched to ${toTitle(roleKey)}. Redirecting...`); - setTimeout(() => { - window.location.href = "/dashboard"; - }, 250); + setTimeout(() => { window.location.href = "/dashboard"; }, 250); } catch { setErr("Network error while switching role."); } finally { @@ -218,376 +259,183 @@ export default function ExploreServicesPage() { } }; + const exitWizard = () => { + setWizardRole(null); + setSharedDocs({}); + setWizardLabel(""); + }; + + const onWizardSubmitted = async (_status: string) => { + exitWizard(); + await load(); + setMsg("Verification submitted. We'll review and notify you once approved."); + }; + + // ── render ──────────────────────────────────────────────────────────────── return (
-
- - - -
-

+ } + > + + +
+

+ + {wizardLabel()} — Verification +

- Discover services, connect with verified users, and expand into additional roles using the same dashboard workflow. + + Complete your profile to submit for admin approval. +

- {/* Main Roles Section */} -
-

- Explore opportunities on Nxtgauge -

-
- - {(role) => { - const isCurrentRole = () => currentRole() === role.key; - const isRegistered = () => activeRoleSet().has(role.key); - return ( -
-
- -
-

- {role.name} -

-

- {role.subtitle} -

- -
- ); - }} -
-
-
- - -
- {msg()} -
-
- -
- {err()} -
+ {/* Inline wizard — shown immediately after registration */} + + - -

- Loading services... -

-
- } - > + {/* Main content — hidden while wizard is open */} + + +
{msg()}
+
+ +
{err()}
+
+ + {/* Main Roles */}
-
-

- Professional Services -

- -
- - 0} - fallback={ -
- No services available. -
- } - > -
- - {(card) => ( -
-
-
- -
- +

+ Explore opportunities on Nxtgauge +

+
+ + {(role) => { + const isCurrent = () => currentRole() === role.key; + const isApproved = () => approvedRoleSet().has(role.key); + const isPending = () => pendingRoleSet().has(role.key); + return ( +
+
+
- -
-

- {card.title} -

-

- {card.subtitle} -

-
- +

{role.name}

+

{role.subtitle}

- )} -
-
- - -
-

- Growth Advantage -

-

- Why Add More Services? -

-

- A multi-service profile helps you acquire more opportunities, improve trust, and scale consistently on a single platform. -

-
-
-
- -
-

Reach More Buyers

-

Get discovered by customers across multiple demand categories.

-
-
-
- -
-

Increase Revenue Paths

-

Offer additional services and create new income streams.

-
-
-
- -
-

Strengthen Credibilities

-

Verified multi-service profiles build confidence and improve conversion.

-
-
-
- -
-

Scale Faster

-

Grow your business from one unified account and workflow.

-
+ ); + }} +
-
+ + {/* Professional Services */} + +

Loading services...

+
+ } + > +
+
+

Professional Services

+ +
+
+ + {(card) => { + const busy = () => busyRoleKey() === card.key; + const disabled = () => busy() || card.action === "Current Role" || card.action === "Under Review"; + return ( +
+
+
+ +
+ +
+
+

{card.title}

+

{card.subtitle}

+
+ +
+ ); + }} +
+
+ + {/* Why Add More Services */} +
+

Growth Advantage

+

Why Add More Services?

+

+ A multi-service profile helps you acquire more opportunities, improve trust, and scale consistently on a single platform. +

+
+ + {(item) => ( +
+
+ +
+

{item.title}

+

{item.body}

+
+ )} +
+
+
+
+
); }