Initial commit: Mana Collection dashboards (Next.js).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent, FormEvent, useRef, useState } from "react";
|
||||
import ClothesShell from "@/components/clothes/ClothesShell";
|
||||
import ImageCropper from "@/components/ui/ImageCropper";
|
||||
|
||||
type AddressItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
province: string;
|
||||
city: string;
|
||||
fullAddress: string;
|
||||
landline: string;
|
||||
};
|
||||
|
||||
const DEFAULT_AVATAR = "/images/ryanwar-hanif-98XENwtPMCc-unsplash.jpg";
|
||||
|
||||
const provinces = [
|
||||
"تهران",
|
||||
"اصفهان",
|
||||
"فارس",
|
||||
"خراسان رضوی",
|
||||
"آذربایجان شرقی",
|
||||
"آذربایجان غربی",
|
||||
"گیلان",
|
||||
"مازندران",
|
||||
"خوزستان",
|
||||
"کرمان",
|
||||
"یزد",
|
||||
"قم",
|
||||
"البرز",
|
||||
"همدان",
|
||||
"کردستان",
|
||||
"کرمانشاه",
|
||||
"هرمزگان",
|
||||
"بوشهر",
|
||||
"سیستان و بلوچستان",
|
||||
"گلستان",
|
||||
"قزوین",
|
||||
"زنجان",
|
||||
"اردبیل",
|
||||
"لرستان",
|
||||
"مرکزی",
|
||||
"سمنان",
|
||||
"چهارمحال و بختیاری",
|
||||
"کهگیلویه و بویراحمد",
|
||||
"ایلام",
|
||||
"خراسان شمالی",
|
||||
"خراسان جنوبی",
|
||||
];
|
||||
|
||||
function emptyAddress(): AddressItem {
|
||||
return {
|
||||
id: `addr-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
label: "",
|
||||
province: "",
|
||||
city: "",
|
||||
fullAddress: "",
|
||||
landline: "",
|
||||
};
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [avatar, setAvatar] = useState(DEFAULT_AVATAR);
|
||||
const [nameFa, setNameFa] = useState("سارا محمدی");
|
||||
const [nameEn, setNameEn] = useState("Sara Mohammadi");
|
||||
const [phone] = useState("09121234567");
|
||||
const [email, setEmail] = useState("sara@manacollection.com");
|
||||
const [instagram, setInstagram] = useState("@manacollection");
|
||||
const [about, setAbout] = useState(
|
||||
"علاقهمند به طراحی مینیمال و استایلهای آرام روزمره.",
|
||||
);
|
||||
const [addresses, setAddresses] = useState<AddressItem[]>([
|
||||
{
|
||||
id: "addr-1",
|
||||
label: "منزل",
|
||||
province: "تهران",
|
||||
city: "تهران",
|
||||
fullAddress: "خیابان ولیعصر، بالاتر از پارک وی، پلاک ۱۲",
|
||||
landline: "02122001122",
|
||||
},
|
||||
]);
|
||||
|
||||
function handleAvatarChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
setAvatar(URL.createObjectURL(file));
|
||||
}
|
||||
|
||||
function updateAddress(
|
||||
id: string,
|
||||
field: keyof Omit<AddressItem, "id">,
|
||||
value: string,
|
||||
) {
|
||||
setAddresses((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id ? { ...item, [field]: value } : item,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function addAddress() {
|
||||
setAddresses((prev) => [...prev, emptyAddress()]);
|
||||
}
|
||||
|
||||
function removeAddress(id: string) {
|
||||
setAddresses((prev) =>
|
||||
prev.length <= 1 ? prev : prev.filter((item) => item.id !== id),
|
||||
);
|
||||
}
|
||||
|
||||
function handleSubmit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return (
|
||||
<ClothesShell
|
||||
eyebrow="Profile"
|
||||
title="پروفایل"
|
||||
description="اطلاعات شخصی، تصویر آواتار و آدرسهای خود را مدیریت کنید."
|
||||
breadcrumbs={[
|
||||
{ label: "داشبورد", href: "/" },
|
||||
{ label: "پروفایل" },
|
||||
]}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="grid grid-cols-1 gap-3 lg:grid-cols-[260px_1fr]"
|
||||
>
|
||||
<aside className="dash-enter rounded-lg border border-line bg-surface p-4">
|
||||
<p className="mb-2 text-sm text-ink">آواتار</p>
|
||||
<p className="mb-3 text-xs leading-6 text-muted">نسبت ۱:۱</p>
|
||||
<ImageCropper
|
||||
src={avatar}
|
||||
aspect="1:1"
|
||||
alt="آواتار پروفایل"
|
||||
className="profile-avatar-cropper"
|
||||
/>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="sr-only"
|
||||
onChange={handleAvatarChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 w-full border border-line bg-white px-3 py-2 text-sm text-ink transition-colors hover:border-ink"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
>
|
||||
انتخاب تصویر
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<div className="space-y-3 dash-enter" style={{ animationDelay: "70ms" }}>
|
||||
<section className="grid grid-cols-1 gap-3 rounded-lg border border-line bg-surface p-4 sm:grid-cols-12">
|
||||
<div className="clothes-field sm:col-span-6">
|
||||
<label htmlFor="profile-name-fa">نام فارسی</label>
|
||||
<input
|
||||
id="profile-name-fa"
|
||||
value={nameFa}
|
||||
onChange={(e) => setNameFa(e.target.value)}
|
||||
placeholder="نام و نام خانوادگی"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-6">
|
||||
<label htmlFor="profile-name-en">نام انگلیسی</label>
|
||||
<input
|
||||
id="profile-name-en"
|
||||
dir="ltr"
|
||||
className="!text-end font-montserrat"
|
||||
value={nameEn}
|
||||
onChange={(e) => setNameEn(e.target.value)}
|
||||
placeholder="Full name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-6">
|
||||
<label htmlFor="profile-phone">شماره موبایل</label>
|
||||
<input
|
||||
id="profile-phone"
|
||||
dir="ltr"
|
||||
className="!text-end font-montserrat profile-input-locked"
|
||||
value={phone}
|
||||
readOnly
|
||||
aria-readonly="true"
|
||||
title="شماره موبایل قابل تغییر نیست"
|
||||
/>
|
||||
<span className="text-[0.65rem] text-muted">قابل تغییر نیست</span>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-6">
|
||||
<label htmlFor="profile-email">ایمیل</label>
|
||||
<input
|
||||
id="profile-email"
|
||||
type="email"
|
||||
dir="ltr"
|
||||
className="!text-end font-montserrat"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-12">
|
||||
<label htmlFor="profile-instagram">اینستاگرام</label>
|
||||
<input
|
||||
id="profile-instagram"
|
||||
dir="ltr"
|
||||
className="!text-end font-montserrat"
|
||||
value={instagram}
|
||||
onChange={(e) => setInstagram(e.target.value)}
|
||||
placeholder="@username"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-12">
|
||||
<label htmlFor="profile-about">درباره من</label>
|
||||
<textarea
|
||||
id="profile-about"
|
||||
value={about}
|
||||
onChange={(e) => setAbout(e.target.value)}
|
||||
placeholder="چند خط درباره خودتان..."
|
||||
className="!min-h-[6rem]"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-line bg-surface p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-sm text-ink">آدرسها</p>
|
||||
<p className="text-xs text-muted">
|
||||
میتوانید چند آدرس اضافه کنید.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addAddress}
|
||||
className="border border-ink bg-ink px-3 py-1.5 text-xs text-white transition-colors hover:bg-white hover:text-ink"
|
||||
>
|
||||
+ آدرس جدید
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{addresses.map((address, index) => (
|
||||
<div key={address.id} className="address-block">
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<p className="text-sm text-ink">
|
||||
آدرس {new Intl.NumberFormat("fa-IR").format(index + 1)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-muted transition-colors hover:text-ink disabled:opacity-30"
|
||||
disabled={addresses.length <= 1}
|
||||
onClick={() => removeAddress(address.id)}
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-12">
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor={`addr-label-${address.id}`}>برچسب</label>
|
||||
<input
|
||||
id={`addr-label-${address.id}`}
|
||||
value={address.label}
|
||||
onChange={(e) =>
|
||||
updateAddress(address.id, "label", e.target.value)
|
||||
}
|
||||
placeholder="منزل، محل کار..."
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor={`addr-province-${address.id}`}>
|
||||
استان
|
||||
</label>
|
||||
<select
|
||||
id={`addr-province-${address.id}`}
|
||||
value={address.province}
|
||||
onChange={(e) =>
|
||||
updateAddress(address.id, "province", e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="" disabled>
|
||||
انتخاب استان
|
||||
</option>
|
||||
{provinces.map((province) => (
|
||||
<option key={province} value={province}>
|
||||
{province}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor={`addr-city-${address.id}`}>شهر</label>
|
||||
<input
|
||||
id={`addr-city-${address.id}`}
|
||||
value={address.city}
|
||||
onChange={(e) =>
|
||||
updateAddress(address.id, "city", e.target.value)
|
||||
}
|
||||
placeholder="نام شهر"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-8">
|
||||
<label htmlFor={`addr-full-${address.id}`}>
|
||||
آدرس کامل
|
||||
</label>
|
||||
<textarea
|
||||
id={`addr-full-${address.id}`}
|
||||
value={address.fullAddress}
|
||||
onChange={(e) =>
|
||||
updateAddress(
|
||||
address.id,
|
||||
"fullAddress",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="خیابان، کوچه، پلاک، واحد..."
|
||||
className="!min-h-[4.5rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="clothes-field sm:col-span-4">
|
||||
<label htmlFor={`addr-landline-${address.id}`}>
|
||||
تلفن ثابت
|
||||
</label>
|
||||
<input
|
||||
id={`addr-landline-${address.id}`}
|
||||
dir="ltr"
|
||||
className="!text-end font-montserrat"
|
||||
inputMode="tel"
|
||||
value={address.landline}
|
||||
onChange={(e) =>
|
||||
updateAddress(address.id, "landline", e.target.value)
|
||||
}
|
||||
placeholder="021..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="border border-line px-5 py-2.5 text-sm text-muted hover:text-ink"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="auth-button !mt-0 !max-w-none !w-auto px-6"
|
||||
>
|
||||
<span>ذخیره پروفایل</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ClothesShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user