feat(profile): add 3-step wizard for COMPANY role with single submit button
Some checks failed
build-and-release / build (push) Failing after 1m15s

This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-11 01:44:27 +05:30
parent ec4bb48358
commit 3d6a81d4ff

View file

@ -19,7 +19,7 @@ import {
isValidLocation,
isValidURL,
} from "~/lib/form-validation";
import { request, uploadDocument } from "~/lib/api";
import { request, uploadDocument, submitCompanyProfileWithDocuments } from "~/lib/api";
import {
getBasicFields,
getDocFields,
@ -101,6 +101,444 @@ interface Props {
type Tab = "basic" | "documents";
// ── CompanyWizard — 3-step verification wizard for COMPANY role ──────────────
interface CompanyWizardProps {
wizardStep: () => number;
setWizardStep: (s: number | ((prev: number) => number)) => void;
wizardDocs: () => Record<string, File>;
setWizardDocs: (
s: Record<string, File> | ((prev: Record<string, File>) => Record<string, File>)
) => void;
wizardDocErrors: () => Record<string, string>;
setWizardDocErrors: (
s: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>)
) => void;
wizardSubmitting: () => boolean;
wizardSubmitMsg: () => string;
wizardSubmitSuccess: () => boolean;
wizardCanAdvance: () => boolean;
handleWizardSubmit: () => void;
form: () => Record<string, string>;
setField: (key: string, val: string) => void;
basicFields: () => Array<{
key: string;
label: string;
type?: string;
required?: boolean;
options?: string[];
}>;
getDocFields: (roleKey: string) => Array<{
key: string;
label: string;
required?: boolean;
hint?: string;
}>;
fieldNote: (
key: string,
label: string,
required: boolean,
fieldType?: string
) => any;
wizardReset: () => void;
}
function CompanyWizard(props: CompanyWizardProps) {
const stepLabels = ["Basic Information", "Documents", "Review & Submit"];
const formatBytes = (n: number) => {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
};
return (
<div>
{/* ── Success card ─────────────────────────────────────────────── */}
<Show when={props.wizardSubmitSuccess()}>
<div
style={{
...CARD,
"margin-bottom": "16px",
padding: "24px",
background: "#ECFDF5",
border: "1px solid #6EE7B7",
"text-align": "center",
}}
>
<div style={{ "font-size": "36px", "margin-bottom": "8px" }}></div>
<h3 style={{ margin: "0 0 8px", "font-size": "18px", color: "#065F46" }}>
Submitted!
</h3>
<p style={{ margin: "0 0 16px", "font-size": "13px", color: "#065F46" }}>
We'll review your profile and notify you within 2448 hours.
</p>
<button
type="button"
onClick={() => {
if (typeof window !== "undefined") window.location.reload();
}}
style={{ ...BTN_PRIMARY }}
>
View Dashboard
</button>
</div>
</Show>
<Show when={!props.wizardSubmitSuccess()}>
{/* ── Stepper ────────────────────────────────────────────────── */}
<div
style={{
display: "flex",
"align-items": "center",
gap: "8px",
"margin-bottom": "24px",
}}
>
<For each={[0, 1, 2]}>
{(i) => (
<>
<div
style={{
width: "32px",
height: "32px",
"border-radius": "50%",
display: "flex",
"align-items": "center",
"justify-content": "center",
"font-weight": "700",
"font-size": "14px",
background:
props.wizardStep() === i
? "#FF5E13"
: props.wizardStep() > i
? "#FFF"
: "#F3F4F6",
color:
props.wizardStep() === i
? "#FFF"
: props.wizardStep() > i
? "#FF5E13"
: "#9CA3AF",
border:
props.wizardStep() >= i
? "2px solid #FF5E13"
: "2px solid #E5E7EB",
}}
>
{props.wizardStep() > i ? "✓" : i + 1}
</div>
<Show when={i < 2}>
<div
style={{
flex: 1,
height: "2px",
background: props.wizardStep() > i ? "#FF5E13" : "#E5E7EB",
}}
/>
</Show>
</>
)}
</For>
</div>
<p
style={{
"font-size": "13px",
color: "#6B7280",
"margin-bottom": "16px",
}}
>
Step {props.wizardStep() + 1} of 3 {stepLabels[props.wizardStep()]}
</p>
{/* ── Step body ─────────────────────────────────────────────── */}
<div style={CARD}>
<Show when={props.wizardStep() === 0}>
<h3 style={{ margin: "0 0 4px", "font-size": "15px", color: "#111827" }}>
Tell us about your company
</h3>
<p style={{ margin: "0 0 16px", "font-size": "13px", color: "#6B7280" }}>
Fill in your company details to get started.
</p>
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "10px" }}>
<For each={props.basicFields()}>
{(field) => (
<div style={{ "grid-column": field.type === "textarea" ? "span 2" : "span 1" }}>
<label style={LABEL}>
{field.label}
<Show when={field.required}>
<span style={{ color: "#EF4444" }}> *</span>
</Show>
</label>
<Switch>
<Match when={field.type === "textarea"}>
<textarea
rows={3}
value={props.form()[field.key] ?? ""}
onInput={(e) => props.setField(field.key, e.currentTarget.value)}
style={{
...INPUT,
height: "auto",
padding: "10px 12px",
resize: "vertical",
}}
/>
</Match>
<Match when={field.type === "select"}>
<select
value={props.form()[field.key] ?? ""}
onChange={(e) => props.setField(field.key, e.currentTarget.value)}
style={{ ...INPUT }}
>
<option value="">Select</option>
<For each={field.options ?? []}>
{(opt) => <option value={opt}>{opt}</option>}
</For>
</select>
</Match>
<Match when={true}>
<input
type={field.type ?? "text"}
value={props.form()[field.key] ?? ""}
onInput={(e) => props.setField(field.key, e.currentTarget.value)}
style={{ ...INPUT }}
/>
</Match>
</Switch>
{props.fieldNote(field.key, field.label, !!field.required, field.type)}
</div>
)}
</For>
</div>
</Show>
<Show when={props.wizardStep() === 1}>
<h3 style={{ margin: "0 0 4px", "font-size": "15px", color: "#111827" }}>
Upload your verification documents
</h3>
<p style={{ margin: "0 0 16px", "font-size": "13px", color: "#6B7280" }}>
We need to verify your business. Upload the following:
</p>
<div style={{ display: "flex", "flex-direction": "column", gap: "16px" }}>
<For each={props.getDocFields("COMPANY")}>
{(doc) => (
<div
style={{
border: "1px dashed #E5E7EB",
"border-radius": "10px",
padding: "16px",
}}
>
<p
style={{
margin: "0",
"font-size": "13px",
"font-weight": "700",
color: "#111827",
}}
>
{doc.label}
<Show when={doc.required}>
<span style={{ color: "#EF4444" }}> *</span>
</Show>
</p>
<Show when={doc.hint}>
<p style={{ margin: "2px 0 10px", "font-size": "11px", color: "#9CA3AF" }}>
{doc.hint}
</p>
</Show>
<Show
when={props.wizardDocs()[doc.key]}
fallback={
<div style={{ display: "flex", "align-items": "center", gap: "10px" }}>
<input
type="file"
id={`wiz-file-${doc.key}`}
style={{ display: "none" }}
onChange={(e) => {
const file = e.currentTarget.files?.[0];
if (!file) return;
props.setWizardDocs((prev) => ({ ...prev, [doc.key]: file }));
props.setWizardDocErrors((prev) => ({ ...prev, [doc.key]: "" }));
e.currentTarget.value = "";
}}
/>
<label
for={`wiz-file-${doc.key}`}
style={{
...BTN_GHOST,
display: "inline-flex",
"align-items": "center",
"line-height": "1",
cursor: "pointer",
}}
>
Choose File
</label>
<span style={{ "font-size": "12px", color: "#9CA3AF" }}>
No file chosen
</span>
</div>
}
>
<div style={{ display: "flex", "align-items": "center", gap: "10px" }}>
<span
style={{
"font-size": "12px",
"font-weight": "600",
color: "#10B981",
background: "#ECFDF5",
padding: "4px 10px",
"border-radius": "6px",
}}
>
{props.wizardDocs()[doc.key].name} ({formatBytes(props.wizardDocs()[doc.key].size)})
</span>
<button
type="button"
onClick={() => {
props.setWizardDocs((prev) => {
const next = { ...prev };
delete next[doc.key];
return next;
});
}}
style={{
...BTN_GHOST,
height: "28px",
"font-size": "11px",
padding: "0 10px",
}}
>
Remove
</button>
</div>
</Show>
<Show when={props.wizardDocErrors()[doc.key]}>
<p style={{ margin: "4px 0 0", "font-size": "11px", color: "#EF4444" }}>
{props.wizardDocErrors()[doc.key]}
</p>
</Show>
</div>
)}
</For>
</div>
</Show>
<Show when={props.wizardStep() === 2}>
<h3 style={{ margin: "0 0 4px", "font-size": "15px", color: "#111827" }}>
Review your information
</h3>
<p style={{ margin: "0 0 16px", "font-size": "13px", color: "#6B7280" }}>
Please confirm everything looks correct before submitting.
</p>
<div
style={{
padding: "16px",
background: "#F9FAFB",
"border-radius": "8px",
"margin-bottom": "16px",
}}
>
<For each={props.basicFields()}>
{(field) => (
<p style={{ margin: "4px 0", "font-size": "13px", color: "#374151" }}>
<strong>{field.label}:</strong>{" "}
{String(props.form()[field.key] || "").trim() || (
<span style={{ color: "#9CA3AF" }}></span>
)}
</p>
)}
</For>
</div>
<div
style={{
padding: "16px",
background: "#F9FAFB",
"border-radius": "8px",
}}
>
<p style={{ margin: "0 0 6px", "font-size": "13px", "font-weight": "700", color: "#111827" }}>
Documents:
</p>
<For each={props.getDocFields("COMPANY")}>
{(doc) => {
const f = props.wizardDocs()[doc.key];
return (
<p style={{ margin: "2px 0", "font-size": "12px", color: "#374151" }}>
<strong>{doc.label}:</strong>{" "}
{f ? `${f.name} (${formatBytes(f.size)})` : <span style={{ color: "#9CA3AF" }}></span>}
</p>
);
}}
</For>
</div>
</Show>
{/* ── Navigation buttons ──────────────────────────────────── */}
<div
style={{
display: "flex",
"justify-content": "space-between",
"margin-top": "24px",
}}
>
<button
type="button"
onClick={() => props.setWizardStep((s) => s - 1)}
disabled={props.wizardStep() === 0}
style={{ ...BTN_GHOST, opacity: props.wizardStep() === 0 ? 0.5 : 1 }}
>
Back
</button>
<Show
when={props.wizardStep() < 2}
fallback={
<button
type="button"
onClick={props.handleWizardSubmit}
disabled={props.wizardSubmitting()}
style={{ ...BTN_PRIMARY, opacity: props.wizardSubmitting() ? 0.7 : 1 }}
>
{props.wizardSubmitting() ? "Submitting..." : "Submit for Verification"}
</button>
}
>
<button
type="button"
onClick={() => props.setWizardStep((s) => s + 1)}
disabled={!props.wizardCanAdvance()}
style={{
...BTN_PRIMARY,
opacity: props.wizardCanAdvance() ? 1 : 0.5,
}}
>
Next
</button>
</Show>
</div>
<Show when={props.wizardSubmitMsg() && props.wizardStep() === 2}>
<div
style={{
"margin-top": "12px",
padding: "10px 14px",
background: "#FEF2F2",
border: "1px solid #FECACA",
color: "#B91C1C",
"font-size": "13px",
"font-weight": "600",
"border-radius": "6px",
}}
>
{props.wizardSubmitMsg()}
</div>
</Show>
</div>
</Show>
</div>
);
}
export default function ProfilePage(props: Props) {
const [tab, setTab] = createSignal<Tab>("basic");
const [form, setForm] = createSignal<Record<string, string>>({});
@ -117,6 +555,16 @@ export default function ProfilePage(props: Props) {
const [photoUploading, setPhotoUploading] = createSignal(false);
const [photoMsg, setPhotoMsg] = createSignal("");
// ── Wizard state (COMPANY role only) ────────────────────────────────────
const [wizardStep, setWizardStep] = createSignal(0);
const [wizardDocs, setWizardDocs] = createSignal<Record<string, File>>({});
const [wizardDocErrors, setWizardDocErrors] = createSignal<Record<string, string>>({});
const [wizardSubmitting, setWizardSubmitting] = createSignal(false);
const [wizardSubmitMsg, setWizardSubmitMsg] = createSignal("");
const [wizardSubmitSuccess, setWizardSubmitSuccess] = createSignal(false);
const isCompany = () => props.roleKey === "COMPANY";
const requiresPortfolio = () => props.roleKey === "JOB_SEEKER" || Boolean(PORTFOLIO_PREFIX[props.roleKey]);
const refreshPortfolioSubmission = async (): Promise<string[]> => {
@ -461,8 +909,64 @@ export default function ProfilePage(props: Props) {
missingPortfolioLabels().length === 0
);
// ── Wizard helpers (COMPANY only) ───────────────────────────────────────
const wizardCanAdvance = createMemo(() => {
if (wizardStep() === 0) return missingBasicLabels().length === 0;
if (wizardStep() === 1) {
return requiredDocFields().every((doc) => !!wizardDocs()[doc.key]);
}
return true;
});
const handleWizardSubmit = async () => {
setWizardSubmitting(true);
setWizardSubmitMsg("");
try {
const docs = Object.entries(wizardDocs()).map(([k, f]) => ({
documentType: k,
file: f,
}));
await submitCompanyProfileWithDocuments(
{ roleKey: props.roleKey, profile_data: form() },
docs
);
setWizardSubmitSuccess(true);
setVerificationStatus("PENDING");
props.onVerificationStatusChange?.("PENDING");
} catch (err: any) {
setWizardSubmitMsg(err?.message ?? "Submission failed. Please try again.");
} finally {
setWizardSubmitting(false);
}
};
return (
<div style={{ "max-width": "760px" }}>
<Show when={!isCompany()} fallback={<CompanyWizard
wizardStep={wizardStep}
setWizardStep={setWizardStep}
wizardDocs={wizardDocs}
setWizardDocs={setWizardDocs}
wizardDocErrors={wizardDocErrors}
setWizardDocErrors={setWizardDocErrors}
wizardSubmitting={wizardSubmitting}
wizardSubmitMsg={wizardSubmitMsg}
wizardSubmitSuccess={wizardSubmitSuccess}
wizardCanAdvance={wizardCanAdvance}
handleWizardSubmit={handleWizardSubmit}
form={form}
setField={setField}
basicFields={basicFields}
getDocFields={getDocFields}
fieldNote={fieldNote}
wizardReset={() => {
setWizardStep(0);
setWizardDocs({});
setWizardDocErrors({});
setWizardSubmitMsg("");
setWizardSubmitSuccess(false);
}}
/>}>
<Show when={submitMsg()}>
<div
style={{
@ -823,6 +1327,7 @@ export default function ProfilePage(props: Props) {
</Show>
</div>
</Show>
</Show>
</div>
);
}