Add customer partnership form and admin requests list with detail modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f192085c1f
commit
e687e4502e
@@ -30,6 +30,13 @@ export default function CustomerHomePage() {
|
||||
description: "اطلاعات حساب و آدرسها",
|
||||
icon: <ProfileIcon />,
|
||||
},
|
||||
{
|
||||
href: "/account/partners",
|
||||
title: "شروع همکاری",
|
||||
titleEn: "Partnership",
|
||||
description: "درخواست همکاری بهعنوان تولیدکننده",
|
||||
icon: <PartnersIcon />,
|
||||
},
|
||||
...(showAdmin
|
||||
? [
|
||||
{
|
||||
@@ -104,13 +111,13 @@ function OrdersIcon() {
|
||||
<path
|
||||
d="M6 7h12l-.8 11.2a1 1 0 0 1-1 .8H7.8a1 1 0 0 1-1-.8L6 7Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.15"
|
||||
strokeWidth="0.85"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 7a3 3 0 0 1 6 0"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.15"
|
||||
strokeWidth="0.85"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
@@ -120,24 +127,43 @@ function OrdersIcon() {
|
||||
function ProfileIcon() {
|
||||
return (
|
||||
<svg {...iconProps()}>
|
||||
<circle cx="12" cy="8" r="3" stroke="currentColor" strokeWidth="1.15" />
|
||||
<circle cx="12" cy="8" r="3" stroke="currentColor" strokeWidth="0.85" />
|
||||
<path
|
||||
d="M5 19c1.5-3.2 3.8-4.8 7-4.8s5.5 1.6 7 4.8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.15"
|
||||
strokeWidth="0.85"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PartnersIcon() {
|
||||
return (
|
||||
<svg {...iconProps()}>
|
||||
<path
|
||||
d="M8 12h8M12 8v8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0.85"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M7 5.5h10a1.5 1.5 0 0 1 1.5 1.5v10a1.5 1.5 0 0 1-1.5 1.5H7A1.5 1.5 0 0 1 5.5 17V7A1.5 1.5 0 0 1 7 5.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0.85"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminIcon() {
|
||||
return (
|
||||
<svg {...iconProps()}>
|
||||
<path
|
||||
d="M4.5 10.5 12 4.5l7.5 6V19a1 1 0 0 1-1 1h-4.5v-5h-5v5H5.5a1 1 0 0 1-1-1v-8.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.15"
|
||||
strokeWidth="0.85"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import ClothesShell from "@/components/clothes/ClothesShell";
|
||||
import SearchableMultiSelect from "@/components/ui/SearchableMultiSelect";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { fetchProfile } from "@/lib/profile-api";
|
||||
import {
|
||||
createPartnershipRequest,
|
||||
fetchPublicCategoryOptions,
|
||||
uploadPartnershipImage,
|
||||
type FlatCategoryOption,
|
||||
} from "@/lib/partnership-api";
|
||||
|
||||
const PROVINCES = [
|
||||
"تهران",
|
||||
"اصفهان",
|
||||
"فارس",
|
||||
"خراسان رضوی",
|
||||
"آذربایجان شرقی",
|
||||
"آذربایجان غربی",
|
||||
"گیلان",
|
||||
"مازندران",
|
||||
"خوزستان",
|
||||
"کرمان",
|
||||
"یزد",
|
||||
"قم",
|
||||
"البرز",
|
||||
"همدان",
|
||||
"کردستان",
|
||||
"کرمانشاه",
|
||||
"هرمزگان",
|
||||
"بوشهر",
|
||||
"سیستان و بلوچستان",
|
||||
"گلستان",
|
||||
"قزوین",
|
||||
"زنجان",
|
||||
"اردبیل",
|
||||
"لرستان",
|
||||
"مرکزی",
|
||||
"سمنان",
|
||||
"چهارمحال و بختیاری",
|
||||
"کهگیلویه و بویراحمد",
|
||||
"ایلام",
|
||||
"خراسان شمالی",
|
||||
"خراسان جنوبی",
|
||||
];
|
||||
|
||||
const MAX_IMAGES = 6;
|
||||
|
||||
export default function AccountPartnersPage() {
|
||||
const [categories, setCategories] = useState<FlatCategoryOption[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cellNumber, setCellNumber] = useState("");
|
||||
const [businessNameFa, setBusinessNameFa] = useState("");
|
||||
const [businessNameEn, setBusinessNameEn] = useState("");
|
||||
const [selectedCategories, setSelectedCategories] = useState<number[]>([]);
|
||||
const [description, setDescription] = useState("");
|
||||
const [imageUrls, setImageUrls] = useState<string[]>([]);
|
||||
const [landline, setLandline] = useState("");
|
||||
const [instagram, setInstagram] = useState("");
|
||||
const [city, setCity] = useState("");
|
||||
const [province, setProvince] = useState("");
|
||||
const [address, setAddress] = useState("");
|
||||
const [postalCode, setPostalCode] = useState("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [profile, cats] = await Promise.all([
|
||||
fetchProfile(),
|
||||
fetchPublicCategoryOptions(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setCellNumber(profile.cellNumber || "");
|
||||
if (profile.instagram) setInstagram(profile.instagram);
|
||||
setCategories(cats);
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setError(
|
||||
err instanceof ApiError
|
||||
? err.message
|
||||
: "بارگذاری فرم ناموفق بود",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleImagesSelected(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
const files = Array.from(event.target.files ?? []);
|
||||
event.target.value = "";
|
||||
if (!files.length) return;
|
||||
|
||||
const remaining = MAX_IMAGES - imageUrls.length;
|
||||
if (remaining <= 0) {
|
||||
setError(`حداکثر ${MAX_IMAGES} تصویر میتوانید بارگذاری کنید`);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setUploading(true);
|
||||
try {
|
||||
const uploaded: string[] = [];
|
||||
for (const file of files.slice(0, remaining)) {
|
||||
const result = await uploadPartnershipImage(file);
|
||||
uploaded.push(result.url);
|
||||
}
|
||||
setImageUrls((prev) => [...prev, ...uploaded]);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof ApiError ? err.message : "بارگذاری تصویر ناموفق بود",
|
||||
);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!selectedCategories.length) {
|
||||
setError("حداقل یک دستهبندی فعالیت انتخاب کنید");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await createPartnershipRequest({
|
||||
businessNameFa: businessNameFa.trim(),
|
||||
businessNameEn: businessNameEn.trim(),
|
||||
categoryIds: selectedCategories,
|
||||
description: description.trim(),
|
||||
imageUrls,
|
||||
landline: landline.trim() || undefined,
|
||||
instagram: instagram.trim() || undefined,
|
||||
city: city.trim(),
|
||||
province,
|
||||
address: address.trim(),
|
||||
postalCode: postalCode.trim(),
|
||||
});
|
||||
setSent(true);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof ApiError ? err.message : "ارسال درخواست ناموفق بود",
|
||||
);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ClothesShell
|
||||
eyebrow="Partnership"
|
||||
title="شروع همکاری"
|
||||
description="اگر تولیدکنندهاید، اطلاعات کسبوکار خود را ارسال کنید تا امکان همکاری را بررسی کنیم."
|
||||
breadcrumbs={[
|
||||
{ label: "حساب کاربری", href: "/account" },
|
||||
{ label: "شروع همکاری" },
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<p className="rounded-lg border border-line bg-surface px-4 py-10 text-center text-sm text-muted">
|
||||
در حال بارگذاری…
|
||||
</p>
|
||||
) : sent ? (
|
||||
<div className="rounded-lg border border-line bg-surface px-5 py-10 text-center">
|
||||
<p className="font-doran text-xl text-ink">درخواست شما ثبت شد</p>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-1 font-display text-[10px] uppercase tracking-[0.16em] text-muted"
|
||||
>
|
||||
Partnership request received
|
||||
</p>
|
||||
<p className="mt-3 text-sm leading-7 text-muted">
|
||||
تیم مانا بهزودی درخواست همکاری شما را بررسی میکند.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={(e) => void handleSubmit(e)}
|
||||
className="dash-enter space-y-3"
|
||||
dir="rtl"
|
||||
>
|
||||
{error ? (
|
||||
<p className="rounded-lg border border-line bg-surface px-4 py-3 text-sm text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<section className="rounded-lg border border-line bg-surface p-4 sm:p-5">
|
||||
<h2 className="mb-4 font-doran text-lg text-ink">اطلاعات کسبوکار</h2>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
||||
<div className="clothes-field sm:col-span-6">
|
||||
<label htmlFor="partner-name-fa">نام کسبوکار (فارسی)</label>
|
||||
<input
|
||||
id="partner-name-fa"
|
||||
required
|
||||
value={businessNameFa}
|
||||
onChange={(e) => setBusinessNameFa(e.target.value)}
|
||||
placeholder="نام برند یا فروشگاه"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-6">
|
||||
<label htmlFor="partner-name-en">Business name (EN)</label>
|
||||
<input
|
||||
id="partner-name-en"
|
||||
dir="ltr"
|
||||
value={businessNameEn}
|
||||
onChange={(e) => setBusinessNameEn(e.target.value)}
|
||||
placeholder="Brand or shop name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="clothes-field sm:col-span-12">
|
||||
<label>دستهبندی فعالیت</label>
|
||||
<SearchableMultiSelect
|
||||
options={categories}
|
||||
value={selectedCategories}
|
||||
onChange={setSelectedCategories}
|
||||
placeholder="انتخاب دستهبندیها"
|
||||
searchPlaceholder="جستجوی دستهبندی..."
|
||||
/>
|
||||
<span className="text-[0.65rem] text-muted">
|
||||
میتوانید چند مورد انتخاب کنید
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="clothes-field sm:col-span-12">
|
||||
<label htmlFor="partner-desc">توضیحات</label>
|
||||
<textarea
|
||||
id="partner-desc"
|
||||
required
|
||||
rows={5}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="درباره محصولات، ظرفیت تولید و نحوه همکاری بنویسید..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-12">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-ink">تصاویر</span>
|
||||
<span className="text-xs text-muted">
|
||||
{imageUrls.length}/{MAX_IMAGES}
|
||||
</span>
|
||||
</div>
|
||||
{imageUrls.length > 0 ? (
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{imageUrls.map((url) => (
|
||||
<div
|
||||
key={url}
|
||||
className="relative overflow-hidden rounded border border-line bg-[#f7f7f5]"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
className="h-28 w-auto max-w-full object-contain"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setImageUrls((prev) => prev.filter((u) => u !== url))
|
||||
}
|
||||
className="absolute start-1 top-1 rounded bg-ink/80 px-1.5 py-0.5 text-[10px] text-white"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{imageUrls.length < MAX_IMAGES ? (
|
||||
<label className="inline-flex cursor-pointer items-center rounded border border-dashed border-line bg-white px-3 py-2.5 text-sm text-ink/80 transition-colors hover:border-ink hover:text-ink">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
multiple
|
||||
className="sr-only"
|
||||
disabled={uploading}
|
||||
onChange={handleImagesSelected}
|
||||
/>
|
||||
{uploading ? "در حال بارگذاری…" : "افزودن تصویر"}
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-line bg-surface p-4 sm:p-5">
|
||||
<h2 className="mb-4 font-doran text-lg text-ink">اطلاعات تماس</h2>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor="partner-cell">شماره موبایل</label>
|
||||
<input
|
||||
id="partner-cell"
|
||||
dir="ltr"
|
||||
className="!text-end font-montserrat profile-input-locked"
|
||||
value={cellNumber}
|
||||
readOnly
|
||||
aria-readonly="true"
|
||||
/>
|
||||
<span className="text-[0.65rem] text-muted">قابل تغییر نیست</span>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor="partner-landline">تلفن ثابت</label>
|
||||
<input
|
||||
id="partner-landline"
|
||||
dir="ltr"
|
||||
value={landline}
|
||||
onChange={(e) => setLandline(e.target.value)}
|
||||
placeholder="021xxxxxxx"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor="partner-ig">اینستاگرام</label>
|
||||
<input
|
||||
id="partner-ig"
|
||||
dir="ltr"
|
||||
value={instagram}
|
||||
onChange={(e) => setInstagram(e.target.value)}
|
||||
placeholder="@username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-line bg-surface p-4 sm:p-5">
|
||||
<h2 className="mb-4 font-doran text-lg text-ink">آدرس</h2>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor="partner-province">استان</label>
|
||||
<select
|
||||
id="partner-province"
|
||||
required
|
||||
value={province}
|
||||
onChange={(e) => setProvince(e.target.value)}
|
||||
>
|
||||
<option value="">انتخاب استان</option>
|
||||
{PROVINCES.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor="partner-city">شهر</label>
|
||||
<input
|
||||
id="partner-city"
|
||||
required
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
placeholder="شهر"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor="partner-postal">کد پستی</label>
|
||||
<input
|
||||
id="partner-postal"
|
||||
dir="ltr"
|
||||
required
|
||||
value={postalCode}
|
||||
onChange={(e) => setPostalCode(e.target.value)}
|
||||
placeholder="xxxxxxxxxx"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-12">
|
||||
<label htmlFor="partner-address">آدرس</label>
|
||||
<textarea
|
||||
id="partner-address"
|
||||
required
|
||||
rows={3}
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
placeholder="آدرس کامل"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-end pt-1">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving || uploading}
|
||||
className="inline-flex h-11 min-w-[12rem] items-center justify-center rounded-[5px] bg-ink px-5 text-sm text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "..." : "ارسال درخواست همکاری"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</ClothesShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user