feat: multi-popular toggle for pricing, BYN price field for master classes
- Replace single popular dropdown with per-item toggle switch in pricing admin - Add PriceField component to master classes admin (strips/adds BYN suffix) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,30 @@ import { ArrayEditor } from "../_components/ArrayEditor";
|
|||||||
import { Plus, X, Upload, Loader2, ImageIcon, AlertCircle, Check, ChevronDown, ChevronUp, Instagram, Send, Trash2, Pencil } from "lucide-react";
|
import { Plus, X, Upload, Loader2, ImageIcon, AlertCircle, Check, ChevronDown, ChevronUp, Instagram, Send, Trash2, Pencil } from "lucide-react";
|
||||||
import type { MasterClassItem, MasterClassSlot } from "@/types/content";
|
import type { MasterClassItem, MasterClassSlot } from "@/types/content";
|
||||||
|
|
||||||
|
function PriceField({ label, value, onChange, placeholder }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string }) {
|
||||||
|
const raw = value.replace(/\s*BYN\s*$/i, "").trim();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
|
||||||
|
<div className="flex rounded-lg border border-white/10 bg-neutral-800 focus-within:border-gold transition-colors">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={raw}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
onChange(v ? `${v} BYN` : "");
|
||||||
|
}}
|
||||||
|
placeholder={placeholder ?? "0"}
|
||||||
|
className="flex-1 bg-transparent px-4 py-2.5 text-white placeholder-neutral-500 outline-none min-w-0"
|
||||||
|
/>
|
||||||
|
<span className="flex items-center pr-4 text-sm font-medium text-gold select-none">
|
||||||
|
BYN
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface MasterClassesData {
|
interface MasterClassesData {
|
||||||
title: string;
|
title: string;
|
||||||
successMessage?: string;
|
successMessage?: string;
|
||||||
@@ -888,11 +912,11 @@ export default function MasterClassesEditorPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InputField
|
<PriceField
|
||||||
label="Стоимость"
|
label="Стоимость"
|
||||||
value={item.cost}
|
value={item.cost}
|
||||||
onChange={(v) => updateItem({ ...item, cost: v })}
|
onChange={(v) => updateItem({ ...item, cost: v })}
|
||||||
placeholder="40 BYN"
|
placeholder="40"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{locations.length > 0 && (
|
{locations.length > 0 && (
|
||||||
|
|||||||
@@ -63,45 +63,28 @@ export default function PricingEditorPage() {
|
|||||||
onChange={(v) => update({ ...data, subtitle: v })}
|
onChange={(v) => update({ ...data, subtitle: v })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Popular & Featured selectors */}
|
{/* Featured selector */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const itemOptions = data.items
|
const itemOptions = data.items
|
||||||
.map((it, idx) => ({ value: String(idx), label: it.name }))
|
.map((it, idx) => ({ value: String(idx), label: it.name }))
|
||||||
.filter((o) => o.label.trim() !== "");
|
.filter((o) => o.label.trim() !== "");
|
||||||
const noneOption = { value: "", label: "— Нет —" };
|
const noneOption = { value: "", label: "— Нет —" };
|
||||||
|
|
||||||
const popularIdx = data.items.findIndex((it) => it.popular);
|
|
||||||
const featuredIdx = data.items.findIndex((it) => it.featured);
|
const featuredIdx = data.items.findIndex((it) => it.featured);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<SelectField
|
||||||
<SelectField
|
label="Выделенный абонемент (безлимит)"
|
||||||
label="Популярный абонемент"
|
value={featuredIdx >= 0 ? String(featuredIdx) : ""}
|
||||||
value={popularIdx >= 0 ? String(popularIdx) : ""}
|
onChange={(v) => {
|
||||||
onChange={(v) => {
|
const items = data.items.map((it, idx) => ({
|
||||||
const items = data.items.map((it, idx) => ({
|
...it,
|
||||||
...it,
|
featured: v ? idx === Number(v) : false,
|
||||||
popular: v ? idx === Number(v) : false,
|
}));
|
||||||
}));
|
update({ ...data, items });
|
||||||
update({ ...data, items });
|
}}
|
||||||
}}
|
options={[noneOption, ...itemOptions]}
|
||||||
options={[noneOption, ...itemOptions]}
|
placeholder="Выберите..."
|
||||||
placeholder="Выберите..."
|
/>
|
||||||
/>
|
|
||||||
<SelectField
|
|
||||||
label="Выделенный абонемент (безлимит)"
|
|
||||||
value={featuredIdx >= 0 ? String(featuredIdx) : ""}
|
|
||||||
onChange={(v) => {
|
|
||||||
const items = data.items.map((it, idx) => ({
|
|
||||||
...it,
|
|
||||||
featured: v ? idx === Number(v) : false,
|
|
||||||
}));
|
|
||||||
update({ ...data, items });
|
|
||||||
}}
|
|
||||||
options={[noneOption, ...itemOptions]}
|
|
||||||
placeholder="Выберите..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
@@ -110,22 +93,42 @@ export default function PricingEditorPage() {
|
|||||||
items={data.items}
|
items={data.items}
|
||||||
onChange={(items) => update({ ...data, items })}
|
onChange={(items) => update({ ...data, items })}
|
||||||
renderItem={(item, _i, updateItem) => (
|
renderItem={(item, _i, updateItem) => (
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="space-y-3">
|
||||||
<InputField
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
label="Название"
|
<InputField
|
||||||
value={item.name}
|
label="Название"
|
||||||
onChange={(v) => updateItem({ ...item, name: v })}
|
value={item.name}
|
||||||
/>
|
onChange={(v) => updateItem({ ...item, name: v })}
|
||||||
<PriceField
|
/>
|
||||||
label="Цена"
|
<PriceField
|
||||||
value={item.price}
|
label="Цена"
|
||||||
onChange={(v) => updateItem({ ...item, price: v })}
|
value={item.price}
|
||||||
/>
|
onChange={(v) => updateItem({ ...item, price: v })}
|
||||||
<InputField
|
/>
|
||||||
label="Примечание"
|
<InputField
|
||||||
value={item.note || ""}
|
label="Примечание"
|
||||||
onChange={(v) => updateItem({ ...item, note: v })}
|
value={item.note || ""}
|
||||||
/>
|
onChange={(v) => updateItem({ ...item, note: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center gap-2 cursor-pointer select-none">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={!!item.popular}
|
||||||
|
onClick={() => updateItem({ ...item, popular: !item.popular })}
|
||||||
|
className={`relative h-5 w-9 rounded-full transition-colors ${
|
||||||
|
item.popular ? "bg-gold" : "bg-neutral-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white transition-transform ${
|
||||||
|
item.popular ? "translate-x-4" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-neutral-400">Популярный</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
createItem={() => ({ name: "", price: "", note: "" })}
|
createItem={() => ({ name: "", price: "", note: "" })}
|
||||||
|
|||||||
Reference in New Issue
Block a user