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:
2026-04-10 18:42:54 +03:00
parent bbe485d8fc
commit a587736dd3
74 changed files with 724 additions and 298 deletions
+49 -3
View File
@@ -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);
}