feat: mobile UX, admin polish, rate limiting, and media assets
- Mobile responsiveness improvements across admin and public sections - Admin: bookings modal, open-day page, team page, layout polish - Added rate limiting, CSRF hardening, auth-edge improvements - Scroll reveal, floating contact, back-to-top, Yandex map fixes - Schedule filters refactor, team profile/info component updates - New useTrainerPhotos hook - Added class, team, master-class, and news images
This commit is contained in:
+49
-3
@@ -634,6 +634,33 @@ export function addMcRegistration(
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
/** Atomic check-and-insert: counts confirmed registrations and inserts in a single transaction */
|
||||
export function addMcRegistrationAtomic(
|
||||
masterClassTitle: string,
|
||||
name: string,
|
||||
instagram: string,
|
||||
telegram?: string,
|
||||
phone?: string,
|
||||
maxParticipants?: number
|
||||
): { id: number; isWaiting: boolean } {
|
||||
const db = getDb();
|
||||
const txn = db.transaction(() => {
|
||||
let isWaiting = false;
|
||||
if (maxParticipants) {
|
||||
const row = db.prepare(
|
||||
"SELECT COUNT(*) as cnt FROM mc_registrations WHERE master_class_title = ? AND status = 'confirmed'"
|
||||
).get(masterClassTitle) as { cnt: number };
|
||||
isWaiting = row.cnt >= maxParticipants;
|
||||
}
|
||||
const result = db.prepare(
|
||||
`INSERT INTO mc_registrations (master_class_title, name, instagram, telegram, phone, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
).run(masterClassTitle, name, instagram, telegram || null, phone || null, isWaiting ? "Лист ожидания" : null);
|
||||
return { id: result.lastInsertRowid as number, isWaiting };
|
||||
});
|
||||
return txn();
|
||||
}
|
||||
|
||||
export function getMcRegistrations(masterClassTitle: string): McRegistration[] {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
@@ -652,6 +679,17 @@ export function getAllMcRegistrations(): McRegistration[] {
|
||||
return rows.map(mapMcRow);
|
||||
}
|
||||
|
||||
/** Efficient count of registrations grouped by master class title */
|
||||
export function getMcRegistrationCounts(): Record<string, number> {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare("SELECT master_class_title, COUNT(*) as cnt FROM mc_registrations GROUP BY master_class_title")
|
||||
.all() as { master_class_title: string; cnt: number }[];
|
||||
const result: Record<string, number> = {};
|
||||
for (const r of rows) result[r.master_class_title] = r.cnt;
|
||||
return result;
|
||||
}
|
||||
|
||||
function mapMcRow(r: McRegistrationRow): McRegistration {
|
||||
return {
|
||||
id: r.id,
|
||||
@@ -686,11 +724,15 @@ export function updateMcRegistration(
|
||||
).run(name, instagram, telegram || null, id);
|
||||
}
|
||||
|
||||
const VALID_NOTIFY_FIELDS = new Set(["notified_confirm", "notified_reminder"]);
|
||||
const VALID_BOOKING_TABLES = new Set(["mc_registrations", "group_bookings", "open_day_bookings"]);
|
||||
|
||||
export function toggleMcNotification(
|
||||
id: number,
|
||||
field: "notified_confirm" | "notified_reminder",
|
||||
value: boolean
|
||||
): void {
|
||||
if (!VALID_NOTIFY_FIELDS.has(field)) throw new Error(`Invalid field: ${field}`);
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE mc_registrations SET ${field} = ? WHERE id = ?`
|
||||
@@ -823,6 +865,7 @@ export function toggleGroupBookingNotification(
|
||||
field: "notified_confirm" | "notified_reminder",
|
||||
value: boolean
|
||||
): void {
|
||||
if (!VALID_NOTIFY_FIELDS.has(field)) throw new Error(`Invalid field: ${field}`);
|
||||
const db = getDb();
|
||||
db.prepare(`UPDATE group_bookings SET ${field} = ? WHERE id = ?`).run(
|
||||
value ? 1 : 0,
|
||||
@@ -853,6 +896,7 @@ export function setReminderStatus(
|
||||
id: number,
|
||||
status: ReminderStatus | null
|
||||
): void {
|
||||
if (!VALID_BOOKING_TABLES.has(table)) throw new Error(`Invalid table: ${table}`);
|
||||
const db = getDb();
|
||||
db.prepare(`UPDATE ${table} SET reminder_status = ? WHERE id = ?`).run(status, id);
|
||||
}
|
||||
@@ -862,6 +906,7 @@ export function updateBookingNotes(
|
||||
id: number,
|
||||
notes: string
|
||||
): void {
|
||||
if (!VALID_BOOKING_TABLES.has(table)) throw new Error(`Invalid table: ${table}`);
|
||||
const db = getDb();
|
||||
db.prepare(`UPDATE ${table} SET notes = ? WHERE id = ?`).run(notes || null, id);
|
||||
}
|
||||
@@ -934,7 +979,7 @@ export function getUpcomingReminders(): ReminderItem[] {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (err) { console.warn("[getUpcomingReminders] MC query error:", err); }
|
||||
|
||||
// Group bookings — confirmed with date today/tomorrow
|
||||
try {
|
||||
@@ -956,7 +1001,7 @@ export function getUpcomingReminders(): ReminderItem[] {
|
||||
eventDate: r.confirmed_date!,
|
||||
});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (err) { console.warn("[getUpcomingReminders] group booking query error:", err); }
|
||||
|
||||
// Open Day bookings — check event date
|
||||
try {
|
||||
@@ -986,7 +1031,7 @@ export function getUpcomingReminders(): ReminderItem[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (err) { console.warn("[getUpcomingReminders] open day query error:", err); }
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -1384,6 +1429,7 @@ export function toggleOpenDayNotification(
|
||||
field: "notified_confirm" | "notified_reminder",
|
||||
value: boolean
|
||||
): void {
|
||||
if (!VALID_NOTIFY_FIELDS.has(field)) throw new Error(`Invalid field: ${field}`);
|
||||
const db = getDb();
|
||||
db.prepare(`UPDATE open_day_bookings SET ${field} = ? WHERE id = ?`).run(value ? 1 : 0, id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user