diff --git a/frontend-solid.dev.log b/frontend-solid.dev.log
new file mode 100644
index 0000000..e69de29
diff --git a/frontend-solid.dev.pid b/frontend-solid.dev.pid
new file mode 100644
index 0000000..3813b54
--- /dev/null
+++ b/frontend-solid.dev.pid
@@ -0,0 +1 @@
+7995
diff --git a/src/components/dashboard/CustomerRequirementsPage.tsx b/src/components/dashboard/CustomerRequirementsPage.tsx
index 7def79c..0891cad 100644
--- a/src/components/dashboard/CustomerRequirementsPage.tsx
+++ b/src/components/dashboard/CustomerRequirementsPage.tsx
@@ -16,10 +16,9 @@ type RequirementItem = {
title: string;
description?: string | null;
status?: string;
- budget_min?: number | null;
- budget_max?: number | null;
+ budget_inr?: number | null;
area?: string | null;
- city?: string | null;
+ location?: string | null;
created_at?: string;
};
@@ -44,7 +43,7 @@ export default function CustomerRequirementsPage() {
budget_min: "",
budget_max: "",
area: "",
- city: "",
+ location: "",
});
const loadRequirements = async () => {
@@ -79,10 +78,12 @@ export default function CustomerRequirementsPage() {
const payload = {
title: form().title.trim(),
description: form().description.trim() || undefined,
- budget_min: form().budget_min ? Number(form().budget_min) : undefined,
- budget_max: form().budget_max ? Number(form().budget_max) : undefined,
+ budget_inr:
+ form().budget_min || form().budget_max
+ ? Number(form().budget_min) || Number(form().budget_max)
+ : undefined,
area: form().area.trim() || undefined,
- city: form().city.trim() || undefined,
+ location: form().city.trim() || undefined,
};
const res = await apiFetch("/api/customers/requirements", {
method: "POST",
@@ -94,7 +95,14 @@ export default function CustomerRequirementsPage() {
return;
}
setMsg("Requirement created.");
- setForm({ title: "", description: "", budget_min: "", budget_max: "", area: "", city: "" });
+ setForm({
+ title: "",
+ description: "",
+ budget_min: "",
+ budget_max: "",
+ area: "",
+ location: "",
+ });
await loadRequirements();
} catch {
setErr("Network error while creating requirement.");
@@ -193,10 +201,10 @@ export default function CustomerRequirementsPage() {
/>
-
+
setField("city", e.currentTarget.value)}
+ value={form().location}
+ onInput={(e) => setField("location", e.currentTarget.value)}
style={INPUT}
placeholder="Chennai"
/>
@@ -303,7 +311,7 @@ export default function CustomerRequirementsPage() {
{row.title}
- {row.city || "—"} {row.area ? `• ${row.area}` : ""}{" "}
+ {row.location || row.city || "—"} {row.area ? `• ${row.area}` : ""}{" "}
{row.created_at
? `• ${new Date(row.created_at).toLocaleString("en-IN")}`
: ""}
diff --git a/src/components/dashboard/PortfolioPage.tsx b/src/components/dashboard/PortfolioPage.tsx
index cca9567..d15c691 100644
--- a/src/components/dashboard/PortfolioPage.tsx
+++ b/src/components/dashboard/PortfolioPage.tsx
@@ -57,6 +57,26 @@ const EMPTY_JOB_SEEKER_FORM: JobSeekerPortfolioState = {
skills: '',
};
const JOB_SEEKER_FALLBACK_TABS = ['About', 'Education', 'Work Experience', 'Skills'];
+const PROFESSIONAL_PORTFOLIO_TABS: Record = {
+ DEVELOPER: ['About', 'Services & Pricing', 'Projects', 'Tech Stack & Experience', 'Testimonials', 'FAQs'],
+ default: ['About', 'Services & Pricing', 'Projects', 'Experience', 'Testimonials', 'FAQs'],
+};
+
+type ProfessionalPortfolioState = {
+ about: string;
+ services: string;
+ experience: string;
+ testimonials: string;
+ faqs: string;
+};
+
+const EMPTY_PROFESSIONAL_FORM: ProfessionalPortfolioState = {
+ about: '',
+ services: '',
+ experience: '',
+ testimonials: '',
+ faqs: '',
+};
// ── Helpers ───────────────────────────────────────────────────────────────────
@@ -95,6 +115,9 @@ export default function PortfolioPage(props: Props) {
const [jobSeekerSaving, setJobSeekerSaving] = createSignal(false);
const [jobSeekerMsg, setJobSeekerMsg] = createSignal('');
const [jobSeekerErr, setJobSeekerErr] = createSignal('');
+ const [professionalTab, setProfessionalTab] = createSignal('About');
+ const [professionalForm, setProfessionalForm] = createSignal({ ...EMPTY_PROFESSIONAL_FORM });
+ const [professionalMsg, setProfessionalMsg] = createSignal('');
const loadItems = async () => {
if (!isProfessional()) { setLoading(false); return; }
@@ -196,6 +219,54 @@ export default function PortfolioPage(props: Props) {
return grouped;
};
+ const professionalTabs = () => {
+ const runtimeRaw = Array.isArray(props.runtimeTabs) ? props.runtimeTabs : [];
+ const fromRuntime = runtimeRaw
+ .map((tab) => toLabel(tab))
+ .filter(Boolean)
+ .map((tab) => {
+ const t = normalizeToken(tab);
+ if (t.includes('about') || t.includes('overview') || t.includes('profile')) return 'About';
+ if (t.includes('service') || t.includes('pricing') || t.includes('package')) return 'Services & Pricing';
+ if (t.includes('project') || t.includes('portfolio') || t.includes('gallery') || t.includes('showreel')) return 'Projects';
+ if (t.includes('stack') || t.includes('experience') || t.includes('qualification') || t.includes('tool')) return props.roleKey === 'DEVELOPER' ? 'Tech Stack & Experience' : 'Experience';
+ if (t.includes('testimonial') || t.includes('review')) return 'Testimonials';
+ if (t.includes('faq') || t.includes('question')) return 'FAQs';
+ return '';
+ })
+ .filter(Boolean);
+ const uniqueRuntime = Array.from(new Set(fromRuntime));
+ if (uniqueRuntime.length >= 3) return uniqueRuntime;
+ return PROFESSIONAL_PORTFOLIO_TABS[props.roleKey] || PROFESSIONAL_PORTFOLIO_TABS.default;
+ };
+
+ const professionalFormStorageKey = () => `nxtgauge_portfolio_meta_${String(props.roleKey || 'professional').toLowerCase()}`;
+ const loadProfessionalForm = () => {
+ if (typeof window === 'undefined') return;
+ try {
+ const raw = window.localStorage.getItem(professionalFormStorageKey());
+ if (!raw) return;
+ const parsed = JSON.parse(raw) as Partial;
+ setProfessionalForm({
+ about: String(parsed?.about || ''),
+ services: String(parsed?.services || ''),
+ experience: String(parsed?.experience || ''),
+ testimonials: String(parsed?.testimonials || ''),
+ faqs: String(parsed?.faqs || ''),
+ });
+ } catch {
+ // Ignore malformed local storage payloads.
+ }
+ };
+
+ const saveProfessionalForm = () => {
+ if (typeof window !== 'undefined') {
+ window.localStorage.setItem(professionalFormStorageKey(), JSON.stringify(professionalForm()));
+ }
+ setProfessionalMsg('Portfolio section saved.');
+ window.setTimeout(() => setProfessionalMsg(''), 1800);
+ };
+
onMount(() => {
if (isJobSeeker()) {
loadJobSeekerPortfolio();
@@ -204,6 +275,11 @@ export default function PortfolioPage(props: Props) {
setLoading(false);
return;
}
+ if (isProfessional()) {
+ const tabs = professionalTabs();
+ setProfessionalTab(tabs[0] || 'About');
+ loadProfessionalForm();
+ }
void loadItems();
});
@@ -418,195 +494,184 @@ export default function PortfolioPage(props: Props) {
);
}
+ const isProjectsTab = () => {
+ const key = normalizeToken(professionalTab());
+ return key.includes('project') || key.includes('portfolio') || key.includes('gallery') || key.includes('showreel');
+ };
+
+ const isServicesTab = () => {
+ const key = normalizeToken(professionalTab());
+ return key.includes('service') || key.includes('pricing') || key.includes('package');
+ };
+
+ const isTestimonialsTab = () => normalizeToken(professionalTab()).includes('testimonial');
+ const isFaqTab = () => normalizeToken(professionalTab()).includes('faq');
+ const sectionFieldKey = () => {
+ if (isServicesTab()) return 'services' as const;
+ if (isTestimonialsTab()) return 'testimonials' as const;
+ if (isFaqTab()) return 'faqs' as const;
+ const key = normalizeToken(professionalTab());
+ if (key.includes('experience') || key.includes('stack') || key.includes('tool') || key.includes('qualification')) return 'experience' as const;
+ return 'about' as const;
+ };
+
+ const sectionPlaceholder = () => {
+ if (isServicesTab()) return 'List your plans, pricing slabs, and deliverables...';
+ if (isTestimonialsTab()) return 'Add client quotes and project outcomes...';
+ if (isFaqTab()) return 'Add common questions and answers...';
+ if (sectionFieldKey() === 'experience') return 'Share stack, years of experience, and toolchain...';
+ return 'Write a short summary about your profile and strengths...';
+ };
+
return (
-
- {/* ── Header ────────────────────────────────────────────────────── */}
-
-
-
My Portfolio
-
- Showcase your work to attract clients.
-
+
+
+
+ {(tab) => (
+
+ )}
+
+
+
+
+
My Portfolio
+
+ Runtime-config driven tab layout aligned with external dashboard preview.
+
+
+
+
+
-
- {/* ── Create / Edit form ─────────────────────────────────────────── */}
-
-
-
- {editId() ? 'Edit Portfolio Item' : 'New Portfolio Item'}
-
-
-
-
-
- setField('title', e.currentTarget.value)}
- style={INPUT}
- />
-
-
-
+
+
+
+ {professionalMsg()}
+
+
+ {professionalTab()}
+
+
-
-
-
setField('tags', e.currentTarget.value)}
- style={INPUT}
- />
+
+
+
-
-
-
- {error()}
-
-
-
-
-
-
-
-
-
-
- {/* ── Loading ─────────────────────────────────────────────────────── */}
-
-
- Loading portfolio…
-
-
-
- {/* ── Empty state ─────────────────────────────────────────────────── */}
-
-
-
🗂️
-
- No portfolio items yet
-
-
- Add your first work sample to attract clients.
-
-
-
-
-
- {/* ── Portfolio grid ─────────────────────────────────────────────── */}
-
0}>
-
-
- {(item) => (
-
- {/* Placeholder image area */}
-
- 🖼️
+ }
+ >
+
+
+
+
+ {editId() ? 'Edit Portfolio Item' : 'New Portfolio Item'}
+
+
+
+
+ setField('title', e.currentTarget.value)} style={INPUT} />
-
-
- {item.title}
-
-
-
-
- {item.description}
-
-
-
-
0}>
-
-
- {(tag) => (
-
- {tag}
-
- )}
-
-
-
-
-
-
-
+
+
+
+
+
+ setField('tags', e.currentTarget.value)} style={INPUT} />
- )}
-
+
+ {error()}
+
+
+
+
+
+
+
+
+
+ Loading portfolio…
+
+
+
+
+
🗂️
+
No portfolio items yet
+
Add your first work sample to attract clients.
+
+
+
+
0}>
+
+
+ {(item) => (
+
+
🖼️
+
{item.title}
+
+ {item.description}
+
+
0}>
+
+
+ {(tag) => {tag}}
+
+
+
+
+
+
+
+
+ )}
+
+
+
diff --git a/src/components/dashboard/ProfilePage.tsx b/src/components/dashboard/ProfilePage.tsx
index 05a36a1..d988eb1 100644
--- a/src/components/dashboard/ProfilePage.tsx
+++ b/src/components/dashboard/ProfilePage.tsx
@@ -3,111 +3,192 @@
* Supports all 13 roles. Tabs: Basic Info · Documents.
* 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 {
- For, Match, Show, Switch, createEffect, createSignal, onMount,
-} from 'solid-js';
-import { CARD, BTN_ORANGE, BTN_GHOST, INPUT, LABEL, BTN_PRIMARY } from '~/components/DashboardShell';
+ CARD,
+ BTN_ORANGE,
+ BTN_GHOST,
+ INPUT,
+ LABEL,
+ BTN_PRIMARY,
+} from "~/components/DashboardShell";
-const API = '/api/gateway';
+const API = "/api/gateway";
// ── Role-specific field definitions ──────────────────────────────────────────
-const BASIC_FIELDS: Record
> = {
+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' },
+ { 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: "location", 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)' },
+ { 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: "location", 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' },
+ 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: 'location', 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' },
+ ],
+ },
+ { 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' },
+ 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: 'location', 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' },
+ ],
+ },
+ { 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: 'location', 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: 'location', 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> = {
+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' },
+ {
+ 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' },
+ {
+ 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 (2–3 images)', required: true, hint: 'JPG or PNG · Max 5MB each' },
+ {
+ key: "aadhar_doc",
+ label: "Aadhar / Government ID",
+ required: true,
+ hint: "JPG, PNG or PDF · Max 10MB",
+ },
+ {
+ key: "sample_work",
+ label: "Sample Work Photos (2–3 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 (2–3 images)', required: true, hint: 'JPG or PNG · Max 5MB each' },
+ {
+ key: "aadhar_doc",
+ label: "Aadhar / Government ID",
+ required: true,
+ hint: "JPG, PNG or PDF · Max 10MB",
+ },
+ {
+ key: "sample_work",
+ label: "Sample Work Photos (2–3 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' },
+ {
+ 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' },
+ {
+ 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' },
+ {
+ 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",
+ },
],
};
@@ -123,8 +204,8 @@ function getDocFields(roleKey: string) {
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
- credentials: 'include',
- headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
+ credentials: "include",
+ headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) },
});
}
@@ -134,17 +215,17 @@ interface Props {
roleKey: string;
}
-type Tab = 'basic' | 'documents';
+type Tab = "basic" | "documents";
export default function ProfilePage(props: Props) {
- const [tab, setTab] = createSignal('basic');
+ const [tab, setTab] = createSignal("basic");
const [form, setForm] = createSignal>({});
const [saving, setSaving] = createSignal(false);
- const [saveMsg, setSaveMsg] = createSignal('');
- const [verificationStatus, setVerificationStatus] = createSignal('NOT_SUBMITTED');
+ const [saveMsg, setSaveMsg] = createSignal("");
+ const [verificationStatus, setVerificationStatus] = createSignal("NOT_SUBMITTED");
const [docRequest, setDocRequest] = createSignal(null);
const [submitting, setSubmitting] = createSignal(false);
- const [submitMsg, setSubmitMsg] = createSignal('');
+ const [submitMsg, setSubmitMsg] = createSignal("");
// Load saved profile + verification status on mount
onMount(async () => {
@@ -155,10 +236,10 @@ export default function ProfilePage(props: Props) {
if (profileRes.ok) {
const data = await profileRes.json();
- if (data.profile_data && typeof data.profile_data === 'object') {
+ if (data.profile_data && typeof data.profile_data === "object") {
const flat: Record = {};
for (const [k, v] of Object.entries(data.profile_data)) {
- flat[k] = String(v ?? '');
+ flat[k] = String(v ?? "");
}
setForm(flat);
}
@@ -166,158 +247,165 @@ export default function ProfilePage(props: Props) {
if (statusRes.ok) {
const s = await statusRes.json();
- setVerificationStatus(s.status ?? 'NOT_SUBMITTED');
+ setVerificationStatus(s.status ?? "NOT_SUBMITTED");
setDocRequest(s.document_request ?? null);
}
});
- const isLocked = () =>
- ['PENDING', 'UNDER_REVIEW'].includes(verificationStatus());
+ const isLocked = () => ["PENDING", "UNDER_REVIEW"].includes(verificationStatus());
- const setField = (key: string, val: string) =>
- setForm((prev) => ({ ...prev, [key]: val }));
+ const setField = (key: string, val: string) => setForm((prev) => ({ ...prev, [key]: val }));
const handleSave = async () => {
setSaving(true);
- setSaveMsg('');
+ setSaveMsg("");
try {
- const res = await apiFetch('/api/profile', {
- method: 'PATCH',
+ 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.');
+ setSaveMsg(res.ok ? "Saved successfully." : "Failed to save. Please try again.");
} catch {
- setSaveMsg('Network error. Please try again.');
+ setSaveMsg("Network error. Please try again.");
} finally {
setSaving(false);
- setTimeout(() => setSaveMsg(''), 3000);
+ setTimeout(() => setSaveMsg(""), 3000);
}
};
const handleSubmitForVerification = async () => {
setSubmitting(true);
- setSubmitMsg('');
+ setSubmitMsg("");
try {
- const res = await apiFetch('/api/profile/submit-for-verification', {
- method: 'POST',
+ 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.');
+ 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.');
+ setSubmitMsg(data.error ?? "A verification is already in progress.");
} else {
- setSubmitMsg(data.error ?? 'Submission failed. Please try again.');
+ setSubmitMsg(data.error ?? "Submission failed. Please try again.");
}
} catch {
- setSubmitMsg('Network error. Please try again.');
+ setSubmitMsg("Network error. Please try again.");
} finally {
setSubmitting(false);
}
};
const statusColor: Record = {
- PENDING: '#F59E0B',
- UNDER_REVIEW: '#3B82F6',
- DOCUMENTS_REQUESTED: '#FF5E13',
- REVISION_REQUESTED: '#FF5E13',
- APPROVED: '#10B981',
- REJECTED: '#EF4444',
- NOT_SUBMITTED: '#9CA3AF',
+ PENDING: "#F59E0B",
+ UNDER_REVIEW: "#3B82F6",
+ DOCUMENTS_REQUESTED: "#FF5E13",
+ REVISION_REQUESTED: "#FF5E13",
+ APPROVED: "#10B981",
+ REJECTED: "#EF4444",
+ NOT_SUBMITTED: "#9CA3AF",
};
const statusLabel: Record = {
- PENDING: 'Pending Review',
- UNDER_REVIEW: 'Under Review',
- DOCUMENTS_REQUESTED: 'Documents Requested',
- REVISION_REQUESTED: 'Revision Requested',
- APPROVED: 'Approved',
- REJECTED: 'Rejected',
- NOT_SUBMITTED: 'Not Submitted',
+ PENDING: "Pending Review",
+ UNDER_REVIEW: "Under Review",
+ DOCUMENTS_REQUESTED: "Documents Requested",
+ REVISION_REQUESTED: "Revision Requested",
+ APPROVED: "Approved",
+ REJECTED: "Rejected",
+ NOT_SUBMITTED: "Not Submitted",
};
return (
-
-
+
{/* ── Verification status banner ─────────────────────────────────── */}
-
-
-
+
+
+
{statusLabel[verificationStatus()] ?? verificationStatus()}
-
- Action needed: {docRequest()}
+
+ Action needed: {docRequest()}
-
+
-
+
{submitMsg()}
{/* ── Tabs ──────────────────────────────────────────────────────── */}
-
-
}>
+
+
+ }
+ >
{(t) => (