Add password modal and VIP product controls on clothes list.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1041ab20c5
commit
4a516e31db
@@ -8,6 +8,7 @@ import IconButton, {
|
|||||||
DiscountIcon,
|
DiscountIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
|
SubscriptionIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
VariantIcon,
|
VariantIcon,
|
||||||
} from "@/components/ui/IconButton";
|
} from "@/components/ui/IconButton";
|
||||||
@@ -94,6 +95,14 @@ export default function ProductListPage() {
|
|||||||
const [discountProduct, setDiscountProduct] = useState<Product | null>(null);
|
const [discountProduct, setDiscountProduct] = useState<Product | null>(null);
|
||||||
const [discountRows, setDiscountRows] = useState<ProductVariantPrice[]>([]);
|
const [discountRows, setDiscountRows] = useState<ProductVariantPrice[]>([]);
|
||||||
|
|
||||||
|
const [vipProduct, setVipProduct] = useState<Product | null>(null);
|
||||||
|
const [vipEnabled, setVipEnabled] = useState(false);
|
||||||
|
const [vipOnly, setVipOnly] = useState(false);
|
||||||
|
const [vipSpecialPrice, setVipSpecialPrice] = useState(false);
|
||||||
|
const [vipRows, setVipRows] = useState<ProductVariantPrice[]>([]);
|
||||||
|
const [vipSaving, setVipSaving] = useState(false);
|
||||||
|
const [vipError, setVipError] = useState<string | null>(null);
|
||||||
|
|
||||||
const reload = useCallback(async () => {
|
const reload = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadError(null);
|
setLoadError(null);
|
||||||
@@ -178,6 +187,99 @@ export default function ProductListPage() {
|
|||||||
setDiscountProduct(null);
|
setDiscountProduct(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openVip(product: Product) {
|
||||||
|
setVipProduct(product);
|
||||||
|
setVipEnabled(Boolean(product.vipEnabled));
|
||||||
|
setVipOnly(Boolean(product.vipOnly));
|
||||||
|
setVipSpecialPrice(Boolean(product.vipSpecialPrice));
|
||||||
|
if (product.variantPrices?.length) {
|
||||||
|
setVipRows(product.variantPrices.map((row) => ({ ...row })));
|
||||||
|
} else {
|
||||||
|
setVipRows([
|
||||||
|
{
|
||||||
|
...emptyVariantRow(String(product.price)),
|
||||||
|
vipPrice:
|
||||||
|
typeof product.vipPrice === "number"
|
||||||
|
? String(product.vipPrice)
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
setVipError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeVip() {
|
||||||
|
setVipProduct(null);
|
||||||
|
setVipRows([]);
|
||||||
|
setVipError(null);
|
||||||
|
setVipSaving(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVipPrice(id: string, value: string) {
|
||||||
|
setVipRows((prev) =>
|
||||||
|
prev.map((row) => (row.id === id ? { ...row, vipPrice: value } : row)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveVip(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!vipProduct) return;
|
||||||
|
setVipSaving(true);
|
||||||
|
setVipError(null);
|
||||||
|
|
||||||
|
const enabled = vipEnabled;
|
||||||
|
const only = enabled && vipOnly;
|
||||||
|
const special = enabled && vipSpecialPrice;
|
||||||
|
const hadVariants = Boolean(vipProduct.variantPrices?.length);
|
||||||
|
|
||||||
|
let nextVipPrice: number | null = null;
|
||||||
|
let nextVariants: ReturnType<typeof toVariantPayload> | undefined;
|
||||||
|
|
||||||
|
if (special) {
|
||||||
|
const nextRows = vipRows.map((row) => ({ ...row }));
|
||||||
|
const priced = nextRows
|
||||||
|
.map((row) => Number(row.vipPrice))
|
||||||
|
.filter((n) => !Number.isNaN(n) && n > 0);
|
||||||
|
const baseVip = nextRows.find((row) => isBaseRow(row))?.vipPrice;
|
||||||
|
nextVipPrice = baseVip
|
||||||
|
? Number(baseVip) || null
|
||||||
|
: priced.length
|
||||||
|
? Math.min(...priced)
|
||||||
|
: null;
|
||||||
|
if (hadVariants) {
|
||||||
|
nextVariants = toVariantPayload(nextRows);
|
||||||
|
}
|
||||||
|
} else if (hadVariants) {
|
||||||
|
nextVariants = toVariantPayload(
|
||||||
|
(vipProduct.variantPrices ?? []).map((row) => ({
|
||||||
|
...row,
|
||||||
|
vipPrice: "",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await updateProduct(vipProduct.id, {
|
||||||
|
vipEnabled: enabled,
|
||||||
|
vipOnly: only,
|
||||||
|
vipSpecialPrice: special,
|
||||||
|
vipPrice: special ? nextVipPrice : null,
|
||||||
|
...(nextVariants ? { variants: nextVariants } : {}),
|
||||||
|
});
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((item) => (item.id === updated.id ? updated : item)),
|
||||||
|
);
|
||||||
|
if (detailProduct?.id === updated.id) setDetailProduct(updated);
|
||||||
|
closeVip();
|
||||||
|
} catch (err) {
|
||||||
|
setVipError(
|
||||||
|
err instanceof ApiError ? err.message : "ذخیره دسترسی VIP ناموفق بود.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setVipSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateRow(
|
function updateRow(
|
||||||
id: string,
|
id: string,
|
||||||
field: keyof Omit<ProductVariantPrice, "id">,
|
field: keyof Omit<ProductVariantPrice, "id">,
|
||||||
@@ -483,6 +585,16 @@ export default function ProductListPage() {
|
|||||||
>
|
>
|
||||||
<DiscountIcon />
|
<DiscountIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
label="دسترسی VIP"
|
||||||
|
variant="ghost"
|
||||||
|
className={
|
||||||
|
product.vipEnabled ? "customer-sub-premium" : undefined
|
||||||
|
}
|
||||||
|
onClick={() => openVip(product)}
|
||||||
|
>
|
||||||
|
<SubscriptionIcon premium={Boolean(product.vipEnabled)} />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
@@ -845,12 +957,11 @@ export default function ProductListPage() {
|
|||||||
: "بدون تنوع";
|
: "بدون تنوع";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={row.id} className="variant-price-row">
|
<div key={row.id} className="variant-price-row variant-price-row-inline">
|
||||||
<div className="mb-2 text-sm text-ink">{label}</div>
|
<div className="variant-price-label">{label}</div>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
||||||
<div className="clothes-field">
|
<div className="clothes-field">
|
||||||
<span className="block text-xs text-muted">قیمت فعلی</span>
|
<span className="block text-xs text-muted">قیمت فعلی</span>
|
||||||
<p className="mt-1.5 text-sm text-ink">
|
<p className="text-sm text-ink">
|
||||||
<span className="font-montserrat">
|
<span className="font-montserrat">
|
||||||
{Number.isNaN(currentPrice)
|
{Number.isNaN(currentPrice)
|
||||||
? row.price || "—"
|
? row.price || "—"
|
||||||
@@ -876,10 +987,157 @@ export default function ProductListPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
|
<FormModal
|
||||||
|
open={Boolean(vipProduct)}
|
||||||
|
title={`دسترسی VIP — ${vipProduct?.nameFa ?? ""}`}
|
||||||
|
onClose={closeVip}
|
||||||
|
onSubmit={(e) => void saveVip(e)}
|
||||||
|
submitLabel={vipSaving ? "..." : "ذخیره"}
|
||||||
|
wide="xl"
|
||||||
|
>
|
||||||
|
{vipError ? (
|
||||||
|
<p className="text-sm text-red-600" role="alert">
|
||||||
|
{vipError}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<label className="mana-switch">
|
||||||
|
<span className="mana-switch-copy">
|
||||||
|
<span className="mana-switch-title">فعالسازی VIP</span>
|
||||||
|
<span className="mana-switch-hint">
|
||||||
|
{vipEnabled
|
||||||
|
? "تنظیمات VIP برای این محصول فعال است."
|
||||||
|
: "برای تنظیم دسترسی و قیمت VIP، این گزینه را روشن کنید."}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="mana-switch-track"
|
||||||
|
data-on={vipEnabled ? "true" : "false"}
|
||||||
|
>
|
||||||
|
<span className="mana-switch-thumb" />
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={vipEnabled}
|
||||||
|
checked={vipEnabled}
|
||||||
|
onChange={(e) => setVipEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="vip-modal-options"
|
||||||
|
data-disabled={vipEnabled ? "false" : "true"}
|
||||||
|
aria-disabled={!vipEnabled}
|
||||||
|
>
|
||||||
|
<label className="mana-switch">
|
||||||
|
<span className="mana-switch-copy">
|
||||||
|
<span className="mana-switch-title">
|
||||||
|
نمایش ندادن به مشتری عادی
|
||||||
|
</span>
|
||||||
|
<span className="mana-switch-hint">
|
||||||
|
محصول اصلاً برای مشتریان عادی نمایش داده نمیشود.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="mana-switch-track"
|
||||||
|
data-on={vipOnly ? "true" : "false"}
|
||||||
|
>
|
||||||
|
<span className="mana-switch-thumb" />
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={vipOnly}
|
||||||
|
checked={vipOnly}
|
||||||
|
disabled={!vipEnabled}
|
||||||
|
onChange={(e) => setVipOnly(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="mana-switch">
|
||||||
|
<span className="mana-switch-copy">
|
||||||
|
<span className="mana-switch-title">قیمت ویژه VIP</span>
|
||||||
|
<span className="mana-switch-hint">
|
||||||
|
برای اعضای VIP قیمت جداگانه تعریف کنید.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="mana-switch-track"
|
||||||
|
data-on={vipSpecialPrice ? "true" : "false"}
|
||||||
|
>
|
||||||
|
<span className="mana-switch-thumb" />
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={vipSpecialPrice}
|
||||||
|
checked={vipSpecialPrice}
|
||||||
|
disabled={!vipEnabled}
|
||||||
|
onChange={(e) => setVipSpecialPrice(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{vipSpecialPrice ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs leading-6 text-muted">
|
||||||
|
تنوعهای موجود و قیمت فعلی را ببینید و قیمت VIP را وارد کنید.
|
||||||
|
</p>
|
||||||
|
{vipRows.map((row) => {
|
||||||
|
const currentPrice = Number(row.price);
|
||||||
|
const label = vipProduct
|
||||||
|
? getVariantLabel(
|
||||||
|
row,
|
||||||
|
vipProduct.categoryId,
|
||||||
|
categoryTree,
|
||||||
|
)
|
||||||
|
: "بدون تنوع";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={row.id}
|
||||||
|
className="variant-price-row variant-price-row-inline"
|
||||||
|
>
|
||||||
|
<div className="variant-price-label">{label}</div>
|
||||||
|
<div className="clothes-field">
|
||||||
|
<span className="block text-xs text-muted">قیمت فعلی</span>
|
||||||
|
<p className="text-sm text-ink">
|
||||||
|
<span className="font-montserrat">
|
||||||
|
{Number.isNaN(currentPrice)
|
||||||
|
? row.price || "—"
|
||||||
|
: formatPrice(currentPrice)}
|
||||||
|
</span>
|
||||||
|
{!Number.isNaN(currentPrice) && row.price ? (
|
||||||
|
<span className="ms-1">تومان</span>
|
||||||
|
) : null}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="clothes-field">
|
||||||
|
<label htmlFor={`vip-price-${row.id}`}>قیمت VIP</label>
|
||||||
|
<input
|
||||||
|
id={`vip-price-${row.id}`}
|
||||||
|
dir="ltr"
|
||||||
|
inputMode="numeric"
|
||||||
|
className="!text-end font-montserrat"
|
||||||
|
value={row.vipPrice ?? ""}
|
||||||
|
disabled={!vipEnabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateVipPrice(row.id, e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="3570000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+193
-32
@@ -7,6 +7,7 @@
|
|||||||
--muted: #6b6b6b;
|
--muted: #6b6b6b;
|
||||||
--line: #e4e4e4;
|
--line: #e4e4e4;
|
||||||
--ink: #0a0a0a;
|
--ink: #0a0a0a;
|
||||||
|
--radius-card: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -297,7 +298,7 @@ body {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
padding: 1.15rem 1rem;
|
padding: 1.15rem 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -366,7 +367,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dash-chart {
|
.dash-chart {
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clothes admin */
|
/* Clothes admin */
|
||||||
@@ -378,7 +379,7 @@ body {
|
|||||||
background: var(--ink);
|
background: var(--ink);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0.65rem 1rem;
|
padding: 0.65rem 1rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
transition:
|
transition:
|
||||||
color 0.35s ease,
|
color 0.35s ease,
|
||||||
background-color 0.35s ease,
|
background-color 0.35s ease,
|
||||||
@@ -411,7 +412,7 @@ body {
|
|||||||
padding: 0.45rem 0.6rem;
|
padding: 0.45rem 0.6rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s ease;
|
transition: border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -445,7 +446,7 @@ body {
|
|||||||
max-height: 16rem;
|
max-height: 16rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.1);
|
||||||
padding: 0.35rem 0;
|
padding: 0.35rem 0;
|
||||||
@@ -533,7 +534,7 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
transition:
|
transition:
|
||||||
border-color 0.3s ease,
|
border-color 0.3s ease,
|
||||||
transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
|
transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
@@ -548,7 +549,7 @@ body {
|
|||||||
|
|
||||||
.product-card-actions {
|
.product-card-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +567,7 @@ body {
|
|||||||
.tree-box {
|
.tree-box {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +584,7 @@ body {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.85rem 0.95rem;
|
padding: 0.85rem 0.95rem;
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
@@ -612,7 +613,7 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
color 0.2s ease,
|
color 0.2s ease,
|
||||||
@@ -637,7 +638,7 @@ body {
|
|||||||
content: attr(data-tooltip);
|
content: attr(data-tooltip);
|
||||||
bottom: calc(100% + 8px);
|
bottom: calc(100% + 8px);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
background: var(--ink);
|
background: var(--ink);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
@@ -702,7 +703,7 @@ body {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.65rem;
|
padding: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +727,7 @@ body {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
transition: border-color 0.25s ease;
|
transition: border-color 0.25s ease;
|
||||||
}
|
}
|
||||||
@@ -741,7 +742,7 @@ body {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
transition: border-color 0.25s ease;
|
transition: border-color 0.25s ease;
|
||||||
}
|
}
|
||||||
@@ -789,7 +790,7 @@ body {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
transition: border-color 0.25s ease;
|
transition: border-color 0.25s ease;
|
||||||
}
|
}
|
||||||
@@ -804,7 +805,7 @@ body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
aspect-ratio: 3 / 2;
|
aspect-ratio: 3 / 2;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
background: #f3f3f1;
|
background: #f3f3f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +817,7 @@ body {
|
|||||||
|
|
||||||
.rich-text {
|
.rich-text {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -878,7 +879,7 @@ body {
|
|||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0.75rem 0;
|
margin: 0.75rem 0;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
@@ -908,7 +909,7 @@ body {
|
|||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
padding: 0 0.7rem;
|
padding: 0 0.7rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -960,7 +961,7 @@ body {
|
|||||||
max-height: min(92dvh, 40rem);
|
max-height: min(92dvh, 40rem);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.16);
|
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.16);
|
||||||
animation: modal-in 0.22s ease;
|
animation: modal-in 0.22s ease;
|
||||||
@@ -1014,7 +1015,7 @@ body {
|
|||||||
font-size: 1.35rem;
|
font-size: 1.35rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover {
|
.modal-close:hover {
|
||||||
@@ -1036,7 +1037,7 @@ body {
|
|||||||
|
|
||||||
.modal-btn-ghost,
|
.modal-btn-ghost,
|
||||||
.modal-btn-solid {
|
.modal-btn-solid {
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.55rem 1rem;
|
padding: 0.55rem 1rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1092,7 +1093,7 @@ body {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: min(100%, 22rem);
|
width: min(100%, 22rem);
|
||||||
border: 1px solid #e8d0d2;
|
border: 1px solid #e8d0d2;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 1.25rem 1.15rem 1.1rem;
|
padding: 1.25rem 1.15rem 1.1rem;
|
||||||
box-shadow: 0 24px 60px rgba(120, 10, 20, 0.22);
|
box-shadow: 0 24px 60px rgba(120, 10, 20, 0.22);
|
||||||
@@ -1123,7 +1124,7 @@ body {
|
|||||||
.confirm-btn-danger {
|
.confirm-btn-danger {
|
||||||
min-width: 5.5rem;
|
min-width: 5.5rem;
|
||||||
padding: 0.55rem 1rem;
|
padding: 0.55rem 1rem;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -1165,7 +1166,7 @@ body {
|
|||||||
width: 4.5rem;
|
width: 4.5rem;
|
||||||
height: 4.5rem;
|
height: 4.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
border: 1px dashed var(--line);
|
border: 1px dashed var(--line);
|
||||||
background: #f3f3f1;
|
background: #f3f3f1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -1189,7 +1190,7 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.7rem 1.35rem;
|
padding: 0.7rem 1.35rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -1273,7 +1274,7 @@ body {
|
|||||||
0 8px,
|
0 8px,
|
||||||
8px -8px,
|
8px -8px,
|
||||||
-8px 0;
|
-8px 0;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
transition: aspect-ratio 0.25s ease;
|
transition: aspect-ratio 0.25s ease;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -1307,14 +1308,14 @@ body {
|
|||||||
|
|
||||||
.address-block {
|
.address-block {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 0.9rem;
|
padding: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.variant-type-block {
|
.variant-type-block {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background: #fafaf8;
|
background: #fafaf8;
|
||||||
}
|
}
|
||||||
@@ -1391,11 +1392,40 @@ body {
|
|||||||
|
|
||||||
.variant-price-row {
|
.variant-price-row {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.variant-price-row-inline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.variant-price-row-inline {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-price-row-inline .variant-price-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-price-row-inline .clothes-field {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-price-row-inline .clothes-field label,
|
||||||
|
.variant-price-row-inline .clothes-field > span {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.variant-price-grid {
|
.variant-price-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1480,7 +1510,7 @@ body {
|
|||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -1517,7 +1547,7 @@ body {
|
|||||||
.gallery-slot {
|
.gallery-slot {
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border: 1px dashed var(--line);
|
border: 1px dashed var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-card);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1543,3 +1573,134 @@ body {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mana-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.95rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-copy {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 3.1rem;
|
||||||
|
height: 1.55rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: #f3f3f1;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track[data-on="true"] {
|
||||||
|
border-color: var(--ink);
|
||||||
|
background: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track::before,
|
||||||
|
.mana-switch-track::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-family: var(--font-montserrat), system-ui, sans-serif;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track::before {
|
||||||
|
content: "0";
|
||||||
|
inset-inline-start: 0.35rem;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track::after {
|
||||||
|
content: "1";
|
||||||
|
inset-inline-end: 0.4rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track[data-on="true"]::before {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track[data-on="true"]::after {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
inset-inline-start: 2px;
|
||||||
|
z-index: 1;
|
||||||
|
width: 1.15rem;
|
||||||
|
height: 1.15rem;
|
||||||
|
border-radius: calc(var(--radius-card) - 1px);
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
transition: inset-inline-start 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch-track[data-on="true"] .mana-switch-thumb {
|
||||||
|
inset-inline-start: calc(100% - 1.15rem - 2px);
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-switch input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-modal-options {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
transition:
|
||||||
|
filter 0.25s ease,
|
||||||
|
opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-modal-options[data-disabled="true"] {
|
||||||
|
filter: blur(2.5px);
|
||||||
|
opacity: 0.45;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vip-modal-options[data-disabled="true"]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
|
import { FormModal } from "@/components/ui/Modal";
|
||||||
|
import { ApiError, api } from "@/lib/api";
|
||||||
|
|
||||||
|
type ChangePasswordModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
userName?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChangePasswordModal({
|
||||||
|
open,
|
||||||
|
userName,
|
||||||
|
onClose,
|
||||||
|
}: ChangePasswordModalProps) {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
setError(null);
|
||||||
|
setSaving(false);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
const next = password.trim();
|
||||||
|
if (next.length < 4) {
|
||||||
|
setError("رمز عبور باید حداقل ۴ کاراکتر باشد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (next !== confirmPassword) {
|
||||||
|
setError("تکرار رمز عبور مطابقت ندارد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await api("/auth/password", {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { password: next },
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
setError(
|
||||||
|
err instanceof ApiError ? err.message : "تغییر رمز عبور ناموفق بود.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
open={open}
|
||||||
|
title="تغییر رمز عبور"
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitLabel={saving ? "..." : "ذخیره"}
|
||||||
|
>
|
||||||
|
{userName ? (
|
||||||
|
<p className="text-sm text-muted">{userName}</p>
|
||||||
|
) : null}
|
||||||
|
{error ? (
|
||||||
|
<p className="text-sm text-red-600" role="alert">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<div className="clothes-field">
|
||||||
|
<label htmlFor="change-password-new">رمز عبور جدید</label>
|
||||||
|
<input
|
||||||
|
id="change-password-new"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
dir="ltr"
|
||||||
|
className="!text-end"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={4}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="clothes-field">
|
||||||
|
<label htmlFor="change-password-confirm">تکرار رمز عبور جدید</label>
|
||||||
|
<input
|
||||||
|
id="change-password-confirm"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
dir="ltr"
|
||||||
|
className="!text-end"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import ChangePasswordModal from "@/components/dashboard/ChangePasswordModal";
|
||||||
import ManaLogo from "@/components/ui/ManaLogo";
|
import ManaLogo from "@/components/ui/ManaLogo";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export default function DashboardHeader({
|
|||||||
const displayName = userName ?? user?.name ?? "کاربر";
|
const displayName = userName ?? user?.name ?? "کاربر";
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [userOpen, setUserOpen] = useState(false);
|
const [userOpen, setUserOpen] = useState(false);
|
||||||
|
const [passwordOpen, setPasswordOpen] = useState(false);
|
||||||
const userRef = useRef<HTMLDivElement>(null);
|
const userRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -87,15 +89,18 @@ export default function DashboardHeader({
|
|||||||
<UserIcon />
|
<UserIcon />
|
||||||
پروفایل
|
پروفایل
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<button
|
||||||
href="/change-password"
|
type="button"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
className="dash-menu-item"
|
className="dash-menu-item w-full"
|
||||||
onClick={() => setUserOpen(false)}
|
onClick={() => {
|
||||||
|
setUserOpen(false);
|
||||||
|
setPasswordOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<KeyIcon />
|
<KeyIcon />
|
||||||
تغییر رمز عبور
|
تغییر رمز عبور
|
||||||
</Link>
|
</button>
|
||||||
<div className="h-px bg-line" />
|
<div className="h-px bg-line" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -136,6 +141,12 @@ export default function DashboardHeader({
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ChangePasswordModal
|
||||||
|
open={passwordOpen}
|
||||||
|
userName={displayName}
|
||||||
|
onClose={() => setPasswordOpen(false)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,22 +115,16 @@ export function AddChildIcon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SubscriptionIcon({ premium = false }: { premium?: boolean }) {
|
export function SubscriptionIcon({ premium = false }: { premium?: boolean }) {
|
||||||
if (premium) {
|
|
||||||
return (
|
return (
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden>
|
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden>
|
||||||
<path
|
<path
|
||||||
{...stroke}
|
stroke="currentColor"
|
||||||
d="M5.5 9.5 8.2 12l3.8-5.5 3.8 5.5 2.7-2.5V17.5H5.5V9.5Z"
|
strokeWidth="1"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill={premium ? "currentColor" : "none"}
|
||||||
|
d="M12 4.5 13.9 9.2l5.1.4-3.9 3.3 1.2 4.9L12 15.4 7.7 17.8l1.2-4.9-3.9-3.3 5.1-.4L12 4.5Z"
|
||||||
/>
|
/>
|
||||||
<path {...stroke} d="M5.5 17.5h13" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden>
|
|
||||||
<circle {...stroke} cx="12" cy="9" r="3.2" />
|
|
||||||
<path {...stroke} d="M6.5 18.5c1.4-2.6 3.3-3.9 5.5-3.9s4.1 1.3 5.5 3.9" />
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type ProductVariantPrice = {
|
|||||||
packageId: string;
|
packageId: string;
|
||||||
price: string;
|
price: string;
|
||||||
discountPrice?: string;
|
discountPrice?: string;
|
||||||
|
vipPrice?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
@@ -41,6 +42,10 @@ export type Product = {
|
|||||||
image: string;
|
image: string;
|
||||||
gallery?: string[];
|
gallery?: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
vipEnabled?: boolean;
|
||||||
|
vipOnly?: boolean;
|
||||||
|
vipSpecialPrice?: boolean;
|
||||||
|
vipPrice?: number;
|
||||||
variantPrices?: ProductVariantPrice[];
|
variantPrices?: ProductVariantPrice[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type ApiProductVariant = {
|
|||||||
packageId: string;
|
packageId: string;
|
||||||
price: number;
|
price: number;
|
||||||
discountPrice: number | null;
|
discountPrice: number | null;
|
||||||
|
vipPrice: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiProduct = {
|
export type ApiProduct = {
|
||||||
@@ -28,6 +29,10 @@ export type ApiProduct = {
|
|||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
galleryUrls?: string[];
|
galleryUrls?: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
vipEnabled?: boolean;
|
||||||
|
vipOnly?: boolean;
|
||||||
|
vipSpecialPrice?: boolean;
|
||||||
|
vipPrice?: number | null;
|
||||||
variants: ApiProductVariant[];
|
variants: ApiProductVariant[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,6 +57,7 @@ export function mapVariant(v: ApiProductVariant): ProductVariantPrice {
|
|||||||
price: String(v.price ?? ""),
|
price: String(v.price ?? ""),
|
||||||
discountPrice:
|
discountPrice:
|
||||||
v.discountPrice != null ? String(v.discountPrice) : undefined,
|
v.discountPrice != null ? String(v.discountPrice) : undefined,
|
||||||
|
vipPrice: v.vipPrice != null ? String(v.vipPrice) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +75,10 @@ export function mapProduct(p: ApiProduct): Product {
|
|||||||
image: p.imageUrl || "/images/ahmad-ebadi-r5IzkiO08JU-unsplash.jpg",
|
image: p.imageUrl || "/images/ahmad-ebadi-r5IzkiO08JU-unsplash.jpg",
|
||||||
gallery: p.galleryUrls ?? [],
|
gallery: p.galleryUrls ?? [],
|
||||||
tags: p.tags ?? [],
|
tags: p.tags ?? [],
|
||||||
|
vipEnabled: Boolean(p.vipEnabled),
|
||||||
|
vipOnly: Boolean(p.vipOnly),
|
||||||
|
vipSpecialPrice: Boolean(p.vipSpecialPrice),
|
||||||
|
vipPrice: p.vipPrice ?? undefined,
|
||||||
variantPrices: (p.variants ?? []).map(mapVariant),
|
variantPrices: (p.variants ?? []).map(mapVariant),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -83,6 +93,7 @@ export function toVariantPayload(rows: ProductVariantPrice[]) {
|
|||||||
discountPrice: row.discountPrice
|
discountPrice: row.discountPrice
|
||||||
? Number(row.discountPrice) || undefined
|
? Number(row.discountPrice) || undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
|
vipPrice: row.vipPrice ? Number(row.vipPrice) || undefined : undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user