Initial commit: Mana Collection dashboards (Next.js).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-07 20:50:24 +03:30
co-authored by Cursor
commit 18b29f5b62
95 changed files with 16414 additions and 0 deletions
+365
View File
@@ -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>
);
}