feat: add inline role-registration wizard to ExploreServicesPage
All checks were successful
build-and-release / build (push) Successful in 1m48s
All checks were successful
build-and-release / build (push) Successful in 1m48s
- After registerRole() succeeds, fetch existing profile for identity doc pre-fill and immediately launch RoleWizard inline (same pattern as SwitchServicesPage) - Wizard header shows role name + back arrow; main card grid hides - On wizard submission: show approval-pending message and reload roles - Card status now uses /api/me/roles (returns status field) so Pending roles show 'Under Review' badge instead of 'Switch' button - Main roles (Company, Job Seeker, Customer) also go through the wizard - StatusBadge component uses reactive accessor thunks (fixes solid/reactivity warning) - Remove unused BTN_PRIMARY import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09a5c36310
commit
7b28301793
1 changed files with 275 additions and 427 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
import { For, Show, createMemo, createSignal, onMount } from "solid-js";
|
||||||
import { BTN_GHOST, BTN_PRIMARY, CARD } from "~/components/DashboardShell";
|
import { ArrowLeft, Camera, Scissors, GraduationCap, Code2, Clapperboard, PenTool, Megaphone, Dumbbell, UtensilsCrossed, Globe, Users, UserCircle, FileText, TrendingUp, Award, BarChart3, ShieldCheck } from "lucide-solid";
|
||||||
import { Camera, Scissors, GraduationCap, Code2, Clapperboard, PenTool, Megaphone, Dumbbell, UtensilsCrossed, Briefcase, 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 NAVY = "#0D0D2A";
|
||||||
const ORANGE = "#FF5E13";
|
const ORANGE = "#FF5E13";
|
||||||
|
|
@ -8,31 +10,37 @@ const ORANGE = "#FF5E13";
|
||||||
const API = "";
|
const API = "";
|
||||||
|
|
||||||
const MAIN_ROLES = [
|
const MAIN_ROLES = [
|
||||||
{ key: "COMPANY", name: "Company", Icon: Users, subtitle: "Hire talent, post jobs, and manage applications in one path." },
|
{ 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: "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: "CUSTOMER", name: "Service Seeker", Icon: FileText, subtitle: "Post requirements and connect with verified professionals." },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PROFESSIONAL_ROLES = [
|
const PROFESSIONAL_ROLES = [
|
||||||
{ key: "PHOTOGRAPHER", name: "Photographer", Icon: Camera },
|
{ key: "PHOTOGRAPHER", name: "Photographer", Icon: Camera },
|
||||||
{ key: "MAKEUP_ARTIST", name: "Makeup Artist", Icon: Scissors },
|
{ key: "MAKEUP_ARTIST", name: "Makeup Artist", Icon: Scissors },
|
||||||
{ key: "TUTOR", name: "Tutor", Icon: GraduationCap },
|
{ key: "TUTOR", name: "Tutor", Icon: GraduationCap },
|
||||||
{ key: "DEVELOPER", name: "Developer", Icon: Code2 },
|
{ key: "DEVELOPER", name: "Developer", Icon: Code2 },
|
||||||
{ key: "VIDEO_EDITOR", name: "Video Editor", Icon: Clapperboard },
|
{ key: "VIDEO_EDITOR", name: "Video Editor", Icon: Clapperboard },
|
||||||
{ key: "UGC_CONTENT_CREATOR", name: "UGC Content Creator", Icon: Clapperboard },
|
{ key: "UGC_CONTENT_CREATOR", name: "UGC Content Creator", Icon: Clapperboard },
|
||||||
{ key: "GRAPHIC_DESIGNER", name: "Graphic Designer", Icon: PenTool },
|
{ key: "GRAPHIC_DESIGNER", name: "Graphic Designer", Icon: PenTool },
|
||||||
{ key: "SOCIAL_MEDIA_MANAGER", name: "Social Media Manager", Icon: Megaphone },
|
{ key: "SOCIAL_MEDIA_MANAGER", name: "Social Media Manager", Icon: Megaphone },
|
||||||
{ key: "FITNESS_TRAINER", name: "Fitness Trainer", Icon: Dumbbell },
|
{ key: "FITNESS_TRAINER", name: "Fitness Trainer", Icon: Dumbbell },
|
||||||
{ key: "CATERING_SERVICES", name: "Catering Services", Icon: UtensilsCrossed },
|
{ key: "CATERING_SERVICES", name: "Catering Services", Icon: UtensilsCrossed },
|
||||||
];
|
];
|
||||||
|
|
||||||
type RoleCard = {
|
type RoleCard = {
|
||||||
key: string;
|
key: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
/** "Register" | "Switch" | "Current Role" | "Under Review" */
|
||||||
action: string;
|
action: string;
|
||||||
Icon: any;
|
Icon: (props: { size: number; color: string; strokeWidth: number }) => unknown;
|
||||||
status: "Active" | "Registered" | "Available";
|
status: "Active" | "Pending" | "Registered" | "Available";
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserRoleItem = {
|
||||||
|
role_key: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function apiFetch(path: string, opts?: RequestInit) {
|
async function apiFetch(path: string, opts?: RequestInit) {
|
||||||
|
|
@ -58,53 +66,93 @@ function toTitle(value: string): string {
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusBadge(props: { status: "Active" | "Registered" | "Available" }) {
|
function StatusBadge(props: { status: "Active" | "Pending" | "Registered" | "Available" }) {
|
||||||
const { status } = props;
|
const dotColor = () => props.status === "Active" ? ORANGE : props.status === "Pending" ? "#F59E0B" : "#9CA3AF";
|
||||||
const isActive = status === "Active";
|
const bg = () => props.status === "Active" ? "#FFF1EB" : props.status === "Pending" ? "#FFFBEB" : "#F3F4F6";
|
||||||
const isRegistered = status === "Registered";
|
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 (
|
return (
|
||||||
<span
|
<span style={{ display: "inline-flex", "align-items": "center", "border-radius": "9999px", border: `1px solid ${border()}`, background: bg(), color: color(), padding: "2px 10px", "font-size": "12px", "font-weight": "500" }}>
|
||||||
style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${isActive ? "#FFD8C2" : "#D1D5DB"};background:${isActive ? "#FFF1EB" : isRegistered ? "#F3F4F6" : "#F3F4F6"};color:${isActive ? ORANGE : isRegistered ? "#4B5563" : "#4B5563"};padding:2px 10px;font-size:12px;font-weight:500`}
|
<span style={{ display: "inline-block", width: "6px", height: "6px", "border-radius": "50%", background: dotColor(), "margin-right": "5px", "flex-shrink": "0" }} />
|
||||||
>
|
{label()}
|
||||||
<span
|
|
||||||
style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${isActive ? ORANGE : "#9CA3AF"};margin-right:5px;flex-shrink:0`}
|
|
||||||
/>
|
|
||||||
{status}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch the user's existing profile data to pass as pre-fill candidates to RoleWizard. */
|
||||||
|
async function fetchExistingProfile(activeRoleKey: string): Promise<Record<string, string>> {
|
||||||
|
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<string, unknown> = data?.profile_data ?? data ?? {};
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const [key, val] of Object.entries(profile)) {
|
||||||
|
if (typeof val === "string" && val) result[key] = val;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ExploreServicesPage() {
|
export default function ExploreServicesPage() {
|
||||||
const [activeRoles, setActiveRoles] = createSignal<string[]>([]);
|
const [roleItems, setRoleItems] = createSignal<UserRoleItem[]>([]);
|
||||||
const [currentRole, setCurrentRole] = createSignal("");
|
const [currentRole, setCurrentRole] = createSignal("");
|
||||||
const [loading, setLoading] = createSignal(true);
|
const [loading, setLoading] = createSignal(true);
|
||||||
const [busyRoleKey, setBusyRoleKey] = createSignal<string | null>(null);
|
const [busyRoleKey, setBusyRoleKey] = createSignal<string | null>(null);
|
||||||
const [msg, setMsg] = createSignal("");
|
const [msg, setMsg] = createSignal("");
|
||||||
const [err, setErr] = createSignal("");
|
const [err, setErr] = createSignal("");
|
||||||
|
|
||||||
const activeRoleSet = createMemo(() => new Set(activeRoles()));
|
// ── wizard state ──────────────────────────────────────────────────────────
|
||||||
|
const [wizardRole, setWizardRole] = createSignal<string | null>(null);
|
||||||
|
const [wizardLabel, setWizardLabel] = createSignal("");
|
||||||
|
const [sharedDocs, setSharedDocs] = createSignal<Record<string, string>>({});
|
||||||
|
|
||||||
|
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[] =>
|
const cards = createMemo((): RoleCard[] =>
|
||||||
PROFESSIONAL_ROLES.map(({ key, name, Icon }) => {
|
PROFESSIONAL_ROLES.map(({ key, name, Icon }) => {
|
||||||
const upperKey = key;
|
const isCurrent = currentRole() === key;
|
||||||
const isRegistered = activeRoleSet().has(upperKey);
|
const isApproved = approvedRoleSet().has(key);
|
||||||
const isCurrent = currentRole() === upperKey;
|
const isPending = pendingRoleSet().has(key);
|
||||||
let status: "Active" | "Registered" | "Available" = "Available";
|
|
||||||
if (isCurrent) status = "Active";
|
let status: RoleCard["status"] = "Available";
|
||||||
else if (isRegistered) status = "Registered";
|
if (isCurrent) status = "Active";
|
||||||
return {
|
else if (isPending) status = "Pending";
|
||||||
key: upperKey,
|
else if (isApproved) status = "Registered";
|
||||||
title: name,
|
|
||||||
subtitle: isCurrent
|
const action = isCurrent ? "Current Role"
|
||||||
? "This is your current active role."
|
: isPending ? "Under Review"
|
||||||
: isRegistered
|
: isApproved ? "Switch"
|
||||||
? "Role is linked to your account. Switch instantly."
|
: "Register";
|
||||||
: "Add this role to unlock its dashboard and workflows.",
|
|
||||||
action: isCurrent ? "Current Role" : isRegistered ? "Switch" : "Register",
|
const subtitle = isCurrent ? "This is your current active role."
|
||||||
Icon,
|
: isPending ? "Submitted for verification. Awaiting admin approval."
|
||||||
status,
|
: 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 () => {
|
const load = async () => {
|
||||||
|
|
@ -121,37 +169,36 @@ export default function ExploreServicesPage() {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
setCurrentRole(String(parsed?.active_role || parsed?.role || "").toUpperCase());
|
setCurrentRole(String(parsed?.active_role || parsed?.role || "").toUpperCase());
|
||||||
} catch {
|
} catch { setCurrentRole(""); }
|
||||||
setCurrentRole("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Use /api/me/roles which returns status info
|
||||||
const res = await apiFetch("/api/users/roles");
|
const res = await apiFetch("/api/me/roles");
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => []);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const roles: string[] = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : [];
|
const items: UserRoleItem[] = Array.isArray(data) ? data : [];
|
||||||
setActiveRoles(roles.map((r) => String(typeof r === "string" ? r : (r as any).role_key || (r as any).key || "").toUpperCase()));
|
setRoleItems(items.map((r) => ({
|
||||||
|
role_key: String(r.role_key || "").toUpperCase(),
|
||||||
|
status: String(r.status || "").toUpperCase(),
|
||||||
|
})));
|
||||||
} else {
|
} else {
|
||||||
setActiveRoles([]);
|
setRoleItems([]);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setActiveRoles([]);
|
setRoleItems([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => { void load(); });
|
||||||
void load();
|
|
||||||
});
|
|
||||||
|
|
||||||
const registerRole = async (roleKey: string) => {
|
const registerRole = async (roleKey: string, label: string) => {
|
||||||
setBusyRoleKey(roleKey);
|
setBusyRoleKey(roleKey);
|
||||||
setMsg("");
|
setMsg("");
|
||||||
setErr("");
|
setErr("");
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch("/api/users/roles/register", {
|
const res = await apiFetch("/api/me/roles/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ role_key: roleKey }),
|
body: JSON.stringify({ role_key: roleKey }),
|
||||||
});
|
});
|
||||||
|
|
@ -160,8 +207,12 @@ export default function ExploreServicesPage() {
|
||||||
setErr(String(data?.error || data?.message || "Failed to register role."));
|
setErr(String(data?.error || data?.message || "Failed to register role."));
|
||||||
return;
|
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();
|
await load();
|
||||||
|
setWizardLabel(label);
|
||||||
|
setWizardRole(roleKey);
|
||||||
} catch {
|
} catch {
|
||||||
setErr("Network error while registering role.");
|
setErr("Network error while registering role.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -195,22 +246,12 @@ export default function ExploreServicesPage() {
|
||||||
if (!raw) continue;
|
if (!raw) continue;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const next = {
|
window.localStorage.setItem(key, JSON.stringify({ ...parsed, active_role: roleKey, role: roleKey.toLowerCase(), roleKey: roleKey.toLowerCase() }));
|
||||||
...parsed,
|
} catch { /* ignore */ }
|
||||||
active_role: roleKey,
|
|
||||||
role: roleKey.toLowerCase(),
|
|
||||||
roleKey: roleKey.toLowerCase(),
|
|
||||||
};
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(next));
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setMsg(`Switched to ${toTitle(roleKey)}. Redirecting...`);
|
setMsg(`Switched to ${toTitle(roleKey)}. Redirecting...`);
|
||||||
setTimeout(() => {
|
setTimeout(() => { window.location.href = "/dashboard"; }, 250);
|
||||||
window.location.href = "/dashboard";
|
|
||||||
}, 250);
|
|
||||||
} catch {
|
} catch {
|
||||||
setErr("Network error while switching role.");
|
setErr("Network error while switching role.");
|
||||||
} finally {
|
} 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 (
|
return (
|
||||||
<div style={{ display: "grid", gap: "14px", "max-width": "1080px" }}>
|
<div style={{ display: "grid", gap: "14px", "max-width": "1080px" }}>
|
||||||
<div
|
{/* Header */}
|
||||||
style={{
|
<div style={{ background: NAVY, "border-radius": "12px", padding: "16px 20px", display: "flex", "align-items": "center", gap: "12px" }}>
|
||||||
background: NAVY,
|
<Show
|
||||||
"border-radius": "12px",
|
when={wizardRole()}
|
||||||
padding: "16px 20px",
|
fallback={<span style={{ color: ORANGE }}><Globe size={24} /></span>}
|
||||||
display: "flex",
|
>
|
||||||
"align-items": "center",
|
<button
|
||||||
gap: "12px",
|
type="button"
|
||||||
}}
|
onClick={exitWizard}
|
||||||
>
|
style={{ background: "none", border: "none", cursor: "pointer", color: ORANGE, display: "flex", "align-items": "center", padding: "0" }}
|
||||||
<span style={{ color: ORANGE }}>
|
aria-label="Back to Explore Services"
|
||||||
<Globe size={24} />
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h1
|
|
||||||
style={{
|
|
||||||
margin: "0",
|
|
||||||
"font-size": "18px",
|
|
||||||
"font-weight": "800",
|
|
||||||
color: "#fff",
|
|
||||||
"line-height": "1.2",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Explore Nxtgauge
|
<ArrowLeft size={24} />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<div>
|
||||||
|
<h1 style={{ margin: "0", "font-size": "18px", "font-weight": "800", color: "#fff", "line-height": "1.2" }}>
|
||||||
|
<Show when={wizardRole()} fallback="Explore Nxtgauge">
|
||||||
|
{wizardLabel()} — Verification
|
||||||
|
</Show>
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "rgba(255,255,255,0.65)" }}>
|
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "rgba(255,255,255,0.65)" }}>
|
||||||
Discover services, connect with verified users, and expand into additional roles using the same dashboard workflow.
|
<Show when={wizardRole()} fallback="Discover services, connect with verified users, and expand into additional roles.">
|
||||||
|
Complete your profile to submit for admin approval.
|
||||||
|
</Show>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Roles Section */}
|
{/* Inline wizard — shown immediately after registration */}
|
||||||
<div style={CARD}>
|
<Show when={wizardRole()}>
|
||||||
<p style={{ margin: "0 0 16px", "font-size": "20px", "font-weight": "800", color: "#111827" }}>
|
<RoleWizard
|
||||||
Explore opportunities on Nxtgauge
|
roleKey={wizardRole()!}
|
||||||
</p>
|
rolePrefix={roleKeyToPrefix(wizardRole()!)}
|
||||||
<div style={{ display: "grid", "grid-template-columns": "repeat(3, minmax(0, 1fr))", gap: "12px" }}>
|
prefilledDocs={sharedDocs()}
|
||||||
<For each={MAIN_ROLES}>
|
onSubmitted={onWizardSubmitted}
|
||||||
{(role) => {
|
/>
|
||||||
const isCurrentRole = () => currentRole() === role.key;
|
|
||||||
const isRegistered = () => activeRoleSet().has(role.key);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
background: "#fff",
|
|
||||||
"border-radius": "16px",
|
|
||||||
padding: "16px",
|
|
||||||
display: "flex",
|
|
||||||
"flex-direction": "column",
|
|
||||||
gap: "10px",
|
|
||||||
"box-shadow": "0 1px 4px rgba(0,0,0,0.06)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "40px",
|
|
||||||
height: "40px",
|
|
||||||
"border-radius": "10px",
|
|
||||||
background: "#FFF3EE",
|
|
||||||
display: "flex",
|
|
||||||
"align-items": "center",
|
|
||||||
"justify-content": "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<role.Icon size={20} color={ORANGE} strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<p style={{ margin: "0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>
|
|
||||||
{role.name}
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280", "line-height": "1.5" }}>
|
|
||||||
{role.subtitle}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={isCurrentRole()}
|
|
||||||
onClick={() => isRegistered() && !isCurrentRole() ? void switchRole(role.key) : void registerRole(role.key)}
|
|
||||||
style={{
|
|
||||||
height: "32px",
|
|
||||||
"border-radius": "8px",
|
|
||||||
border: "none",
|
|
||||||
background: isCurrentRole() ? "#E5E7EB" : NAVY,
|
|
||||||
color: isCurrentRole() ? "#4B5563" : "#fff",
|
|
||||||
padding: "0 10px",
|
|
||||||
"font-size": "12px",
|
|
||||||
"font-weight": "700",
|
|
||||||
cursor: isCurrentRole() ? "not-allowed" : "pointer",
|
|
||||||
"margin-top": "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isCurrentRole() ? "Current Role" : isRegistered() ? "Switch" : `Register as ${role.name}`}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={msg()}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...CARD,
|
|
||||||
border: "1px solid #BBF7D0",
|
|
||||||
background: "#ECFDF5",
|
|
||||||
padding: "12px 16px",
|
|
||||||
color: "#065F46",
|
|
||||||
"font-size": "13px",
|
|
||||||
"font-weight": "600",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{msg()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={err()}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...CARD,
|
|
||||||
border: "1px solid #FECACA",
|
|
||||||
background: "#FEF2F2",
|
|
||||||
padding: "12px 16px",
|
|
||||||
color: "#B91C1C",
|
|
||||||
"font-size": "13px",
|
|
||||||
"font-weight": "600",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{err()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show
|
{/* Main content — hidden while wizard is open */}
|
||||||
when={!loading()}
|
<Show when={!wizardRole()}>
|
||||||
fallback={
|
<Show when={msg()}>
|
||||||
<div
|
<div style={{ ...CARD, border: "1px solid #BBF7D0", background: "#ECFDF5", padding: "12px 16px", color: "#065F46", "font-size": "13px", "font-weight": "600" }}>{msg()}</div>
|
||||||
style={{
|
</Show>
|
||||||
...CARD,
|
<Show when={err()}>
|
||||||
"text-align": "center",
|
<div style={{ ...CARD, border: "1px solid #FECACA", background: "#FEF2F2", padding: "12px 16px", color: "#B91C1C", "font-size": "13px", "font-weight": "600" }}>{err()}</div>
|
||||||
padding: "48px 24px",
|
</Show>
|
||||||
}}
|
|
||||||
>
|
{/* Main Roles */}
|
||||||
<p style={{ margin: "0", "font-size": "15px", "font-weight": "600", color: "#111827" }}>
|
|
||||||
Loading services...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div style={CARD}>
|
<div style={CARD}>
|
||||||
<div
|
<p style={{ margin: "0 0 16px", "font-size": "20px", "font-weight": "800", color: "#111827" }}>
|
||||||
style={{
|
Explore opportunities on Nxtgauge
|
||||||
display: "flex",
|
</p>
|
||||||
"justify-content": "space-between",
|
<div style={{ display: "grid", "grid-template-columns": "repeat(3, minmax(0, 1fr))", gap: "12px" }}>
|
||||||
"align-items": "center",
|
<For each={MAIN_ROLES}>
|
||||||
"margin-bottom": "16px",
|
{(role) => {
|
||||||
}}
|
const isCurrent = () => currentRole() === role.key;
|
||||||
>
|
const isApproved = () => approvedRoleSet().has(role.key);
|
||||||
<p
|
const isPending = () => pendingRoleSet().has(role.key);
|
||||||
style={{
|
return (
|
||||||
margin: "0",
|
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "16px", padding: "16px", display: "flex", "flex-direction": "column", gap: "10px", "box-shadow": "0 1px 4px rgba(0,0,0,0.06)" }}>
|
||||||
"font-size": "16px",
|
<div style={{ width: "40px", height: "40px", "border-radius": "10px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center" }}>
|
||||||
"font-weight": "700",
|
<role.Icon size={20} color={ORANGE} strokeWidth={2} />
|
||||||
color: "#111827",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Professional Services
|
|
||||||
</p>
|
|
||||||
<button type="button" onClick={() => void load()} style={BTN_GHOST}>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={cards().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
"text-align": "center",
|
|
||||||
padding: "32px",
|
|
||||||
color: "#6B7280",
|
|
||||||
"font-size": "14px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
No services available.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
"grid-template-columns": "repeat(auto-fill, minmax(280px, 1fr))",
|
|
||||||
gap: "14px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<For each={cards()}>
|
|
||||||
{(card) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
background: "#fff",
|
|
||||||
"border-radius": "16px",
|
|
||||||
padding: "16px",
|
|
||||||
display: "flex",
|
|
||||||
"flex-direction": "column",
|
|
||||||
gap: "10px",
|
|
||||||
"box-shadow": "0 1px 4px rgba(0,0,0,0.06)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
"align-items": "flex-start",
|
|
||||||
"justify-content": "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "40px",
|
|
||||||
height: "40px",
|
|
||||||
"border-radius": "10px",
|
|
||||||
background: "#FFF3EE",
|
|
||||||
display: "flex",
|
|
||||||
"align-items": "center",
|
|
||||||
"justify-content": "center",
|
|
||||||
"flex-shrink": "0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<card.Icon size={20} color={ORANGE} strokeWidth={2} />
|
|
||||||
</div>
|
|
||||||
<StatusBadge status={card.status} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<p style={{ margin: "0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>{role.name}</p>
|
||||||
<div>
|
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280", "line-height": "1.5" }}>{role.subtitle}</p>
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: "0",
|
|
||||||
"font-size": "16px",
|
|
||||||
"font-weight": "700",
|
|
||||||
color: "#111827",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{card.title}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: "4px 0 0",
|
|
||||||
"font-size": "12px",
|
|
||||||
color: "#6B7280",
|
|
||||||
"line-height": "1.5",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{card.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={
|
disabled={isCurrent() || isPending() || busyRoleKey() === role.key}
|
||||||
busyRoleKey() === card.key || card.action === "Current Role"
|
onClick={() => isApproved() && !isCurrent() ? void switchRole(role.key) : void registerRole(role.key, role.name)}
|
||||||
}
|
style={{ height: "32px", "border-radius": "8px", border: "none", background: isCurrent() || isPending() ? "#E5E7EB" : NAVY, color: isCurrent() || isPending() ? "#4B5563" : "#fff", padding: "0 10px", "font-size": "12px", "font-weight": "700", cursor: isCurrent() || isPending() ? "not-allowed" : "pointer", "margin-top": "auto" }}
|
||||||
onClick={() =>
|
|
||||||
card.action === "Register"
|
|
||||||
? void registerRole(card.key)
|
|
||||||
: void switchRole(card.key)
|
|
||||||
}
|
|
||||||
style={
|
|
||||||
card.action === "Register"
|
|
||||||
? {
|
|
||||||
height: "34px",
|
|
||||||
"border-radius": "8px",
|
|
||||||
border: "none",
|
|
||||||
background: NAVY,
|
|
||||||
color: "#fff",
|
|
||||||
padding: "0 14px",
|
|
||||||
"font-size": "12px",
|
|
||||||
"font-weight": "700",
|
|
||||||
cursor:
|
|
||||||
busyRoleKey() === card.key
|
|
||||||
? "not-allowed"
|
|
||||||
: "pointer",
|
|
||||||
opacity:
|
|
||||||
busyRoleKey() === card.key
|
|
||||||
? "0.6"
|
|
||||||
: "1",
|
|
||||||
"margin-top": "auto",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
height: "34px",
|
|
||||||
"border-radius": "8px",
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
background: "#fff",
|
|
||||||
color: "#374151",
|
|
||||||
padding: "0 14px",
|
|
||||||
"font-size": "12px",
|
|
||||||
"font-weight": "700",
|
|
||||||
cursor:
|
|
||||||
busyRoleKey() === card.key
|
|
||||||
? "not-allowed"
|
|
||||||
: "pointer",
|
|
||||||
opacity:
|
|
||||||
busyRoleKey() === card.key
|
|
||||||
? "0.6"
|
|
||||||
: "1",
|
|
||||||
"margin-top": "auto",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{busyRoleKey() === card.key
|
{isCurrent() ? "Current Role" : isPending() ? "Under Review" : isApproved() ? "Switch" : `Register as ${role.name}`}
|
||||||
? card.action === "Register"
|
|
||||||
? "Registering..."
|
|
||||||
: "Switching..."
|
|
||||||
: card.action}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</For>
|
}}
|
||||||
</div>
|
</For>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
background: "linear-gradient(180deg, #FFFFFF 0%, #FFFAF7 100%)",
|
|
||||||
"border-radius": "20px",
|
|
||||||
padding: "18px",
|
|
||||||
"box-shadow": "0 8px 20px rgba(15,23,42,0.06)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p style={{ margin: "0", "font-size": "12px", "letter-spacing": "0.08em", "text-transform": "uppercase", "font-weight": "700", color: ORANGE, "text-align": "center" }}>
|
|
||||||
Growth Advantage
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: "4px 0 0", "font-size": "24px", "font-weight": "800", color: "#111827", "text-align": "center", "line-height": "1.1" }}>
|
|
||||||
Why Add More Services?
|
|
||||||
</p>
|
|
||||||
<p style={{ margin: "8px auto 0", "font-size": "13px", "line-height": "1.5", color: "#6B7280", "text-align": "center", "max-width": "760px" }}>
|
|
||||||
A multi-service profile helps you acquire more opportunities, improve trust, and scale consistently on a single platform.
|
|
||||||
</p>
|
|
||||||
<div style={{ display: "grid", "grid-template-columns": "repeat(4, minmax(0, 1fr))", gap: "12px", "margin-top": "14px" }}>
|
|
||||||
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "14px", padding: "14px", "box-shadow": "0 2px 6px rgba(0,0,0,0.05)" }}>
|
|
||||||
<div style={{ width: "34px", height: "34px", "border-radius": "999px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center" }}>
|
|
||||||
<TrendingUp size={16} color={ORANGE} />
|
|
||||||
</div>
|
|
||||||
<p style={{ margin: "10px 0 0", "font-size": "15px", "font-weight": "800", color: "#111827" }}>Reach More Buyers</p>
|
|
||||||
<p style={{ margin: "6px 0 0", "font-size": "12px", color: "#6B7280", "line-height": "1.45" }}>Get discovered by customers across multiple demand categories.</p>
|
|
||||||
</div>
|
|
||||||
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "14px", padding: "14px", "box-shadow": "0 2px 6px rgba(0,0,0,0.05)" }}>
|
|
||||||
<div style={{ width: "34px", height: "34px", "border-radius": "999px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center" }}>
|
|
||||||
<Award size={16} color={ORANGE} />
|
|
||||||
</div>
|
|
||||||
<p style={{ margin: "10px 0 0", "font-size": "15px", "font-weight": "800", color: "#111827" }}>Increase Revenue Paths</p>
|
|
||||||
<p style={{ margin: "6px 0 0", "font-size": "12px", color: "#6B7280", "line-height": "1.45" }}>Offer additional services and create new income streams.</p>
|
|
||||||
</div>
|
|
||||||
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "14px", padding: "14px", "box-shadow": "0 2px 6px rgba(0,0,0,0.05)" }}>
|
|
||||||
<div style={{ width: "34px", height: "34px", "border-radius": "999px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center" }}>
|
|
||||||
<ShieldCheck size={16} color={ORANGE} />
|
|
||||||
</div>
|
|
||||||
<p style={{ margin: "10px 0 0", "font-size": "15px", "font-weight": "800", color: "#111827" }}>Strengthen Credibilities</p>
|
|
||||||
<p style={{ margin: "6px 0 0", "font-size": "12px", color: "#6B7280", "line-height": "1.45" }}>Verified multi-service profiles build confidence and improve conversion.</p>
|
|
||||||
</div>
|
|
||||||
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "14px", padding: "14px", "box-shadow": "0 2px 6px rgba(0,0,0,0.05)" }}>
|
|
||||||
<div style={{ width: "34px", height: "34px", "border-radius": "999px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center" }}>
|
|
||||||
<BarChart3 size={16} color={ORANGE} />
|
|
||||||
</div>
|
|
||||||
<p style={{ margin: "10px 0 0", "font-size": "15px", "font-weight": "800", color: "#111827" }}>Scale Faster</p>
|
|
||||||
<p style={{ margin: "6px 0 0", "font-size": "12px", color: "#6B7280", "line-height": "1.45" }}>Grow your business from one unified account and workflow.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Professional Services */}
|
||||||
|
<Show
|
||||||
|
when={!loading()}
|
||||||
|
fallback={
|
||||||
|
<div style={{ ...CARD, "text-align": "center", padding: "48px 24px" }}>
|
||||||
|
<p style={{ margin: "0", "font-size": "15px", "font-weight": "600", color: "#111827" }}>Loading services...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={CARD}>
|
||||||
|
<div style={{ display: "flex", "justify-content": "space-between", "align-items": "center", "margin-bottom": "16px" }}>
|
||||||
|
<p style={{ margin: "0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>Professional Services</p>
|
||||||
|
<button type="button" onClick={() => void load()} style={BTN_GHOST}>Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "grid", "grid-template-columns": "repeat(auto-fill, minmax(280px, 1fr))", gap: "14px" }}>
|
||||||
|
<For each={cards()}>
|
||||||
|
{(card) => {
|
||||||
|
const busy = () => busyRoleKey() === card.key;
|
||||||
|
const disabled = () => busy() || card.action === "Current Role" || card.action === "Under Review";
|
||||||
|
return (
|
||||||
|
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "16px", padding: "16px", display: "flex", "flex-direction": "column", gap: "10px", "box-shadow": "0 1px 4px rgba(0,0,0,0.06)" }}>
|
||||||
|
<div style={{ display: "flex", "align-items": "flex-start", "justify-content": "space-between" }}>
|
||||||
|
<div style={{ width: "40px", height: "40px", "border-radius": "10px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center", "flex-shrink": "0" }}>
|
||||||
|
<card.Icon size={20} color={ORANGE} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={card.status} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style={{ margin: "0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>{card.title}</p>
|
||||||
|
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280", "line-height": "1.5" }}>{card.subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled()}
|
||||||
|
onClick={() => card.action === "Register" ? void registerRole(card.key, card.title) : card.action === "Switch" ? void switchRole(card.key) : undefined}
|
||||||
|
style={{
|
||||||
|
height: "34px", "border-radius": "8px", border: card.action === "Register" ? "none" : "1px solid #E5E7EB",
|
||||||
|
background: card.action === "Register" ? NAVY : disabled() ? "#F3F4F6" : "#fff",
|
||||||
|
color: card.action === "Register" ? "#fff" : disabled() ? "#9CA3AF" : "#374151",
|
||||||
|
padding: "0 14px", "font-size": "12px", "font-weight": "700",
|
||||||
|
cursor: disabled() ? "not-allowed" : "pointer",
|
||||||
|
opacity: busy() ? "0.6" : "1", "margin-top": "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{busy() ? (card.action === "Register" ? "Registering..." : "Switching...") : card.action}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Why Add More Services */}
|
||||||
|
<div style={{ border: "1px solid #E5E7EB", background: "linear-gradient(180deg, #FFFFFF 0%, #FFFAF7 100%)", "border-radius": "20px", padding: "18px", "box-shadow": "0 8px 20px rgba(15,23,42,0.06)", "margin-top": "14px" }}>
|
||||||
|
<p style={{ margin: "0", "font-size": "12px", "letter-spacing": "0.08em", "text-transform": "uppercase", "font-weight": "700", color: ORANGE, "text-align": "center" }}>Growth Advantage</p>
|
||||||
|
<p style={{ margin: "4px 0 0", "font-size": "24px", "font-weight": "800", color: "#111827", "text-align": "center", "line-height": "1.1" }}>Why Add More Services?</p>
|
||||||
|
<p style={{ margin: "8px auto 0", "font-size": "13px", "line-height": "1.5", color: "#6B7280", "text-align": "center", "max-width": "760px" }}>
|
||||||
|
A multi-service profile helps you acquire more opportunities, improve trust, and scale consistently on a single platform.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: "grid", "grid-template-columns": "repeat(4, minmax(0, 1fr))", gap: "12px", "margin-top": "14px" }}>
|
||||||
|
<For each={[
|
||||||
|
{ Icon: TrendingUp, title: "Reach More Buyers", body: "Get discovered by customers across multiple demand categories." },
|
||||||
|
{ Icon: Award, title: "Increase Revenue Paths", body: "Offer additional services and create new income streams." },
|
||||||
|
{ Icon: ShieldCheck,title: "Strengthen Credibilities",body: "Verified multi-service profiles build confidence and improve conversion." },
|
||||||
|
{ Icon: BarChart3, title: "Scale Faster", body: "Grow your business from one unified account and workflow." },
|
||||||
|
]}>
|
||||||
|
{(item) => (
|
||||||
|
<div style={{ border: "1px solid #E5E7EB", background: "#fff", "border-radius": "14px", padding: "14px", "box-shadow": "0 2px 6px rgba(0,0,0,0.05)" }}>
|
||||||
|
<div style={{ width: "34px", height: "34px", "border-radius": "999px", background: "#FFF3EE", display: "flex", "align-items": "center", "justify-content": "center" }}>
|
||||||
|
<item.Icon size={16} color={ORANGE} />
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: "10px 0 0", "font-size": "15px", "font-weight": "800", color: "#111827" }}>{item.title}</p>
|
||||||
|
<p style={{ margin: "6px 0 0", "font-size": "12px", color: "#6B7280", "line-height": "1.45" }}>{item.body}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue