feat: upgrade pricing admin with popular/featured selects and price input with BYN badge
Replace per-item toggles with top-level dropdown selects for popular and featured items. Add PriceField component with inline gold BYN suffix badge. Public Pricing component now uses dynamic popular/featured flags from data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,52 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { SectionEditor } from "../_components/SectionEditor";
|
import { SectionEditor } from "../_components/SectionEditor";
|
||||||
import { InputField } from "../_components/FormField";
|
import { InputField, SelectField } from "../_components/FormField";
|
||||||
import { ArrayEditor } from "../_components/ArrayEditor";
|
import { ArrayEditor } from "../_components/ArrayEditor";
|
||||||
|
|
||||||
|
interface PricingItem {
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
note?: string;
|
||||||
|
popular?: boolean;
|
||||||
|
featured?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface PricingData {
|
interface PricingData {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
items: { name: string; price: string; note?: string }[];
|
items: PricingItem[];
|
||||||
rentalTitle: string;
|
rentalTitle: string;
|
||||||
rentalItems: { name: string; price: string; note?: string }[];
|
rentalItems: { name: string; price: string; note?: string }[];
|
||||||
rules: string[];
|
rules: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PriceField({ label, value, onChange }: { label: string; value: string; onChange: (v: string) => void }) {
|
||||||
|
// Strip "BYN" suffix for editing, add back on save
|
||||||
|
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="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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PricingEditorPage() {
|
export default function PricingEditorPage() {
|
||||||
return (
|
return (
|
||||||
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
|
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
|
||||||
@@ -29,6 +63,48 @@ export default function PricingEditorPage() {
|
|||||||
onChange={(v) => update({ ...data, subtitle: v })}
|
onChange={(v) => update({ ...data, subtitle: v })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Popular & Featured selectors */}
|
||||||
|
{(() => {
|
||||||
|
const itemOptions = data.items
|
||||||
|
.map((it, idx) => ({ value: String(idx), label: it.name }))
|
||||||
|
.filter((o) => o.label.trim() !== "");
|
||||||
|
const noneOption = { value: "", label: "— Нет —" };
|
||||||
|
|
||||||
|
const popularIdx = data.items.findIndex((it) => it.popular);
|
||||||
|
const featuredIdx = data.items.findIndex((it) => it.featured);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<SelectField
|
||||||
|
label="Популярный абонемент"
|
||||||
|
value={popularIdx >= 0 ? String(popularIdx) : ""}
|
||||||
|
onChange={(v) => {
|
||||||
|
const items = data.items.map((it, idx) => ({
|
||||||
|
...it,
|
||||||
|
popular: v ? idx === Number(v) : false,
|
||||||
|
}));
|
||||||
|
update({ ...data, items });
|
||||||
|
}}
|
||||||
|
options={[noneOption, ...itemOptions]}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<ArrayEditor
|
<ArrayEditor
|
||||||
label="Абонементы"
|
label="Абонементы"
|
||||||
items={data.items}
|
items={data.items}
|
||||||
@@ -40,7 +116,7 @@ export default function PricingEditorPage() {
|
|||||||
value={item.name}
|
value={item.name}
|
||||||
onChange={(v) => updateItem({ ...item, name: v })}
|
onChange={(v) => updateItem({ ...item, name: v })}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<PriceField
|
||||||
label="Цена"
|
label="Цена"
|
||||||
value={item.price}
|
value={item.price}
|
||||||
onChange={(v) => updateItem({ ...item, price: v })}
|
onChange={(v) => updateItem({ ...item, price: v })}
|
||||||
@@ -73,7 +149,7 @@ export default function PricingEditorPage() {
|
|||||||
value={item.name}
|
value={item.name}
|
||||||
onChange={(v) => updateItem({ ...item, name: v })}
|
onChange={(v) => updateItem({ ...item, name: v })}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<PriceField
|
||||||
label="Цена"
|
label="Цена"
|
||||||
value={item.price}
|
value={item.price}
|
||||||
onChange={(v) => updateItem({ ...item, price: v })}
|
onChange={(v) => updateItem({ ...item, price: v })}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export function Pricing({ data: pricing }: PricingProps) {
|
|||||||
{ id: "rules", label: "Правила", icon: <ScrollText size={16} /> },
|
{ id: "rules", label: "Правила", icon: <ScrollText size={16} /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Split items: regular + unlimited (last item)
|
// Split items: featured (big card) vs regular
|
||||||
const regularItems = pricing.items.slice(0, -1);
|
const featuredItem = pricing.items.find((item) => item.featured);
|
||||||
const unlimitedItem = pricing.items[pricing.items.length - 1];
|
const regularItems = pricing.items.filter((item) => !item.featured);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="pricing" className="section-glow relative section-padding bg-neutral-50 dark:bg-[#050505]">
|
<section id="pricing" className="section-glow relative section-padding bg-neutral-50 dark:bg-[#050505]">
|
||||||
@@ -66,7 +66,7 @@ export function Pricing({ data: pricing }: PricingProps) {
|
|||||||
{/* Cards grid */}
|
{/* Cards grid */}
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{regularItems.map((item, i) => {
|
{regularItems.map((item, i) => {
|
||||||
const isPopular = i === 0;
|
const isPopular = item.popular ?? false;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
@@ -110,25 +110,25 @@ export function Pricing({ data: pricing }: PricingProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Unlimited — featured card */}
|
{/* Featured — big card */}
|
||||||
{unlimitedItem && (
|
{featuredItem && (
|
||||||
<button onClick={() => setBookingOpen(true)} className="mt-6 w-full cursor-pointer text-left team-card-glitter rounded-2xl border border-gold/30 bg-gradient-to-r from-gold/10 via-gold/5 to-gold/10 dark:from-gold/[0.06] dark:via-transparent dark:to-gold/[0.06] p-6 sm:p-8 transition-shadow duration-300 hover:shadow-xl hover:shadow-gold/20">
|
<button onClick={() => setBookingOpen(true)} className="mt-6 w-full cursor-pointer text-left team-card-glitter rounded-2xl border border-gold/30 bg-gradient-to-r from-gold/10 via-gold/5 to-gold/10 dark:from-gold/[0.06] dark:via-transparent dark:to-gold/[0.06] p-6 sm:p-8 transition-shadow duration-300 hover:shadow-xl hover:shadow-gold/20">
|
||||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
||||||
<div className="text-center sm:text-left">
|
<div className="text-center sm:text-left">
|
||||||
<div className="flex items-center justify-center gap-2 sm:justify-start">
|
<div className="flex items-center justify-center gap-2 sm:justify-start">
|
||||||
<Crown size={18} className="text-gold" />
|
<Crown size={18} className="text-gold" />
|
||||||
<p className="text-lg font-bold text-neutral-900 dark:text-white">
|
<p className="text-lg font-bold text-neutral-900 dark:text-white">
|
||||||
{unlimitedItem.name}
|
{featuredItem.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{unlimitedItem.note && (
|
{featuredItem.note && (
|
||||||
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
{unlimitedItem.note}
|
{featuredItem.note}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="shrink-0 font-display text-3xl font-bold text-gold">
|
<p className="shrink-0 font-display text-3xl font-bold text-gold">
|
||||||
{unlimitedItem.price}
|
{featuredItem.price}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface PricingItem {
|
|||||||
name: string;
|
name: string;
|
||||||
price: string;
|
price: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
|
popular?: boolean;
|
||||||
|
featured?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleClass {
|
export interface ScheduleClass {
|
||||||
|
|||||||
Reference in New Issue
Block a user