542 lines
17 KiB
TypeScript
542 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import CategoryMultiSelect from "@/components/CategoryMultiSelect";
|
|
import DetailBanner from "@/components/DetailBanner";
|
|
import {
|
|
categoryLabel,
|
|
searchMeshkeeCategories,
|
|
type MeshkeeCategory,
|
|
} from "@/lib/meshkee-categories";
|
|
import { registerShop } from "@/lib/marketplace";
|
|
import styles from "./BusinessRegistration.module.css";
|
|
|
|
const steps = [
|
|
{ id: 1, label: "Business information" },
|
|
{ id: 2, label: "insert photo" },
|
|
{ id: 3, label: "Confirm information" },
|
|
] as const;
|
|
|
|
const businessTypes = [
|
|
{
|
|
id: "store",
|
|
title: "Store",
|
|
subtitle: "In publishing and graphic design",
|
|
icon: "/images/register/type-store.png",
|
|
iconClass: "typeIconStore",
|
|
},
|
|
{
|
|
id: "corporates",
|
|
title: "Corporates",
|
|
subtitle: "In publishing and graphic design",
|
|
icon: "/images/register/type-corporates.png",
|
|
iconClass: "typeIconCorporates",
|
|
},
|
|
{
|
|
id: "industrial",
|
|
title: "industrial",
|
|
subtitle: "In publishing and graphic design",
|
|
icon: "/images/register/type-industrial.png",
|
|
iconClass: "typeIconIndustrial",
|
|
},
|
|
] as const;
|
|
|
|
const sideImages = [
|
|
{
|
|
src: "/images/register/side-step1.png",
|
|
className: "sideImageLaptop",
|
|
},
|
|
{
|
|
src: "/images/register/side-step2.png",
|
|
className: "sideImagePainting",
|
|
},
|
|
{
|
|
src: "/images/register/side-step3.png",
|
|
className: "sideImageNotebook",
|
|
},
|
|
] as const;
|
|
|
|
type FormState = {
|
|
businessType: string;
|
|
categoryIds: string[];
|
|
storeName: string;
|
|
telephone: string;
|
|
since: string;
|
|
gmail: string;
|
|
description: string;
|
|
country: string;
|
|
province: string;
|
|
postalCode: string;
|
|
address: string;
|
|
logoName: string;
|
|
bannerName: string;
|
|
};
|
|
|
|
const initialForm: FormState = {
|
|
businessType: "corporates",
|
|
categoryIds: [],
|
|
storeName: "Sani Khani",
|
|
telephone: "025 - 3698 52 69",
|
|
since: "2018",
|
|
gmail: "@Gmail.com",
|
|
description: "In publishing and graphic design, Lorem ipsum is a placeholder.",
|
|
country: "Iran",
|
|
province: "Qom",
|
|
postalCode: "03785692",
|
|
address: "In publishing and graphic design, Lorem ipsum is a placeholder.",
|
|
logoName: "",
|
|
bannerName: "",
|
|
};
|
|
|
|
function Field({
|
|
label,
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
className,
|
|
type = "text",
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
type?: string;
|
|
}) {
|
|
return (
|
|
<label className={`${styles.field} ${className ?? ""}`}>
|
|
<span>{label}</span>
|
|
<input
|
|
type={type}
|
|
value={value}
|
|
placeholder={placeholder}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
/>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function SelectField({
|
|
label,
|
|
value,
|
|
onChange,
|
|
options,
|
|
className,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
options: string[];
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<label className={`${styles.field} ${styles.selectField} ${className ?? ""}`}>
|
|
<span>{label}</span>
|
|
<select value={value} onChange={(event) => onChange(event.target.value)}>
|
|
{options.map((option) => (
|
|
<option key={option} value={option}>
|
|
{option}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<img src="/images/icons/chevron.svg" alt="" className={styles.selectChevron} />
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function SummaryItem({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className={styles.summaryItem}>
|
|
<span>{label}</span>
|
|
<strong>{value}</strong>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function BusinessRegistration() {
|
|
const [step, setStep] = useState(1);
|
|
const [form, setForm] = useState<FormState>(initialForm);
|
|
const [categories, setCategories] = useState<MeshkeeCategory[]>([]);
|
|
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
|
const [categoriesError, setCategoriesError] = useState("");
|
|
const [ownerPassword, setOwnerPassword] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [submitError, setSubmitError] = useState("");
|
|
const [submitOk, setSubmitOk] = useState("");
|
|
|
|
const update = <K extends keyof FormState>(key: K, value: FormState[K]) => {
|
|
setForm((prev) => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController();
|
|
|
|
async function loadCategories() {
|
|
setCategoriesLoading(true);
|
|
setCategoriesError("");
|
|
try {
|
|
const items = await searchMeshkeeCategories("", controller.signal);
|
|
if (controller.signal.aborted) return;
|
|
setCategories(items);
|
|
} catch (err) {
|
|
if (controller.signal.aborted) return;
|
|
setCategories([]);
|
|
setCategoriesError(
|
|
err instanceof Error ? err.message : "Failed to load categories",
|
|
);
|
|
} finally {
|
|
if (!controller.signal.aborted) setCategoriesLoading(false);
|
|
}
|
|
}
|
|
|
|
void loadCategories();
|
|
return () => controller.abort();
|
|
}, []);
|
|
|
|
const goNext = () => setStep((prev) => Math.min(3, prev + 1));
|
|
const goBack = () => setStep((prev) => Math.max(1, prev - 1));
|
|
|
|
const businessTypeLabel =
|
|
businessTypes.find((item) => item.id === form.businessType)?.title ??
|
|
form.businessType;
|
|
|
|
const selectedCategoryLabels = useMemo(() => {
|
|
const byId = new Map(categories.map((item) => [item.id, item]));
|
|
return form.categoryIds
|
|
.map((id) => byId.get(id))
|
|
.filter((item): item is MeshkeeCategory => Boolean(item))
|
|
.map((item) => categoryLabel(item));
|
|
}, [categories, form.categoryIds]);
|
|
|
|
return (
|
|
<div className={styles.root}>
|
|
<DetailBanner
|
|
title="Business registration"
|
|
breadcrumbs={["Home", "Business Register"]}
|
|
/>
|
|
|
|
<div className={styles.container}>
|
|
<nav className={styles.steps} aria-label="Registration steps">
|
|
{steps.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
className={step === item.id ? styles.stepActive : styles.step}
|
|
onClick={() => setStep(item.id)}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
<div className={styles.layout}>
|
|
<aside className={styles.side} aria-hidden>
|
|
<div className={styles.sideRail} />
|
|
<img
|
|
className={`${styles.sideImage} ${styles[sideImages[step - 1].className]}`}
|
|
src={sideImages[step - 1].src}
|
|
alt=""
|
|
/>
|
|
</aside>
|
|
|
|
<div className={styles.panel}>
|
|
{step === 1 ? (
|
|
<div className={styles.stepPanel}>
|
|
<div className={styles.typeRow}>
|
|
<div className={styles.typeIntro}>
|
|
<strong>Category</strong>
|
|
<span>Do Select</span>
|
|
</div>
|
|
<div className={styles.typeCards}>
|
|
{businessTypes.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
className={
|
|
form.businessType === item.id
|
|
? styles.typeCardActive
|
|
: styles.typeCard
|
|
}
|
|
onClick={() => update("businessType", item.id)}
|
|
>
|
|
<span className={styles.typeIcon}>
|
|
<img
|
|
className={styles[item.iconClass]}
|
|
src={item.icon}
|
|
alt=""
|
|
/>
|
|
</span>
|
|
<span>
|
|
<em>{item.title}</em>
|
|
<small>{item.subtitle}</small>
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<CategoryMultiSelect
|
|
label="Category"
|
|
categories={categories}
|
|
value={form.categoryIds}
|
|
onChange={(ids) => update("categoryIds", ids)}
|
|
loading={categoriesLoading}
|
|
error={categoriesError}
|
|
/>
|
|
|
|
<hr className={styles.rule} />
|
|
|
|
<div className={styles.row4}>
|
|
<Field
|
|
label="Store name"
|
|
value={form.storeName}
|
|
onChange={(value) => update("storeName", value)}
|
|
/>
|
|
<Field
|
|
label="Telephone"
|
|
value={form.telephone}
|
|
onChange={(value) => update("telephone", value)}
|
|
/>
|
|
<Field
|
|
label="Since"
|
|
value={form.since}
|
|
onChange={(value) => update("since", value)}
|
|
/>
|
|
<Field
|
|
label="Gmail"
|
|
value={form.gmail}
|
|
onChange={(value) => update("gmail", value)}
|
|
placeholder="@Gmail.com"
|
|
/>
|
|
</div>
|
|
|
|
<label className={`${styles.field} ${styles.textareaField}`}>
|
|
<span>Description</span>
|
|
<textarea
|
|
value={form.description}
|
|
onChange={(event) => update("description", event.target.value)}
|
|
rows={4}
|
|
/>
|
|
</label>
|
|
|
|
<hr className={styles.rule} />
|
|
|
|
<div className={styles.rowLocation}>
|
|
<SelectField
|
|
label="country"
|
|
value={form.country}
|
|
onChange={(value) => update("country", value)}
|
|
options={["Iran", "UAE", "Turkey"]}
|
|
/>
|
|
<SelectField
|
|
label="Province"
|
|
value={form.province}
|
|
onChange={(value) => update("province", value)}
|
|
options={["Qom", "Tehran", "Isfahan", "Shiraz"]}
|
|
/>
|
|
<Field
|
|
label="Postal code"
|
|
value={form.postalCode}
|
|
onChange={(value) => update("postalCode", value)}
|
|
/>
|
|
</div>
|
|
|
|
<label className={`${styles.field} ${styles.textareaField}`}>
|
|
<span>Address</span>
|
|
<textarea
|
|
value={form.address}
|
|
onChange={(event) => update("address", event.target.value)}
|
|
rows={2}
|
|
/>
|
|
</label>
|
|
</div>
|
|
) : null}
|
|
|
|
{step === 2 ? (
|
|
<div className={styles.stepPanel}>
|
|
<div className={styles.uploadGrid}>
|
|
<label className={styles.uploadCard}>
|
|
<span className={styles.uploadHeader}>
|
|
<img src="/images/register/upload.png" alt="" />
|
|
<span>
|
|
<em>Enter Logo</em>
|
|
<small>Max image size: 200kb</small>
|
|
</span>
|
|
</span>
|
|
<span className={styles.uploadBox}>
|
|
{form.logoName ? form.logoName : "111 x 80"}
|
|
</span>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
className={styles.fileInput}
|
|
onChange={(event) =>
|
|
update("logoName", event.target.files?.[0]?.name ?? "")
|
|
}
|
|
/>
|
|
</label>
|
|
|
|
<label className={styles.uploadCardWide}>
|
|
<span className={styles.uploadHeader}>
|
|
<img src="/images/register/upload.png" alt="" />
|
|
<span>
|
|
<em>Enter Banner</em>
|
|
<small>Max image size: 2mb</small>
|
|
</span>
|
|
</span>
|
|
<span className={styles.uploadBox}>
|
|
{form.bannerName ? form.bannerName : "1386 x 189"}
|
|
</span>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
className={styles.fileInput}
|
|
onChange={(event) =>
|
|
update("bannerName", event.target.files?.[0]?.name ?? "")
|
|
}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{step === 3 ? (
|
|
<div className={styles.stepPanel}>
|
|
<div className={styles.summarySection}>
|
|
<SummaryItem label="Business type" value={businessTypeLabel} />
|
|
<SummaryItem
|
|
label="Category"
|
|
value={
|
|
selectedCategoryLabels.length
|
|
? selectedCategoryLabels.join(", ")
|
|
: "—"
|
|
}
|
|
/>
|
|
</div>
|
|
<hr className={styles.rule} />
|
|
<div className={styles.summarySection}>
|
|
<SummaryItem label="Store Name" value={form.storeName} />
|
|
<SummaryItem label="Telephone" value={form.telephone} />
|
|
<SummaryItem label="Since" value={form.since} />
|
|
<SummaryItem label="Gmail" value={form.gmail} />
|
|
</div>
|
|
<hr className={styles.rule} />
|
|
<div className={styles.summaryBlock}>
|
|
<span>Discraption</span>
|
|
<p>{form.description}</p>
|
|
</div>
|
|
<hr className={styles.rule} />
|
|
<div className={styles.summarySection}>
|
|
<SummaryItem label="Caountry" value={form.country} />
|
|
<SummaryItem label="province" value={form.province} />
|
|
<SummaryItem label="postal code" value={form.postalCode} />
|
|
</div>
|
|
<hr className={styles.rule} />
|
|
<div className={styles.summaryBlock}>
|
|
<span>Address</span>
|
|
<p>{form.address}</p>
|
|
</div>
|
|
<hr className={styles.rule} />
|
|
<label className={styles.field}>
|
|
<span>Account password</span>
|
|
<input
|
|
type="password"
|
|
value={ownerPassword}
|
|
onChange={(event) => setOwnerPassword(event.target.value)}
|
|
minLength={8}
|
|
required
|
|
autoComplete="current-password"
|
|
placeholder="Confirm your login password"
|
|
/>
|
|
</label>
|
|
<p className={styles.confirmHint}>
|
|
Confirm your IranBay account password to create your Meshkee
|
|
shop (separate from the iranbay.com auth tenant).
|
|
</p>
|
|
{submitError ? (
|
|
<p className={styles.submitError} role="alert">
|
|
{submitError}
|
|
</p>
|
|
) : null}
|
|
{submitOk ? (
|
|
<p className={styles.submitOk} role="status">
|
|
{submitOk}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className={styles.footerBar}>
|
|
{step > 1 ? (
|
|
<button
|
|
type="button"
|
|
className={styles.backBtn}
|
|
onClick={goBack}
|
|
disabled={submitting}
|
|
>
|
|
<img src="/images/icons/chevron.svg" alt="" />
|
|
step backward
|
|
</button>
|
|
) : (
|
|
<span />
|
|
)}
|
|
{step < 3 ? (
|
|
<button
|
|
type="button"
|
|
className={`outline-btn ${styles.continueBtn}`}
|
|
onClick={goNext}
|
|
>
|
|
Continue registration
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className={`outline-btn ${styles.continueBtn}`}
|
|
disabled={submitting || Boolean(submitOk)}
|
|
onClick={async () => {
|
|
setSubmitError("");
|
|
setSubmitOk("");
|
|
if (ownerPassword.length < 8) {
|
|
setSubmitError("Enter your account password (min 8 chars).");
|
|
return;
|
|
}
|
|
if (!form.storeName.trim()) {
|
|
setSubmitError("Store name is required.");
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const result = await registerShop({
|
|
name: form.storeName.trim(),
|
|
about: form.description.trim() || undefined,
|
|
meshkeeCategoryIds: form.categoryIds.length
|
|
? form.categoryIds
|
|
: undefined,
|
|
ownerPassword,
|
|
});
|
|
setSubmitOk(
|
|
result.message ||
|
|
`Submitted. Meshkee shop #${result.meshkeeBusinessId}`,
|
|
);
|
|
} catch (err) {
|
|
setSubmitError(
|
|
err instanceof Error
|
|
? err.message
|
|
: "Could not register business",
|
|
);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}}
|
|
>
|
|
{submitting ? "Submitting…" : "Confirm information"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|