studio-os 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +7 -0
- package/.env.example +34 -0
- package/CHANGELOG.md +86 -0
- package/Dockerfile +25 -0
- package/LICENSE +21 -0
- package/PROGRESS.md +60 -0
- package/README.md +253 -0
- package/SPEC.md +130 -0
- package/docker-compose.yml +26 -0
- package/docs/screenshots/admin-client.png +0 -0
- package/docs/screenshots/admin-dashboard.png +0 -0
- package/docs/screenshots/admin-reports.png +0 -0
- package/docs/screenshots/admin-roster.png +0 -0
- package/docs/screenshots/admin-schedule.png +0 -0
- package/docs/screenshots/public-booking.png +0 -0
- package/docs/screenshots/public-buy.png +0 -0
- package/docs/screenshots/public-schedule.png +0 -0
- package/package.json +54 -0
- package/public/css/custom.css +40 -0
- package/public/js/icon.svg +4 -0
- package/public/js/sw.js +23 -0
- package/public/manifest.json +17 -0
- package/public/vendor/htmx.min.js +1 -0
- package/public/vendor/pico.min.css +4 -0
- package/scripts/import-mindbody.mjs +109 -0
- package/scripts/seed.mjs +147 -0
- package/src/app.js +141 -0
- package/src/db/002-instructor-role.sql +26 -0
- package/src/db/index.js +82 -0
- package/src/db/schema.sql +164 -0
- package/src/lib/ratelimit.js +50 -0
- package/src/lib/time.js +87 -0
- package/src/routes/admin.js +692 -0
- package/src/routes/auth.js +34 -0
- package/src/routes/instructor.js +82 -0
- package/src/routes/me.js +101 -0
- package/src/routes/public.js +215 -0
- package/src/routes/setup.js +55 -0
- package/src/routes/webhooks.js +28 -0
- package/src/server.js +29 -0
- package/src/services/auth.js +81 -0
- package/src/services/booking.js +261 -0
- package/src/services/importer.js +201 -0
- package/src/services/mailer.js +92 -0
- package/src/services/schedule.js +71 -0
- package/src/services/stripe.js +169 -0
- package/test/booking.test.js +346 -0
- package/test/fixtures/mindbody-clients.csv +9 -0
- package/test/fixtures/mindbody-passes.csv +8 -0
- package/test/helpers.js +59 -0
- package/test/importer.test.js +173 -0
- package/test/instructor.test.js +217 -0
- package/test/ratelimit.test.js +123 -0
- package/test/schedule.test.js +102 -0
- package/test/stripe.test.js +249 -0
- package/test/views.test.js +38 -0
- package/test/web.test.js +392 -0
- package/views/admin/class_types.ejs +45 -0
- package/views/admin/client.ejs +149 -0
- package/views/admin/clients.ejs +33 -0
- package/views/admin/dashboard.ejs +55 -0
- package/views/admin/import.ejs +39 -0
- package/views/admin/instructor_classes.ejs +21 -0
- package/views/admin/instructor_new.ejs +15 -0
- package/views/admin/instructors.ejs +49 -0
- package/views/admin/products.ejs +83 -0
- package/views/admin/reports.ejs +34 -0
- package/views/admin/roster.ejs +58 -0
- package/views/admin/rules.ejs +71 -0
- package/views/admin/schedule.ejs +50 -0
- package/views/admin/settings.ejs +53 -0
- package/views/error.ejs +7 -0
- package/views/instructor/roster.ejs +27 -0
- package/views/instructor/schedule.ejs +21 -0
- package/views/login.ejs +11 -0
- package/views/partials/foot.ejs +11 -0
- package/views/partials/head.ejs +54 -0
- package/views/public/book_result.ejs +26 -0
- package/views/public/buy.ejs +37 -0
- package/views/public/buy_manual.ejs +17 -0
- package/views/public/buy_thanks.ejs +7 -0
- package/views/public/class.ejs +29 -0
- package/views/public/magic_request.ejs +21 -0
- package/views/public/me.ejs +47 -0
- package/views/public/schedule.ejs +49 -0
- package/views/setup.ejs +53 -0
package/src/lib/time.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Timezone helpers. All storage is UTC ISO strings; rendering uses the studio
|
|
2
|
+
// timezone (an IANA name from settings). No external tz library — we lean on Intl.
|
|
3
|
+
|
|
4
|
+
/** Current UTC time as ISO string (second precision). */
|
|
5
|
+
export function utcNow() {
|
|
6
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function partsInTz(tz, date) {
|
|
10
|
+
const fmt = new Intl.DateTimeFormat('en-CA', {
|
|
11
|
+
timeZone: tz,
|
|
12
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
13
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
14
|
+
hour12: false, weekday: 'short',
|
|
15
|
+
});
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const p of fmt.formatToParts(date)) out[p.type] = p.value;
|
|
18
|
+
return {
|
|
19
|
+
y: Number(out.year), m: Number(out.month), d: Number(out.day),
|
|
20
|
+
hh: Number(out.hour) % 24, mm: Number(out.minute), ss: Number(out.second),
|
|
21
|
+
weekday: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(out.weekday),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Offset (ms) of tz relative to UTC at the given instant. */
|
|
26
|
+
function tzOffsetMs(tz, date) {
|
|
27
|
+
const p = partsInTz(tz, date);
|
|
28
|
+
const asUtc = Date.UTC(p.y, p.m - 1, p.d, p.hh, p.mm, p.ss);
|
|
29
|
+
return asUtc - Math.floor(date.getTime() / 1000) * 1000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert a wall-clock time in tz to a UTC Date.
|
|
34
|
+
* (y, m 1-based, d, hh, mm)
|
|
35
|
+
*/
|
|
36
|
+
export function zonedToUtc(tz, y, m, d, hh = 0, mm = 0) {
|
|
37
|
+
const guess = Date.UTC(y, m - 1, d, hh, mm, 0);
|
|
38
|
+
let ts = guess - tzOffsetMs(tz, new Date(guess));
|
|
39
|
+
// refine once for DST edges
|
|
40
|
+
ts = guess - tzOffsetMs(tz, new Date(ts));
|
|
41
|
+
return new Date(ts);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Wall-clock parts of a UTC instant in tz. Accepts Date or ISO string. */
|
|
45
|
+
export function utcToZoned(tz, dateOrIso) {
|
|
46
|
+
const date = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
|
|
47
|
+
return partsInTz(tz, date);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 'YYYY-MM-DD' local date in tz for a UTC instant (default now). */
|
|
51
|
+
export function localDateStr(tz, dateOrIso = new Date()) {
|
|
52
|
+
const p = utcToZoned(tz, dateOrIso);
|
|
53
|
+
return `${p.y}-${String(p.m).padStart(2, '0')}-${String(p.d).padStart(2, '0')}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 'HH:MM' local time in tz. */
|
|
57
|
+
export function localTimeStr(tz, dateOrIso) {
|
|
58
|
+
const p = utcToZoned(tz, dateOrIso);
|
|
59
|
+
return `${String(p.hh).padStart(2, '0')}:${String(p.mm).padStart(2, '0')}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Human display, e.g. "Mon 3 Jun, 09:00". */
|
|
63
|
+
export function formatInTz(tz, dateOrIso, opts = {}) {
|
|
64
|
+
const date = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
|
|
65
|
+
return new Intl.DateTimeFormat('en-GB', {
|
|
66
|
+
timeZone: tz, weekday: 'short', day: 'numeric', month: 'short',
|
|
67
|
+
hour: '2-digit', minute: '2-digit', hour12: false, ...opts,
|
|
68
|
+
}).format(date);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Add whole days to a 'YYYY-MM-DD' string. */
|
|
72
|
+
export function addDays(dateStr, days) {
|
|
73
|
+
const [y, m, d] = dateStr.split('-').map(Number);
|
|
74
|
+
const dt = new Date(Date.UTC(y, m - 1, d + days));
|
|
75
|
+
return dt.toISOString().slice(0, 10);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Weekday (0=Sun..6=Sat) of a 'YYYY-MM-DD' string, calendar-wise. */
|
|
79
|
+
export function weekdayOf(dateStr) {
|
|
80
|
+
const [y, m, d] = dateStr.split('-').map(Number);
|
|
81
|
+
return new Date(Date.UTC(y, m - 1, d)).getUTCDay();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Hours between two instants (b - a), fractional. */
|
|
85
|
+
export function hoursBetween(aIso, bIso) {
|
|
86
|
+
return (new Date(bIso).getTime() - new Date(aIso).getTime()) / 3600000;
|
|
87
|
+
}
|