nxtgauge-frontend-solid/src/components/dashboard/ExploreServicesPage.tsx
2026-04-26 23:58:43 +02:00

596 lines
23 KiB
TypeScript

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";
const NAVY = "#0D0D2A";
const ORANGE = "#FF5E13";
const API = "/api/gateway";
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." },
];
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 },
];
type RoleCard = {
key: string;
title: string;
subtitle: string;
action: string;
Icon: any;
status: "Active" | "Registered" | "Available";
};
async function apiFetch(path: string, opts?: RequestInit) {
const token =
typeof window !== "undefined"
? window.sessionStorage.getItem("nxtgauge_access_token") || ""
: "";
const cleanPath = path.startsWith("/api/") ? path.slice(4) : path;
return fetch(`${API}${cleanPath}`, {
...opts,
credentials: "include",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(opts?.headers ?? {}),
},
});
}
function toTitle(value: string): string {
return String(value || "")
.toLowerCase()
.replace(/_/g, " ")
.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";
return (
<span
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:${isActive ? ORANGE : "#9CA3AF"};margin-right:5px;flex-shrink:0`}
/>
{status}
</span>
);
}
export default function ExploreServicesPage() {
const [activeRoles, setActiveRoles] = createSignal<string[]>([]);
const [currentRole, setCurrentRole] = createSignal("");
const [loading, setLoading] = createSignal(true);
const [busyRoleKey, setBusyRoleKey] = createSignal<string | null>(null);
const [msg, setMsg] = createSignal("");
const [err, setErr] = createSignal("");
const activeRoleSet = createMemo(() => new Set(activeRoles()));
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 load = async () => {
setLoading(true);
setErr("");
try {
if (typeof window !== "undefined") {
const raw =
window.localStorage.getItem("nxtgauge_auth_user") ||
window.localStorage.getItem("nxtgauge_user");
if (raw) {
try {
const parsed = JSON.parse(raw);
setCurrentRole(String(parsed?.active_role || parsed?.role || "").toUpperCase());
} catch {
setCurrentRole("");
}
}
}
const res = await apiFetch("/api/users/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.role_key || r.key || "").toUpperCase()));
} else {
setActiveRoles([]);
}
} catch {
setActiveRoles([]);
} finally {
setLoading(false);
}
};
onMount(() => {
void load();
});
const registerRole = async (roleKey: string) => {
setBusyRoleKey(roleKey);
setMsg("");
setErr("");
try {
const res = await apiFetch("/api/users/roles/register", {
method: "POST",
body: JSON.stringify({ role_key: roleKey }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(String(data?.error || data?.message || "Failed to register role."));
return;
}
setMsg(`${toTitle(roleKey)} registered successfully.`);
await load();
} catch {
setErr("Network error while registering role.");
} finally {
setBusyRoleKey(null);
}
};
const switchRole = async (roleKey: string) => {
setBusyRoleKey(roleKey);
setMsg("");
setErr("");
try {
const res = await apiFetch("/api/auth/switch-role", {
method: "POST",
body: JSON.stringify({ role_key: roleKey }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setErr(String(data?.error || data?.message || "Failed to switch role."));
return;
}
const accessToken = String(data?.access_token || "").trim();
if (typeof window !== "undefined" && accessToken) {
window.sessionStorage.setItem("nxtgauge_access_token", accessToken);
window.sessionStorage.setItem("nxtgauge_frontend_access_token", accessToken);
}
if (typeof window !== "undefined") {
const keys = ["nxtgauge_auth_user", "nxtgauge_user", "nxtgauge_signup_profile_v1"];
for (const key of keys) {
const raw = window.localStorage.getItem(key);
if (!raw) continue;
try {
const parsed = JSON.parse(raw);
const next = {
...parsed,
active_role: roleKey,
role: roleKey.toLowerCase(),
roleKey: roleKey.toLowerCase(),
};
window.localStorage.setItem(key, JSON.stringify(next));
} catch {
// ignore
}
}
}
setMsg(`Switched to ${toTitle(roleKey)}. Redirecting...`);
setTimeout(() => {
window.location.href = "/dashboard";
}, 250);
} catch {
setErr("Network error while switching role.");
} finally {
setBusyRoleKey(null);
}
};
return (
<div style={{ display: "grid", gap: "14px", "max-width": "1080px" }}>
<div
style={{
background: NAVY,
"border-radius": "12px",
padding: "16px 20px",
display: "flex",
"align-items": "center",
gap: "12px",
}}
>
<span style={{ color: ORANGE }}>
<Globe size={24} />
</span>
<div>
<h1
style={{
margin: "0",
"font-size": "18px",
"font-weight": "800",
color: "#fff",
"line-height": "1.2",
}}
>
Explore Nxtgauge
</h1>
<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.
</p>
</div>
</div>
{/* Main Roles Section */}
<div style={CARD}>
<p style={{ margin: "0 0 16px", "font-size": "20px", "font-weight": "800", color: "#111827" }}>
Explore opportunities on Nxtgauge
</p>
<div style={{ display: "grid", "grid-template-columns": "repeat(3, minmax(0, 1fr))", gap: "12px" }}>
<For each={MAIN_ROLES}>
{(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
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>
<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",
}}
>
{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={
busyRoleKey() === card.key || card.action === "Current Role"
}
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 ||
card.action === "Current Role"
? "not-allowed"
: "pointer",
opacity:
busyRoleKey() === card.key ||
card.action === "Current Role"
? "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 ||
card.action === "Current Role"
? "not-allowed"
: "pointer",
opacity:
busyRoleKey() === card.key ||
card.action === "Current Role"
? "0.6"
: "1",
"margin-top": "auto",
}
}
>
{busyRoleKey() === card.key
? card.action === "Register"
? "Registering..."
: "Switching..."
: card.action}
</button>
</div>
)}
</For>
</div>
</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>
</Show>
</div>
);
}