Files
website/components/BusinessRegistration.tsx
T

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>
);
}