public
imalexnord
read
Slot
Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.
Languages
Repository composition by tracked source files.
TypeScript
79%
CSS
19%
SQL
2%
HTML
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
import type { AppSettings, Env } from './types';
export const DEFAULT_SETTINGS: AppSettings = {
title: 'Book a slot',
subtitle: 'Pick a day, choose a time, and you are set.',
timezone: 'Europe/Stockholm',
bookingWindowDays: 365,
minimumNoticeMinutes: 120,
bufferBeforeMinutes: 0,
bufferAfterMinutes: 15,
slotIncrementMinutes: 15,
durations: [15, 30, 45, 60],
calendarId: 'primary',
busyCalendarIds: ['primary'],
defaultMeetingMode: 'google_meet',
allowMeetingModes: ['google_meet'],
inPersonLocation: '',
paymentEnabled: false,
paymentRatePerMinute: 0,
paymentFreeDurations: [],
paymentCurrency: 'sek',
paymentLabel: 'Booking payment',
availability: {
'0': [],
'1': [{ start: '09:00', end: '17:00' }],
'2': [{ start: '09:00', end: '17:00' }],
'3': [{ start: '09:00', end: '17:00' }],
'4': [{ start: '09:00', end: '17:00' }],
'5': [{ start: '09:00', end: '15:00' }],
'6': [],
},
};
export async function getSettings(env: Env): Promise<AppSettings> {
const row = await env.DB.prepare('SELECT value FROM settings WHERE key = ?1')
.bind('app_settings')
.first<{ value: string }>();
if (!row?.value) return DEFAULT_SETTINGS;
try {
const parsed = JSON.parse(row.value) as Partial<AppSettings> & { paymentAmountMinor?: number };
const currency = typeof parsed.paymentCurrency === 'string' && /^[a-zA-Z]{3}$/.test(parsed.paymentCurrency)
? parsed.paymentCurrency.toLowerCase()
: DEFAULT_SETTINGS.paymentCurrency;
const hasPerMinuteRate = parsed.paymentRatePerMinute !== undefined
&& Number.isFinite(Number(parsed.paymentRatePerMinute));
const paymentRatePerMinute = hasPerMinuteRate
? Math.max(0, Number(parsed.paymentRatePerMinute))
: DEFAULT_SETTINGS.paymentRatePerMinute;
return {
...DEFAULT_SETTINGS,
...parsed,
// Old releases stored one fixed Stripe amount in `paymentAmountMinor`.
// There is no safe way to reinterpret that as a per-minute rate, so legacy
// payment settings are deliberately disabled until the owner saves a rate.
paymentEnabled: hasPerMinuteRate ? Boolean(parsed.paymentEnabled) : false,
paymentCurrency: currency,
paymentRatePerMinute,
paymentFreeDurations: Array.isArray(parsed.paymentFreeDurations)
? parsed.paymentFreeDurations.map(Number).filter((value) => Number.isFinite(value) && value > 0)
: DEFAULT_SETTINGS.paymentFreeDurations,
durations: Array.isArray(parsed.durations) ? parsed.durations : DEFAULT_SETTINGS.durations,
busyCalendarIds: Array.isArray(parsed.busyCalendarIds) ? parsed.busyCalendarIds : DEFAULT_SETTINGS.busyCalendarIds,
allowMeetingModes: Array.isArray(parsed.allowMeetingModes)
? parsed.allowMeetingModes
: DEFAULT_SETTINGS.allowMeetingModes,
availability: {
...DEFAULT_SETTINGS.availability,
...(parsed.availability ?? {}),
},
};
} catch {
return DEFAULT_SETTINGS;
}
}
export async function saveSettings(env: Env, settings: AppSettings): Promise<void> {
await env.DB.prepare(`
INSERT INTO settings (key, value, updated_at)
VALUES (?1, ?2, datetime('now'))
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
`).bind('app_settings', JSON.stringify(settings)).run();
}