feat: admin-editable success + waiting list messages for MC and Open Day
This commit is contained in:
@@ -525,6 +525,14 @@ export default function MasterClassesEditorPage() {
|
||||
placeholder="Вы записаны! Мы свяжемся с вами"
|
||||
/>
|
||||
|
||||
<TextareaField
|
||||
label="Текст для листа ожидания"
|
||||
value={data.waitingListText || ""}
|
||||
onChange={(v) => update({ ...data, waitingListText: v || undefined })}
|
||||
placeholder="Все места заняты, но мы добавили вас в лист ожидания..."
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<ArrayEditor
|
||||
label="Мастер-классы"
|
||||
items={data.items}
|
||||
|
||||
@@ -19,6 +19,8 @@ interface OpenDayEvent {
|
||||
discountThreshold: number;
|
||||
minBookings: number;
|
||||
maxParticipants: number;
|
||||
successMessage?: string;
|
||||
waitingListText?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@@ -100,6 +102,29 @@ function EventSettings({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm text-neutral-400 mb-1.5">Текст после записи</label>
|
||||
<textarea
|
||||
value={event.successMessage || ""}
|
||||
onChange={(e) => onChange({ successMessage: e.target.value || undefined })}
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none focus:border-gold transition-colors resize-none"
|
||||
placeholder="Вы записаны!"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-neutral-400 mb-1.5">Текст для листа ожидания</label>
|
||||
<textarea
|
||||
value={event.waitingListText || ""}
|
||||
onChange={(e) => onChange({ waitingListText: e.target.value || undefined })}
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-white/10 bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none focus:border-gold transition-colors resize-none"
|
||||
placeholder="Все места заняты, но мы добавили вас в лист ожидания..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-neutral-400 mb-1.5">Цена за занятие (BYN)</label>
|
||||
<input
|
||||
|
||||
@@ -279,6 +279,7 @@ export function MasterClasses({ data, regCounts = {} }: MasterClassesProps) {
|
||||
endpoint="/api/master-class-register"
|
||||
extraBody={{ masterClassTitle: signupTitle }}
|
||||
successMessage={data.successMessage}
|
||||
waitingMessage={data.waitingListText}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -132,6 +132,8 @@ export function OpenDay({ data }: OpenDayProps) {
|
||||
subtitle={signup.label}
|
||||
endpoint="/api/open-day-register"
|
||||
extraBody={{ classId: signup.classId, eventId: event.id }}
|
||||
successMessage={event.successMessage}
|
||||
waitingMessage={event.waitingListText}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -16,6 +16,8 @@ interface SignupModalProps {
|
||||
extraBody?: Record<string, unknown>;
|
||||
/** Custom success message */
|
||||
successMessage?: string;
|
||||
/** Custom waiting list message */
|
||||
waitingMessage?: string;
|
||||
/** Callback with API response data on success */
|
||||
onSuccess?: (data: Record<string, unknown>) => void;
|
||||
}
|
||||
@@ -28,6 +30,7 @@ export function SignupModal({
|
||||
endpoint,
|
||||
extraBody,
|
||||
successMessage,
|
||||
waitingMessage,
|
||||
onSuccess,
|
||||
}: SignupModalProps) {
|
||||
const [name, setName] = useState("");
|
||||
@@ -154,10 +157,8 @@ export function SignupModal({
|
||||
<CheckCircle size={28} className="text-amber-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">Вы в листе ожидания</h3>
|
||||
<p className="mt-2 text-sm text-neutral-400 leading-relaxed">
|
||||
Все места заняты, но мы добавили вас в лист ожидания.
|
||||
<br />
|
||||
Если кто-то откажется — мы предложим место вам.
|
||||
<p className="mt-2 text-sm text-neutral-400 leading-relaxed whitespace-pre-line">
|
||||
{waitingMessage || "Все места заняты, но мы добавили вас в лист ожидания.\nЕсли кто-то откажется — мы предложим место вам."}
|
||||
</p>
|
||||
<a
|
||||
href={BRAND.instagram}
|
||||
|
||||
@@ -275,6 +275,19 @@ const migrations: Migration[] = [
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 16,
|
||||
name: "add_messages_to_open_day_events",
|
||||
up: (db) => {
|
||||
const cols = db.prepare("PRAGMA table_info(open_day_events)").all() as { name: string }[];
|
||||
if (!cols.some((c) => c.name === "success_message")) {
|
||||
db.exec("ALTER TABLE open_day_events ADD COLUMN success_message TEXT");
|
||||
}
|
||||
if (!cols.some((c) => c.name === "waiting_list_text")) {
|
||||
db.exec("ALTER TABLE open_day_events ADD COLUMN waiting_list_text TEXT");
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function runMigrations(db: Database.Database) {
|
||||
@@ -969,6 +982,8 @@ interface OpenDayEventRow {
|
||||
discount_threshold: number;
|
||||
min_bookings: number;
|
||||
max_participants: number;
|
||||
success_message: string | null;
|
||||
waiting_list_text: string | null;
|
||||
active: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -984,6 +999,8 @@ export interface OpenDayEvent {
|
||||
discountThreshold: number;
|
||||
minBookings: number;
|
||||
maxParticipants: number;
|
||||
successMessage?: string;
|
||||
waitingListText?: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@@ -1064,6 +1081,8 @@ function mapEventRow(r: OpenDayEventRow): OpenDayEvent {
|
||||
discountThreshold: r.discount_threshold,
|
||||
minBookings: r.min_bookings,
|
||||
maxParticipants: r.max_participants ?? 0,
|
||||
successMessage: r.success_message ?? undefined,
|
||||
waitingListText: r.waiting_list_text ?? undefined,
|
||||
active: !!r.active,
|
||||
};
|
||||
}
|
||||
@@ -1174,6 +1193,8 @@ export function updateOpenDayEvent(
|
||||
discountThreshold: number;
|
||||
minBookings: number;
|
||||
maxParticipants: number;
|
||||
successMessage: string;
|
||||
waitingListText: string;
|
||||
active: boolean;
|
||||
}>
|
||||
): void {
|
||||
@@ -1188,6 +1209,8 @@ export function updateOpenDayEvent(
|
||||
if (data.discountThreshold !== undefined) { sets.push("discount_threshold = ?"); vals.push(data.discountThreshold); }
|
||||
if (data.minBookings !== undefined) { sets.push("min_bookings = ?"); vals.push(data.minBookings); }
|
||||
if (data.maxParticipants !== undefined) { sets.push("max_participants = ?"); vals.push(data.maxParticipants); }
|
||||
if (data.successMessage !== undefined) { sets.push("success_message = ?"); vals.push(data.successMessage || null); }
|
||||
if (data.waitingListText !== undefined) { sets.push("waiting_list_text = ?"); vals.push(data.waitingListText || null); }
|
||||
if (data.active !== undefined) { sets.push("active = ?"); vals.push(data.active ? 1 : 0); }
|
||||
if (sets.length === 0) return;
|
||||
sets.push("updated_at = datetime('now')");
|
||||
|
||||
@@ -148,6 +148,7 @@ export interface SiteContent {
|
||||
masterClasses: {
|
||||
title: string;
|
||||
successMessage?: string;
|
||||
waitingListText?: string;
|
||||
items: MasterClassItem[];
|
||||
};
|
||||
schedule: {
|
||||
|
||||
Reference in New Issue
Block a user