@mrcharles/sdk 0.0.0-stage → 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/README.md +132 -2
- package/dist/mrcharles.esm.js +1175 -0
- package/dist/mrcharles.esm.js.map +21 -0
- package/dist/mrcharles.min.js +4 -0
- package/dist/mrcharles.min.js.map +22 -0
- package/dist/types/client.d.ts +79 -0
- package/dist/types/embed.d.ts +28 -0
- package/dist/types/env.d.ts +36 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/format.d.ts +24 -0
- package/dist/types/http.d.ts +7 -0
- package/dist/types/i18n.d.ts +50 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/links.d.ts +34 -0
- package/dist/types/rooms.d.ts +76 -0
- package/dist/types/slots.d.ts +19 -0
- package/dist/types/types.d.ts +340 -0
- package/dist/types/uuid.d.ts +2 -0
- package/package.json +55 -4
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
// src/env.ts
|
|
2
|
+
var ENVIRONMENTS = {
|
|
3
|
+
production: {
|
|
4
|
+
apiBase: "https://api.mrcharles.app/api/v1",
|
|
5
|
+
appBase: "https://app.mrcharles.app",
|
|
6
|
+
sitesBase: "sites.mrcharles.app"
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function resolveEndpoints(config = {}) {
|
|
10
|
+
const base = ENVIRONMENTS[config.env ?? "production"];
|
|
11
|
+
return {
|
|
12
|
+
apiBase: trimSlash(config.apiBase ?? base.apiBase),
|
|
13
|
+
appBase: trimSlash(config.appBase ?? base.appBase),
|
|
14
|
+
sitesBase: (config.sitesBase ?? base.sitesBase).replace(/^https?:\/\//, "").replace(/\/+$/, "")
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function trimSlash(s) {
|
|
18
|
+
return s.replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/errors.ts
|
|
22
|
+
class MrCharlesError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
status;
|
|
25
|
+
requestId;
|
|
26
|
+
constructor(code, message, status = 0, requestId) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "MrCharlesError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.requestId = requestId;
|
|
32
|
+
}
|
|
33
|
+
get holdIsDead() {
|
|
34
|
+
return this.code === "HOLD_EXPIRED" || this.code === "TOO_MANY_ATTEMPTS";
|
|
35
|
+
}
|
|
36
|
+
get slotIsGone() {
|
|
37
|
+
return this.code === "SLOT_TAKEN" || this.code === "OUTSIDE_HOURS" || this.code === "OUTSIDE_BOOKING_WINDOW";
|
|
38
|
+
}
|
|
39
|
+
get isTransient() {
|
|
40
|
+
return this.code === "NETWORK_ERROR" || this.status >= 500;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isMrCharlesError(e) {
|
|
44
|
+
return e instanceof MrCharlesError || typeof e === "object" && e !== null && e.name === "MrCharlesError";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/http.ts
|
|
48
|
+
function createHttp(apiBase, config, getLocale = () => config.locale) {
|
|
49
|
+
const resolved = config.fetch ?? (typeof fetch === "function" ? fetch.bind(globalThis) : undefined);
|
|
50
|
+
if (!resolved)
|
|
51
|
+
throw new Error("@mrcharles/sdk: no fetch available — pass { fetch } in the client config");
|
|
52
|
+
const doFetch = resolved;
|
|
53
|
+
async function request(method, path, body, params) {
|
|
54
|
+
let url = apiBase + path;
|
|
55
|
+
if (params) {
|
|
56
|
+
const q = new URLSearchParams;
|
|
57
|
+
for (const [k, v] of Object.entries(params))
|
|
58
|
+
if (v !== undefined && v !== "")
|
|
59
|
+
q.set(k, String(v));
|
|
60
|
+
const s = q.toString();
|
|
61
|
+
if (s)
|
|
62
|
+
url += (url.includes("?") ? "&" : "?") + s;
|
|
63
|
+
}
|
|
64
|
+
const headers = { Accept: "application/json", ...config.headers ?? {} };
|
|
65
|
+
const locale = getLocale();
|
|
66
|
+
if (locale)
|
|
67
|
+
headers["Accept-Language"] = locale;
|
|
68
|
+
if (body !== undefined)
|
|
69
|
+
headers["Content-Type"] = "application/json";
|
|
70
|
+
let res;
|
|
71
|
+
try {
|
|
72
|
+
res = await doFetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, cache: "no-store" });
|
|
73
|
+
} catch (e) {
|
|
74
|
+
throw new MrCharlesError("NETWORK_ERROR", e?.message || "Network error", 0);
|
|
75
|
+
}
|
|
76
|
+
const requestId = res.headers?.get?.("x-request-id") ?? undefined;
|
|
77
|
+
const text = await res.text();
|
|
78
|
+
let json = null;
|
|
79
|
+
try {
|
|
80
|
+
json = text ? JSON.parse(text) : null;
|
|
81
|
+
} catch {
|
|
82
|
+
json = null;
|
|
83
|
+
}
|
|
84
|
+
const envelope = json && typeof json === "object" ? json : {};
|
|
85
|
+
if (!res.ok || envelope.success === false) {
|
|
86
|
+
const code = envelope.error?.code ?? (res.status === 429 ? "RATE_LIMIT_EXCEEDED" : res.status === 404 ? "RESOURCE_NOT_FOUND" : "INTERNAL_ERROR");
|
|
87
|
+
const message = envelope.error?.message ?? res.statusText ?? "Request failed";
|
|
88
|
+
throw new MrCharlesError(code, message, res.status, requestId);
|
|
89
|
+
}
|
|
90
|
+
return "data" in envelope ? envelope.data : json;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
get: (path, params) => request("GET", path, undefined, params),
|
|
94
|
+
post: (path, body) => request("POST", path, body ?? {})
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/i18n.ts
|
|
99
|
+
var LOCALES = ["es", "ca", "en", "fr", "pt", "it", "de"];
|
|
100
|
+
var HOSTED_LOCALES = ["es", "ca", "en"];
|
|
101
|
+
function normalizeLocale(input, fallback = "en") {
|
|
102
|
+
if (!input)
|
|
103
|
+
return fallback;
|
|
104
|
+
const lang = String(input).trim().toLowerCase().split(/[-_]/)[0];
|
|
105
|
+
return LOCALES.includes(lang) ? lang : fallback;
|
|
106
|
+
}
|
|
107
|
+
function toHostedLocale(input, fallback = "en") {
|
|
108
|
+
const l = normalizeLocale(input, fallback);
|
|
109
|
+
return HOSTED_LOCALES.includes(l) ? l : fallback;
|
|
110
|
+
}
|
|
111
|
+
function intlTag(input) {
|
|
112
|
+
const raw = input ? String(input).trim().replace("_", "-") : "";
|
|
113
|
+
if (/^[a-z]{2,3}-[A-Za-z]{2,4}/.test(raw))
|
|
114
|
+
return raw;
|
|
115
|
+
const l = normalizeLocale(raw);
|
|
116
|
+
return { es: "es-ES", ca: "ca-ES", en: "en-GB", fr: "fr-FR", pt: "pt-PT", it: "it-IT", de: "de-DE" }[l];
|
|
117
|
+
}
|
|
118
|
+
function pick(localized, locale, defaultLocale) {
|
|
119
|
+
if (!localized)
|
|
120
|
+
return "";
|
|
121
|
+
const want = toHostedLocale(locale);
|
|
122
|
+
const def = defaultLocale ? toHostedLocale(defaultLocale) : undefined;
|
|
123
|
+
for (const k of [want, def, "es", "ca", "en"]) {
|
|
124
|
+
if (k && localized[k])
|
|
125
|
+
return localized[k];
|
|
126
|
+
}
|
|
127
|
+
const first = Object.values(localized).find((v) => !!v);
|
|
128
|
+
return first ?? "";
|
|
129
|
+
}
|
|
130
|
+
var en = {
|
|
131
|
+
"step.slot": "Pick a time",
|
|
132
|
+
"step.details": "Your details",
|
|
133
|
+
"step.verify": "Enter the code",
|
|
134
|
+
"step.pay": "Confirm",
|
|
135
|
+
"step.done": "Booked",
|
|
136
|
+
"step.expired": "Time ran out",
|
|
137
|
+
"label.date": "Date",
|
|
138
|
+
"label.time": "Time",
|
|
139
|
+
"label.duration": "Duration",
|
|
140
|
+
"label.name": "Name",
|
|
141
|
+
"label.phone": "Mobile",
|
|
142
|
+
"label.email": "Email",
|
|
143
|
+
"label.contact": "Where to send the code",
|
|
144
|
+
"label.code": "Code",
|
|
145
|
+
"label.notes": "Notes for the studio",
|
|
146
|
+
"label.price": "Price",
|
|
147
|
+
"label.deposit": "Deposit",
|
|
148
|
+
"label.total": "Total",
|
|
149
|
+
"label.perHour": "per hour",
|
|
150
|
+
"action.continue": "Continue",
|
|
151
|
+
"action.back": "Back",
|
|
152
|
+
"action.sendCode": "Send me a code",
|
|
153
|
+
"action.verify": "Verify",
|
|
154
|
+
"action.resend": "Send it again",
|
|
155
|
+
"action.confirm": "Confirm booking",
|
|
156
|
+
"action.payCard": "Pay by card",
|
|
157
|
+
"action.payOnSite": "Pay at the studio",
|
|
158
|
+
"action.startOver": "Start again",
|
|
159
|
+
"action.manage": "See or cancel the booking",
|
|
160
|
+
"action.cancel": "Cancel",
|
|
161
|
+
"hint.codeSent": "We sent a code to {contact}.",
|
|
162
|
+
"hint.resendsLeft": "{n} resends left",
|
|
163
|
+
"hint.holdExpiresIn": "Your slot is held for {time}",
|
|
164
|
+
"hint.payOnSite": "Nothing to pay now; settle at the studio.",
|
|
165
|
+
"hint.payCard": "A deposit of {amount} is charged now.",
|
|
166
|
+
"hint.cancelNotice": "Free cancellation up to {hours} h before.",
|
|
167
|
+
"hint.sandbox": "Test mode: nothing is sent or charged, and this booking will be deleted.",
|
|
168
|
+
"hint.noSlots": "No free times that day.",
|
|
169
|
+
"hint.pickDate": "Choose a day to see free times.",
|
|
170
|
+
"done.title": "Booking confirmed",
|
|
171
|
+
"done.reference": "Reference {ref}",
|
|
172
|
+
"done.manage": "Keep this link to see or cancel the booking.",
|
|
173
|
+
"channel.phone": "SMS",
|
|
174
|
+
"channel.email": "Email",
|
|
175
|
+
"pay.card": "Card",
|
|
176
|
+
"pay.on_site": "At the studio",
|
|
177
|
+
"error.VALIDATION_ERROR": "Please check what you typed.",
|
|
178
|
+
"error.RESOURCE_NOT_FOUND": "That was not found.",
|
|
179
|
+
"error.SITE_NOT_LIVE": "This studio is not taking online bookings yet.",
|
|
180
|
+
"error.ROOM_NOT_BOOKABLE": "This room cannot be booked online right now.",
|
|
181
|
+
"error.STUDIO_NOT_VERIFIED": "This studio is not taking online bookings yet.",
|
|
182
|
+
"error.OUTSIDE_HOURS": "The studio is closed at that time.",
|
|
183
|
+
"error.OUTSIDE_BOOKING_WINDOW": "That date cannot be booked yet — try one closer.",
|
|
184
|
+
"error.SLOT_TAKEN": "That time was just taken. Pick another one.",
|
|
185
|
+
"error.HOLD_EXPIRED": "Time ran out. Pick a time again.",
|
|
186
|
+
"error.HOLD_NOT_VERIFIED": "Enter the code first.",
|
|
187
|
+
"error.HOLD_ALREADY_CONFIRMED": "This booking is already confirmed.",
|
|
188
|
+
"error.CODE_INVALID": "That code is not right.",
|
|
189
|
+
"error.TOO_MANY_ATTEMPTS": "Too many tries. Start again.",
|
|
190
|
+
"error.RATE_LIMIT_EXCEEDED": "Too many requests. Wait a minute and try again.",
|
|
191
|
+
"error.CAPTCHA_FAILED": "Please complete the verification challenge.",
|
|
192
|
+
"error.SMS_UNAVAILABLE": "We cannot send SMS right now — use email.",
|
|
193
|
+
"error.PAYMENTS_UNAVAILABLE": "Card payments are unavailable right now — pay at the studio.",
|
|
194
|
+
"error.PAYMENT_MODE_DENIED": "That payment method is not accepted for this room.",
|
|
195
|
+
"error.CANCEL_TOO_LATE": "It is too late to cancel online — call the studio.",
|
|
196
|
+
"error.INVALID_STATE": "That action is not possible right now.",
|
|
197
|
+
"error.INTERNAL_ERROR": "Something went wrong. Try again.",
|
|
198
|
+
"error.NETWORK_ERROR": "No connection. Check your network and try again.",
|
|
199
|
+
"error.default": "Something went wrong. Try again."
|
|
200
|
+
};
|
|
201
|
+
var es = {
|
|
202
|
+
"step.slot": "Elige una hora",
|
|
203
|
+
"step.details": "Tus datos",
|
|
204
|
+
"step.verify": "Introduce el código",
|
|
205
|
+
"step.pay": "Confirmar",
|
|
206
|
+
"step.done": "Reservado",
|
|
207
|
+
"step.expired": "Se agotó el tiempo",
|
|
208
|
+
"label.date": "Fecha",
|
|
209
|
+
"label.time": "Hora",
|
|
210
|
+
"label.duration": "Duración",
|
|
211
|
+
"label.name": "Nombre",
|
|
212
|
+
"label.phone": "Móvil",
|
|
213
|
+
"label.email": "Email",
|
|
214
|
+
"label.contact": "Dónde enviamos el código",
|
|
215
|
+
"label.code": "Código",
|
|
216
|
+
"label.notes": "Notas para el estudio",
|
|
217
|
+
"label.price": "Precio",
|
|
218
|
+
"label.deposit": "Señal",
|
|
219
|
+
"label.total": "Total",
|
|
220
|
+
"label.perHour": "por hora",
|
|
221
|
+
"action.continue": "Continuar",
|
|
222
|
+
"action.back": "Atrás",
|
|
223
|
+
"action.sendCode": "Enviarme un código",
|
|
224
|
+
"action.verify": "Verificar",
|
|
225
|
+
"action.resend": "Reenviar",
|
|
226
|
+
"action.confirm": "Confirmar reserva",
|
|
227
|
+
"action.payCard": "Pagar con tarjeta",
|
|
228
|
+
"action.payOnSite": "Pagar en el estudio",
|
|
229
|
+
"action.startOver": "Empezar de nuevo",
|
|
230
|
+
"action.manage": "Ver o cancelar la reserva",
|
|
231
|
+
"action.cancel": "Cancelar",
|
|
232
|
+
"hint.codeSent": "Hemos enviado un código a {contact}.",
|
|
233
|
+
"hint.resendsLeft": "Quedan {n} reenvíos",
|
|
234
|
+
"hint.holdExpiresIn": "Tu hora está reservada durante {time}",
|
|
235
|
+
"hint.payOnSite": "No pagas nada ahora; se paga en el estudio.",
|
|
236
|
+
"hint.payCard": "Se cobra ahora una señal de {amount}.",
|
|
237
|
+
"hint.cancelNotice": "Cancelación gratuita hasta {hours} h antes.",
|
|
238
|
+
"hint.sandbox": "Modo pruebas: no se envía ni se cobra nada, y esta reserva se borrará.",
|
|
239
|
+
"hint.noSlots": "No hay horas libres ese día.",
|
|
240
|
+
"hint.pickDate": "Elige un día para ver las horas libres.",
|
|
241
|
+
"done.title": "Reserva confirmada",
|
|
242
|
+
"done.reference": "Referencia {ref}",
|
|
243
|
+
"done.manage": "Guarda este enlace para ver o cancelar la reserva.",
|
|
244
|
+
"channel.phone": "SMS",
|
|
245
|
+
"channel.email": "Email",
|
|
246
|
+
"pay.card": "Tarjeta",
|
|
247
|
+
"pay.on_site": "En el estudio",
|
|
248
|
+
"error.VALIDATION_ERROR": "Revisa lo que has escrito.",
|
|
249
|
+
"error.RESOURCE_NOT_FOUND": "No lo encontramos.",
|
|
250
|
+
"error.SITE_NOT_LIVE": "Este estudio aún no acepta reservas online.",
|
|
251
|
+
"error.ROOM_NOT_BOOKABLE": "Esta sala no se puede reservar online ahora mismo.",
|
|
252
|
+
"error.STUDIO_NOT_VERIFIED": "Este estudio aún no acepta reservas online.",
|
|
253
|
+
"error.OUTSIDE_HOURS": "El estudio está cerrado a esa hora.",
|
|
254
|
+
"error.OUTSIDE_BOOKING_WINDOW": "Esa fecha aún no se puede reservar: prueba una más cercana.",
|
|
255
|
+
"error.SLOT_TAKEN": "Esa hora acaba de ocuparse. Elige otra.",
|
|
256
|
+
"error.HOLD_EXPIRED": "Se agotó el tiempo. Vuelve a elegir una hora.",
|
|
257
|
+
"error.HOLD_NOT_VERIFIED": "Introduce primero el código.",
|
|
258
|
+
"error.HOLD_ALREADY_CONFIRMED": "Esta reserva ya está confirmada.",
|
|
259
|
+
"error.CODE_INVALID": "El código no es correcto.",
|
|
260
|
+
"error.TOO_MANY_ATTEMPTS": "Demasiados intentos. Empieza de nuevo.",
|
|
261
|
+
"error.RATE_LIMIT_EXCEEDED": "Demasiadas peticiones. Espera un minuto y vuelve a intentarlo.",
|
|
262
|
+
"error.CAPTCHA_FAILED": "Completa la verificación de seguridad.",
|
|
263
|
+
"error.SMS_UNAVAILABLE": "Ahora no podemos enviar SMS: usa el email.",
|
|
264
|
+
"error.PAYMENTS_UNAVAILABLE": "El pago con tarjeta no está disponible: paga en el estudio.",
|
|
265
|
+
"error.PAYMENT_MODE_DENIED": "Esta sala no acepta ese método de pago.",
|
|
266
|
+
"error.CANCEL_TOO_LATE": "Ya es tarde para cancelar online: llama al estudio.",
|
|
267
|
+
"error.INVALID_STATE": "Esa acción no es posible ahora.",
|
|
268
|
+
"error.INTERNAL_ERROR": "Algo ha fallado. Inténtalo de nuevo.",
|
|
269
|
+
"error.NETWORK_ERROR": "Sin conexión. Revisa tu red e inténtalo de nuevo.",
|
|
270
|
+
"error.default": "Algo ha fallado. Inténtalo de nuevo."
|
|
271
|
+
};
|
|
272
|
+
var ca = {
|
|
273
|
+
"step.slot": "Tria una hora",
|
|
274
|
+
"step.details": "Les teves dades",
|
|
275
|
+
"step.verify": "Introdueix el codi",
|
|
276
|
+
"step.pay": "Confirmar",
|
|
277
|
+
"step.done": "Reservat",
|
|
278
|
+
"step.expired": "S'ha esgotat el temps",
|
|
279
|
+
"label.date": "Data",
|
|
280
|
+
"label.time": "Hora",
|
|
281
|
+
"label.duration": "Durada",
|
|
282
|
+
"label.name": "Nom",
|
|
283
|
+
"label.phone": "Mòbil",
|
|
284
|
+
"label.email": "Correu",
|
|
285
|
+
"label.contact": "On enviem el codi",
|
|
286
|
+
"label.code": "Codi",
|
|
287
|
+
"label.notes": "Notes per a l'estudi",
|
|
288
|
+
"label.price": "Preu",
|
|
289
|
+
"label.deposit": "Paga i senyal",
|
|
290
|
+
"label.total": "Total",
|
|
291
|
+
"label.perHour": "per hora",
|
|
292
|
+
"action.continue": "Continuar",
|
|
293
|
+
"action.back": "Enrere",
|
|
294
|
+
"action.sendCode": "Envia’m un codi",
|
|
295
|
+
"action.verify": "Verificar",
|
|
296
|
+
"action.resend": "Torna a enviar",
|
|
297
|
+
"action.confirm": "Confirmar la reserva",
|
|
298
|
+
"action.payCard": "Pagar amb targeta",
|
|
299
|
+
"action.payOnSite": "Pagar a l'estudi",
|
|
300
|
+
"action.startOver": "Tornar a començar",
|
|
301
|
+
"action.manage": "Veure o cancel·lar la reserva",
|
|
302
|
+
"action.cancel": "Cancel·lar",
|
|
303
|
+
"hint.codeSent": "Hem enviat un codi a {contact}.",
|
|
304
|
+
"hint.resendsLeft": "Queden {n} reenviaments",
|
|
305
|
+
"hint.holdExpiresIn": "La teva hora està reservada durant {time}",
|
|
306
|
+
"hint.payOnSite": "No pagues res ara; es paga a l'estudi.",
|
|
307
|
+
"hint.payCard": "Ara es cobra una paga i senyal de {amount}.",
|
|
308
|
+
"hint.cancelNotice": "Cancel·lació gratuïta fins a {hours} h abans.",
|
|
309
|
+
"hint.sandbox": "Mode de proves: no s'envia ni es cobra res, i aquesta reserva s'esborrarà.",
|
|
310
|
+
"hint.noSlots": "No hi ha hores lliures aquell dia.",
|
|
311
|
+
"hint.pickDate": "Tria un dia per veure les hores lliures.",
|
|
312
|
+
"done.title": "Reserva confirmada",
|
|
313
|
+
"done.reference": "Referència {ref}",
|
|
314
|
+
"done.manage": "Guarda aquest enllaç per veure o cancel·lar la reserva.",
|
|
315
|
+
"channel.phone": "SMS",
|
|
316
|
+
"channel.email": "Correu",
|
|
317
|
+
"pay.card": "Targeta",
|
|
318
|
+
"pay.on_site": "A l'estudi",
|
|
319
|
+
"error.VALIDATION_ERROR": "Revisa el que has escrit.",
|
|
320
|
+
"error.RESOURCE_NOT_FOUND": "No ho hem trobat.",
|
|
321
|
+
"error.SITE_NOT_LIVE": "Aquest estudi encara no accepta reserves en línia.",
|
|
322
|
+
"error.ROOM_NOT_BOOKABLE": "Aquesta sala no es pot reservar en línia ara mateix.",
|
|
323
|
+
"error.STUDIO_NOT_VERIFIED": "Aquest estudi encara no accepta reserves en línia.",
|
|
324
|
+
"error.OUTSIDE_HOURS": "L'estudi està tancat a aquella hora.",
|
|
325
|
+
"error.OUTSIDE_BOOKING_WINDOW": "Aquesta data encara no es pot reservar: prova’n una de més propera.",
|
|
326
|
+
"error.SLOT_TAKEN": "Aquesta hora s'acaba d'ocupar. Tria’n una altra.",
|
|
327
|
+
"error.HOLD_EXPIRED": "S'ha esgotat el temps. Torna a triar una hora.",
|
|
328
|
+
"error.HOLD_NOT_VERIFIED": "Introdueix primer el codi.",
|
|
329
|
+
"error.HOLD_ALREADY_CONFIRMED": "Aquesta reserva ja està confirmada.",
|
|
330
|
+
"error.CODE_INVALID": "El codi no és correcte.",
|
|
331
|
+
"error.TOO_MANY_ATTEMPTS": "Massa intents. Torna a començar.",
|
|
332
|
+
"error.RATE_LIMIT_EXCEEDED": "Massa peticions. Espera un minut i torna-ho a provar.",
|
|
333
|
+
"error.CAPTCHA_FAILED": "Completa la verificació de seguretat.",
|
|
334
|
+
"error.SMS_UNAVAILABLE": "Ara no podem enviar SMS: fes servir el correu.",
|
|
335
|
+
"error.PAYMENTS_UNAVAILABLE": "El pagament amb targeta no està disponible: paga a l'estudi.",
|
|
336
|
+
"error.PAYMENT_MODE_DENIED": "Aquesta sala no accepta aquest mètode de pagament.",
|
|
337
|
+
"error.CANCEL_TOO_LATE": "Ja és tard per cancel·lar en línia: truca a l'estudi.",
|
|
338
|
+
"error.INVALID_STATE": "Aquesta acció no és possible ara.",
|
|
339
|
+
"error.INTERNAL_ERROR": "Alguna cosa ha fallat. Torna-ho a provar.",
|
|
340
|
+
"error.NETWORK_ERROR": "Sense connexió. Revisa la xarxa i torna-ho a provar.",
|
|
341
|
+
"error.default": "Alguna cosa ha fallat. Torna-ho a provar."
|
|
342
|
+
};
|
|
343
|
+
var fr = {
|
|
344
|
+
"step.slot": "Choisissez un horaire",
|
|
345
|
+
"step.details": "Vos coordonnées",
|
|
346
|
+
"step.verify": "Saisissez le code",
|
|
347
|
+
"step.pay": "Confirmer",
|
|
348
|
+
"step.done": "Réservé",
|
|
349
|
+
"step.expired": "Temps écoulé",
|
|
350
|
+
"label.date": "Date",
|
|
351
|
+
"label.time": "Heure",
|
|
352
|
+
"label.duration": "Durée",
|
|
353
|
+
"label.name": "Nom",
|
|
354
|
+
"label.phone": "Mobile",
|
|
355
|
+
"label.email": "E-mail",
|
|
356
|
+
"label.contact": "Où envoyer le code",
|
|
357
|
+
"label.code": "Code",
|
|
358
|
+
"label.notes": "Notes pour le studio",
|
|
359
|
+
"label.price": "Prix",
|
|
360
|
+
"label.deposit": "Acompte",
|
|
361
|
+
"label.total": "Total",
|
|
362
|
+
"label.perHour": "par heure",
|
|
363
|
+
"action.continue": "Continuer",
|
|
364
|
+
"action.back": "Retour",
|
|
365
|
+
"action.sendCode": "Recevoir un code",
|
|
366
|
+
"action.verify": "Vérifier",
|
|
367
|
+
"action.resend": "Renvoyer",
|
|
368
|
+
"action.confirm": "Confirmer la réservation",
|
|
369
|
+
"action.payCard": "Payer par carte",
|
|
370
|
+
"action.payOnSite": "Payer au studio",
|
|
371
|
+
"action.startOver": "Recommencer",
|
|
372
|
+
"action.manage": "Voir ou annuler la réservation",
|
|
373
|
+
"action.cancel": "Annuler",
|
|
374
|
+
"hint.codeSent": "Nous avons envoyé un code à {contact}.",
|
|
375
|
+
"hint.resendsLeft": "{n} renvois restants",
|
|
376
|
+
"hint.holdExpiresIn": "Votre créneau est bloqué pendant {time}",
|
|
377
|
+
"hint.payOnSite": "Rien à payer maintenant ; réglez au studio.",
|
|
378
|
+
"hint.payCard": "Un acompte de {amount} est débité maintenant.",
|
|
379
|
+
"hint.cancelNotice": "Annulation gratuite jusqu'à {hours} h avant.",
|
|
380
|
+
"hint.sandbox": "Mode test : rien n'est envoyé ni débité, et cette réservation sera supprimée.",
|
|
381
|
+
"hint.noSlots": "Aucun horaire libre ce jour-là.",
|
|
382
|
+
"hint.pickDate": "Choisissez un jour pour voir les horaires libres.",
|
|
383
|
+
"done.title": "Réservation confirmée",
|
|
384
|
+
"done.reference": "Référence {ref}",
|
|
385
|
+
"done.manage": "Conservez ce lien pour voir ou annuler la réservation.",
|
|
386
|
+
"channel.phone": "SMS",
|
|
387
|
+
"channel.email": "E-mail",
|
|
388
|
+
"pay.card": "Carte",
|
|
389
|
+
"pay.on_site": "Au studio",
|
|
390
|
+
"error.VALIDATION_ERROR": "Vérifiez ce que vous avez saisi.",
|
|
391
|
+
"error.RESOURCE_NOT_FOUND": "Introuvable.",
|
|
392
|
+
"error.SITE_NOT_LIVE": "Ce studio n'accepte pas encore les réservations en ligne.",
|
|
393
|
+
"error.ROOM_NOT_BOOKABLE": "Cette salle ne peut pas être réservée en ligne pour le moment.",
|
|
394
|
+
"error.STUDIO_NOT_VERIFIED": "Ce studio n'accepte pas encore les réservations en ligne.",
|
|
395
|
+
"error.OUTSIDE_HOURS": "Le studio est fermé à cette heure.",
|
|
396
|
+
"error.OUTSIDE_BOOKING_WINDOW": "Cette date ne peut pas encore être réservée — essayez une date plus proche.",
|
|
397
|
+
"error.SLOT_TAKEN": "Ce créneau vient d'être pris. Choisissez-en un autre.",
|
|
398
|
+
"error.HOLD_EXPIRED": "Temps écoulé. Choisissez à nouveau un horaire.",
|
|
399
|
+
"error.HOLD_NOT_VERIFIED": "Saisissez d'abord le code.",
|
|
400
|
+
"error.HOLD_ALREADY_CONFIRMED": "Cette réservation est déjà confirmée.",
|
|
401
|
+
"error.CODE_INVALID": "Ce code est incorrect.",
|
|
402
|
+
"error.TOO_MANY_ATTEMPTS": "Trop de tentatives. Recommencez.",
|
|
403
|
+
"error.RATE_LIMIT_EXCEEDED": "Trop de requêtes. Patientez une minute et réessayez.",
|
|
404
|
+
"error.CAPTCHA_FAILED": "Veuillez compléter la vérification de sécurité.",
|
|
405
|
+
"error.SMS_UNAVAILABLE": "Impossible d'envoyer des SMS pour le moment — utilisez l'e-mail.",
|
|
406
|
+
"error.PAYMENTS_UNAVAILABLE": "Le paiement par carte est indisponible — payez au studio.",
|
|
407
|
+
"error.PAYMENT_MODE_DENIED": "Ce moyen de paiement n'est pas accepté pour cette salle.",
|
|
408
|
+
"error.CANCEL_TOO_LATE": "Trop tard pour annuler en ligne — appelez le studio.",
|
|
409
|
+
"error.INVALID_STATE": "Cette action n'est pas possible pour le moment.",
|
|
410
|
+
"error.INTERNAL_ERROR": "Une erreur s'est produite. Réessayez.",
|
|
411
|
+
"error.NETWORK_ERROR": "Pas de connexion. Vérifiez votre réseau et réessayez.",
|
|
412
|
+
"error.default": "Une erreur s'est produite. Réessayez."
|
|
413
|
+
};
|
|
414
|
+
var pt = {
|
|
415
|
+
"step.slot": "Escolha uma hora",
|
|
416
|
+
"step.details": "Os seus dados",
|
|
417
|
+
"step.verify": "Introduza o código",
|
|
418
|
+
"step.pay": "Confirmar",
|
|
419
|
+
"step.done": "Reservado",
|
|
420
|
+
"step.expired": "O tempo esgotou-se",
|
|
421
|
+
"label.date": "Data",
|
|
422
|
+
"label.time": "Hora",
|
|
423
|
+
"label.duration": "Duração",
|
|
424
|
+
"label.name": "Nome",
|
|
425
|
+
"label.phone": "Telemóvel",
|
|
426
|
+
"label.email": "E-mail",
|
|
427
|
+
"label.contact": "Para onde enviamos o código",
|
|
428
|
+
"label.code": "Código",
|
|
429
|
+
"label.notes": "Notas para o estúdio",
|
|
430
|
+
"label.price": "Preço",
|
|
431
|
+
"label.deposit": "Sinal",
|
|
432
|
+
"label.total": "Total",
|
|
433
|
+
"label.perHour": "por hora",
|
|
434
|
+
"action.continue": "Continuar",
|
|
435
|
+
"action.back": "Voltar",
|
|
436
|
+
"action.sendCode": "Enviar-me um código",
|
|
437
|
+
"action.verify": "Verificar",
|
|
438
|
+
"action.resend": "Reenviar",
|
|
439
|
+
"action.confirm": "Confirmar reserva",
|
|
440
|
+
"action.payCard": "Pagar com cartão",
|
|
441
|
+
"action.payOnSite": "Pagar no estúdio",
|
|
442
|
+
"action.startOver": "Recomeçar",
|
|
443
|
+
"action.manage": "Ver ou cancelar a reserva",
|
|
444
|
+
"action.cancel": "Cancelar",
|
|
445
|
+
"hint.codeSent": "Enviámos um código para {contact}.",
|
|
446
|
+
"hint.resendsLeft": "Restam {n} reenvios",
|
|
447
|
+
"hint.holdExpiresIn": "A sua hora está reservada durante {time}",
|
|
448
|
+
"hint.payOnSite": "Não paga nada agora; paga no estúdio.",
|
|
449
|
+
"hint.payCard": "É cobrado agora um sinal de {amount}.",
|
|
450
|
+
"hint.cancelNotice": "Cancelamento gratuito até {hours} h antes.",
|
|
451
|
+
"hint.sandbox": "Modo de testes: nada é enviado nem cobrado, e esta reserva será apagada.",
|
|
452
|
+
"hint.noSlots": "Não há horas livres nesse dia.",
|
|
453
|
+
"hint.pickDate": "Escolha um dia para ver as horas livres.",
|
|
454
|
+
"done.title": "Reserva confirmada",
|
|
455
|
+
"done.reference": "Referência {ref}",
|
|
456
|
+
"done.manage": "Guarde esta ligação para ver ou cancelar a reserva.",
|
|
457
|
+
"channel.phone": "SMS",
|
|
458
|
+
"channel.email": "E-mail",
|
|
459
|
+
"pay.card": "Cartão",
|
|
460
|
+
"pay.on_site": "No estúdio",
|
|
461
|
+
"error.VALIDATION_ERROR": "Verifique o que escreveu.",
|
|
462
|
+
"error.RESOURCE_NOT_FOUND": "Não encontrado.",
|
|
463
|
+
"error.SITE_NOT_LIVE": "Este estúdio ainda não aceita reservas online.",
|
|
464
|
+
"error.ROOM_NOT_BOOKABLE": "Esta sala não pode ser reservada online neste momento.",
|
|
465
|
+
"error.STUDIO_NOT_VERIFIED": "Este estúdio ainda não aceita reservas online.",
|
|
466
|
+
"error.OUTSIDE_HOURS": "O estúdio está fechado a essa hora.",
|
|
467
|
+
"error.OUTSIDE_BOOKING_WINDOW": "Essa data ainda não pode ser reservada — tente uma mais próxima.",
|
|
468
|
+
"error.SLOT_TAKEN": "Essa hora acabou de ser ocupada. Escolha outra.",
|
|
469
|
+
"error.HOLD_EXPIRED": "O tempo esgotou-se. Volte a escolher uma hora.",
|
|
470
|
+
"error.HOLD_NOT_VERIFIED": "Introduza primeiro o código.",
|
|
471
|
+
"error.HOLD_ALREADY_CONFIRMED": "Esta reserva já está confirmada.",
|
|
472
|
+
"error.CODE_INVALID": "Esse código não está correto.",
|
|
473
|
+
"error.TOO_MANY_ATTEMPTS": "Demasiadas tentativas. Recomece.",
|
|
474
|
+
"error.RATE_LIMIT_EXCEEDED": "Demasiados pedidos. Aguarde um minuto e tente novamente.",
|
|
475
|
+
"error.CAPTCHA_FAILED": "Complete a verificação de segurança.",
|
|
476
|
+
"error.SMS_UNAVAILABLE": "Não é possível enviar SMS agora — use o e-mail.",
|
|
477
|
+
"error.PAYMENTS_UNAVAILABLE": "O pagamento com cartão está indisponível — pague no estúdio.",
|
|
478
|
+
"error.PAYMENT_MODE_DENIED": "Esse método de pagamento não é aceite nesta sala.",
|
|
479
|
+
"error.CANCEL_TOO_LATE": "É tarde para cancelar online — ligue ao estúdio.",
|
|
480
|
+
"error.INVALID_STATE": "Essa ação não é possível agora.",
|
|
481
|
+
"error.INTERNAL_ERROR": "Algo correu mal. Tente novamente.",
|
|
482
|
+
"error.NETWORK_ERROR": "Sem ligação. Verifique a rede e tente novamente.",
|
|
483
|
+
"error.default": "Algo correu mal. Tente novamente."
|
|
484
|
+
};
|
|
485
|
+
var it = {
|
|
486
|
+
"step.slot": "Scegli un orario",
|
|
487
|
+
"step.details": "I tuoi dati",
|
|
488
|
+
"step.verify": "Inserisci il codice",
|
|
489
|
+
"step.pay": "Conferma",
|
|
490
|
+
"step.done": "Prenotato",
|
|
491
|
+
"step.expired": "Tempo scaduto",
|
|
492
|
+
"label.date": "Data",
|
|
493
|
+
"label.time": "Ora",
|
|
494
|
+
"label.duration": "Durata",
|
|
495
|
+
"label.name": "Nome",
|
|
496
|
+
"label.phone": "Cellulare",
|
|
497
|
+
"label.email": "E-mail",
|
|
498
|
+
"label.contact": "Dove inviare il codice",
|
|
499
|
+
"label.code": "Codice",
|
|
500
|
+
"label.notes": "Note per lo studio",
|
|
501
|
+
"label.price": "Prezzo",
|
|
502
|
+
"label.deposit": "Caparra",
|
|
503
|
+
"label.total": "Totale",
|
|
504
|
+
"label.perHour": "all'ora",
|
|
505
|
+
"action.continue": "Continua",
|
|
506
|
+
"action.back": "Indietro",
|
|
507
|
+
"action.sendCode": "Inviami un codice",
|
|
508
|
+
"action.verify": "Verifica",
|
|
509
|
+
"action.resend": "Invia di nuovo",
|
|
510
|
+
"action.confirm": "Conferma la prenotazione",
|
|
511
|
+
"action.payCard": "Paga con carta",
|
|
512
|
+
"action.payOnSite": "Paga in studio",
|
|
513
|
+
"action.startOver": "Ricomincia",
|
|
514
|
+
"action.manage": "Vedi o annulla la prenotazione",
|
|
515
|
+
"action.cancel": "Annulla",
|
|
516
|
+
"hint.codeSent": "Abbiamo inviato un codice a {contact}.",
|
|
517
|
+
"hint.resendsLeft": "{n} invii rimasti",
|
|
518
|
+
"hint.holdExpiresIn": "Il tuo orario è bloccato per {time}",
|
|
519
|
+
"hint.payOnSite": "Niente da pagare ora; si paga in studio.",
|
|
520
|
+
"hint.payCard": "Viene addebitata ora una caparra di {amount}.",
|
|
521
|
+
"hint.cancelNotice": "Cancellazione gratuita fino a {hours} h prima.",
|
|
522
|
+
"hint.sandbox": "Modalità test: non viene inviato né addebitato nulla, e questa prenotazione sarà eliminata.",
|
|
523
|
+
"hint.noSlots": "Nessun orario libero quel giorno.",
|
|
524
|
+
"hint.pickDate": "Scegli un giorno per vedere gli orari liberi.",
|
|
525
|
+
"done.title": "Prenotazione confermata",
|
|
526
|
+
"done.reference": "Riferimento {ref}",
|
|
527
|
+
"done.manage": "Conserva questo link per vedere o annullare la prenotazione.",
|
|
528
|
+
"channel.phone": "SMS",
|
|
529
|
+
"channel.email": "E-mail",
|
|
530
|
+
"pay.card": "Carta",
|
|
531
|
+
"pay.on_site": "In studio",
|
|
532
|
+
"error.VALIDATION_ERROR": "Controlla quello che hai scritto.",
|
|
533
|
+
"error.RESOURCE_NOT_FOUND": "Non trovato.",
|
|
534
|
+
"error.SITE_NOT_LIVE": "Questo studio non accetta ancora prenotazioni online.",
|
|
535
|
+
"error.ROOM_NOT_BOOKABLE": "Questa sala non è prenotabile online al momento.",
|
|
536
|
+
"error.STUDIO_NOT_VERIFIED": "Questo studio non accetta ancora prenotazioni online.",
|
|
537
|
+
"error.OUTSIDE_HOURS": "Lo studio è chiuso a quell’ora.",
|
|
538
|
+
"error.OUTSIDE_BOOKING_WINDOW": "Quella data non è ancora prenotabile — provane una più vicina.",
|
|
539
|
+
"error.SLOT_TAKEN": "Quell’orario è appena stato preso. Scegline un altro.",
|
|
540
|
+
"error.HOLD_EXPIRED": "Tempo scaduto. Scegli di nuovo un orario.",
|
|
541
|
+
"error.HOLD_NOT_VERIFIED": "Inserisci prima il codice.",
|
|
542
|
+
"error.HOLD_ALREADY_CONFIRMED": "Questa prenotazione è già confermata.",
|
|
543
|
+
"error.CODE_INVALID": "Il codice non è corretto.",
|
|
544
|
+
"error.TOO_MANY_ATTEMPTS": "Troppi tentativi. Ricomincia.",
|
|
545
|
+
"error.RATE_LIMIT_EXCEEDED": "Troppe richieste. Aspetta un minuto e riprova.",
|
|
546
|
+
"error.CAPTCHA_FAILED": "Completa la verifica di sicurezza.",
|
|
547
|
+
"error.SMS_UNAVAILABLE": "Non possiamo inviare SMS al momento — usa l’e-mail.",
|
|
548
|
+
"error.PAYMENTS_UNAVAILABLE": "Il pagamento con carta non è disponibile — paga in studio.",
|
|
549
|
+
"error.PAYMENT_MODE_DENIED": "Questo metodo di pagamento non è accettato per questa sala.",
|
|
550
|
+
"error.CANCEL_TOO_LATE": "È troppo tardi per annullare online — chiama lo studio.",
|
|
551
|
+
"error.INVALID_STATE": "Questa azione non è possibile adesso.",
|
|
552
|
+
"error.INTERNAL_ERROR": "Qualcosa è andato storto. Riprova.",
|
|
553
|
+
"error.NETWORK_ERROR": "Nessuna connessione. Controlla la rete e riprova.",
|
|
554
|
+
"error.default": "Qualcosa è andato storto. Riprova."
|
|
555
|
+
};
|
|
556
|
+
var de = {
|
|
557
|
+
"step.slot": "Zeit wählen",
|
|
558
|
+
"step.details": "Deine Angaben",
|
|
559
|
+
"step.verify": "Code eingeben",
|
|
560
|
+
"step.pay": "Bestätigen",
|
|
561
|
+
"step.done": "Gebucht",
|
|
562
|
+
"step.expired": "Zeit abgelaufen",
|
|
563
|
+
"label.date": "Datum",
|
|
564
|
+
"label.time": "Uhrzeit",
|
|
565
|
+
"label.duration": "Dauer",
|
|
566
|
+
"label.name": "Name",
|
|
567
|
+
"label.phone": "Handy",
|
|
568
|
+
"label.email": "E-Mail",
|
|
569
|
+
"label.contact": "Wohin wir den Code schicken",
|
|
570
|
+
"label.code": "Code",
|
|
571
|
+
"label.notes": "Hinweise für das Studio",
|
|
572
|
+
"label.price": "Preis",
|
|
573
|
+
"label.deposit": "Anzahlung",
|
|
574
|
+
"label.total": "Gesamt",
|
|
575
|
+
"label.perHour": "pro Stunde",
|
|
576
|
+
"action.continue": "Weiter",
|
|
577
|
+
"action.back": "Zurück",
|
|
578
|
+
"action.sendCode": "Code schicken",
|
|
579
|
+
"action.verify": "Prüfen",
|
|
580
|
+
"action.resend": "Erneut senden",
|
|
581
|
+
"action.confirm": "Buchung bestätigen",
|
|
582
|
+
"action.payCard": "Mit Karte zahlen",
|
|
583
|
+
"action.payOnSite": "Im Studio zahlen",
|
|
584
|
+
"action.startOver": "Von vorn beginnen",
|
|
585
|
+
"action.manage": "Buchung ansehen oder stornieren",
|
|
586
|
+
"action.cancel": "Abbrechen",
|
|
587
|
+
"hint.codeSent": "Wir haben einen Code an {contact} geschickt.",
|
|
588
|
+
"hint.resendsLeft": "Noch {n} Versuche",
|
|
589
|
+
"hint.holdExpiresIn": "Dein Termin ist {time} lang reserviert",
|
|
590
|
+
"hint.payOnSite": "Jetzt nichts zu zahlen; du zahlst im Studio.",
|
|
591
|
+
"hint.payCard": "Eine Anzahlung von {amount} wird jetzt abgebucht.",
|
|
592
|
+
"hint.cancelNotice": "Kostenlose Stornierung bis {hours} h vorher.",
|
|
593
|
+
"hint.sandbox": "Testmodus: nichts wird gesendet oder abgebucht, und diese Buchung wird gelöscht.",
|
|
594
|
+
"hint.noSlots": "An diesem Tag ist nichts frei.",
|
|
595
|
+
"hint.pickDate": "Wähle einen Tag, um freie Zeiten zu sehen.",
|
|
596
|
+
"done.title": "Buchung bestätigt",
|
|
597
|
+
"done.reference": "Referenz {ref}",
|
|
598
|
+
"done.manage": "Bewahre diesen Link auf, um die Buchung anzusehen oder zu stornieren.",
|
|
599
|
+
"channel.phone": "SMS",
|
|
600
|
+
"channel.email": "E-Mail",
|
|
601
|
+
"pay.card": "Karte",
|
|
602
|
+
"pay.on_site": "Im Studio",
|
|
603
|
+
"error.VALIDATION_ERROR": "Bitte prüfe deine Eingaben.",
|
|
604
|
+
"error.RESOURCE_NOT_FOUND": "Nicht gefunden.",
|
|
605
|
+
"error.SITE_NOT_LIVE": "Dieses Studio nimmt noch keine Online-Buchungen an.",
|
|
606
|
+
"error.ROOM_NOT_BOOKABLE": "Dieser Raum kann gerade nicht online gebucht werden.",
|
|
607
|
+
"error.STUDIO_NOT_VERIFIED": "Dieses Studio nimmt noch keine Online-Buchungen an.",
|
|
608
|
+
"error.OUTSIDE_HOURS": "Das Studio ist zu dieser Zeit geschlossen.",
|
|
609
|
+
"error.OUTSIDE_BOOKING_WINDOW": "Dieses Datum kann noch nicht gebucht werden — versuche ein näheres.",
|
|
610
|
+
"error.SLOT_TAKEN": "Diese Zeit wurde gerade vergeben. Wähle eine andere.",
|
|
611
|
+
"error.HOLD_EXPIRED": "Zeit abgelaufen. Wähle die Zeit erneut.",
|
|
612
|
+
"error.HOLD_NOT_VERIFIED": "Gib zuerst den Code ein.",
|
|
613
|
+
"error.HOLD_ALREADY_CONFIRMED": "Diese Buchung ist bereits bestätigt.",
|
|
614
|
+
"error.CODE_INVALID": "Der Code ist falsch.",
|
|
615
|
+
"error.TOO_MANY_ATTEMPTS": "Zu viele Versuche. Beginne von vorn.",
|
|
616
|
+
"error.RATE_LIMIT_EXCEEDED": "Zu viele Anfragen. Warte eine Minute und versuche es erneut.",
|
|
617
|
+
"error.CAPTCHA_FAILED": "Bitte schließe die Sicherheitsprüfung ab.",
|
|
618
|
+
"error.SMS_UNAVAILABLE": "SMS sind gerade nicht möglich — nutze E-Mail.",
|
|
619
|
+
"error.PAYMENTS_UNAVAILABLE": "Kartenzahlung ist gerade nicht verfügbar — zahle im Studio.",
|
|
620
|
+
"error.PAYMENT_MODE_DENIED": "Diese Zahlungsart wird für diesen Raum nicht akzeptiert.",
|
|
621
|
+
"error.CANCEL_TOO_LATE": "Für eine Online-Stornierung ist es zu spät — ruf das Studio an.",
|
|
622
|
+
"error.INVALID_STATE": "Diese Aktion ist gerade nicht möglich.",
|
|
623
|
+
"error.INTERNAL_ERROR": "Etwas ist schiefgelaufen. Versuche es erneut.",
|
|
624
|
+
"error.NETWORK_ERROR": "Keine Verbindung. Prüfe dein Netzwerk und versuche es erneut.",
|
|
625
|
+
"error.default": "Etwas ist schiefgelaufen. Versuche es erneut."
|
|
626
|
+
};
|
|
627
|
+
var MESSAGES = { en, es, ca, fr, pt, it, de };
|
|
628
|
+
function t(locale, key, params) {
|
|
629
|
+
const dict = MESSAGES[normalizeLocale(locale)];
|
|
630
|
+
let s = dict[key] ?? en[key] ?? key;
|
|
631
|
+
if (params)
|
|
632
|
+
for (const [k, v] of Object.entries(params))
|
|
633
|
+
s = s.split(`{${k}}`).join(String(v));
|
|
634
|
+
return s;
|
|
635
|
+
}
|
|
636
|
+
function messageFor(error, locale) {
|
|
637
|
+
const e = error ?? {};
|
|
638
|
+
const dict = MESSAGES[normalizeLocale(locale)];
|
|
639
|
+
if (e.code && dict[`error.${e.code}`])
|
|
640
|
+
return dict[`error.${e.code}`];
|
|
641
|
+
if (e.code && en[`error.${e.code}`])
|
|
642
|
+
return en[`error.${e.code}`];
|
|
643
|
+
return dict["error.default"];
|
|
644
|
+
}
|
|
645
|
+
function translator(locale) {
|
|
646
|
+
const l = normalizeLocale(locale);
|
|
647
|
+
const fn = (key, params) => t(l, key, params);
|
|
648
|
+
fn.locale = l;
|
|
649
|
+
fn.error = (error) => messageFor(error, l);
|
|
650
|
+
return fn;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/links.ts
|
|
654
|
+
function createLinks(endpoints, getLocale = () => {
|
|
655
|
+
return;
|
|
656
|
+
}) {
|
|
657
|
+
const siteOrigin = (slug) => `https://${slug}.${endpoints.sitesBase}`;
|
|
658
|
+
return {
|
|
659
|
+
site(slug) {
|
|
660
|
+
return `${siteOrigin(slug)}/`;
|
|
661
|
+
},
|
|
662
|
+
bookRoom(o) {
|
|
663
|
+
const q = new URLSearchParams;
|
|
664
|
+
if (o.date)
|
|
665
|
+
q.set("date", o.date);
|
|
666
|
+
if (o.start)
|
|
667
|
+
q.set("start", o.start);
|
|
668
|
+
if (o.durationMinutes)
|
|
669
|
+
q.set("duration", String(o.durationMinutes));
|
|
670
|
+
const lang = o.lang ?? getLocale();
|
|
671
|
+
if (lang)
|
|
672
|
+
q.set("lang", toHostedLocale(lang));
|
|
673
|
+
if (o.embed)
|
|
674
|
+
q.set("embed", "1");
|
|
675
|
+
const s = q.toString();
|
|
676
|
+
return `${siteOrigin(o.slug)}/reservar/${o.roomId}${s ? `?${s}` : ""}`;
|
|
677
|
+
},
|
|
678
|
+
manage(slug, manageToken) {
|
|
679
|
+
return `${siteOrigin(slug)}/r/${manageToken}`;
|
|
680
|
+
},
|
|
681
|
+
bookLesson(o) {
|
|
682
|
+
const q = new URLSearchParams({ instructorId: o.instructorId });
|
|
683
|
+
if (o.offerId)
|
|
684
|
+
q.set("offerId", o.offerId);
|
|
685
|
+
return `${endpoints.appBase}/bookings/book?${q.toString()}`;
|
|
686
|
+
},
|
|
687
|
+
teacherProfile(instructorUserId) {
|
|
688
|
+
return `${endpoints.appBase}/instructors/${instructorUserId}`;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/embed.ts
|
|
694
|
+
function isTrustedOrigin(origin, sitesBase) {
|
|
695
|
+
try {
|
|
696
|
+
const host = new URL(origin).host;
|
|
697
|
+
return host === sitesBase || host.endsWith(`.${sitesBase}`);
|
|
698
|
+
} catch {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function parseEmbedEvent(data) {
|
|
703
|
+
if (!data || typeof data !== "object")
|
|
704
|
+
return null;
|
|
705
|
+
const d = data;
|
|
706
|
+
if (d.type === "mrcharles:resize" && typeof d.height === "number")
|
|
707
|
+
return { type: "mrcharles:resize", height: d.height };
|
|
708
|
+
if (d.type === "mrcharles:booked" && typeof d.reference === "string") {
|
|
709
|
+
return {
|
|
710
|
+
type: "mrcharles:booked",
|
|
711
|
+
reference: d.reference,
|
|
712
|
+
bookingId: String(d.bookingId ?? ""),
|
|
713
|
+
manageUrl: String(d.manageUrl ?? ""),
|
|
714
|
+
status: String(d.status ?? "")
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function createEmbed(endpoints, getLocale = () => {
|
|
720
|
+
return;
|
|
721
|
+
}) {
|
|
722
|
+
const links = createLinks(endpoints, getLocale);
|
|
723
|
+
function mountRoomBooking(target, opts) {
|
|
724
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
725
|
+
if (!el)
|
|
726
|
+
throw new Error(`@mrcharles/sdk: embed target not found: ${String(target)}`);
|
|
727
|
+
const iframe = document.createElement("iframe");
|
|
728
|
+
iframe.src = links.bookRoom({ ...opts, embed: true });
|
|
729
|
+
iframe.title = opts.title ?? "Mr Charles booking";
|
|
730
|
+
iframe.loading = "lazy";
|
|
731
|
+
iframe.referrerPolicy = "strict-origin-when-cross-origin";
|
|
732
|
+
iframe.setAttribute("allow", "payment");
|
|
733
|
+
iframe.style.width = "100%";
|
|
734
|
+
iframe.style.border = "0";
|
|
735
|
+
iframe.style.display = "block";
|
|
736
|
+
iframe.style.height = `${opts.height ?? 760}px`;
|
|
737
|
+
if (opts.className)
|
|
738
|
+
iframe.className = opts.className;
|
|
739
|
+
const expectedOrigin = new URL(iframe.src).origin;
|
|
740
|
+
const onMessage = (e) => {
|
|
741
|
+
if (e.source !== iframe.contentWindow)
|
|
742
|
+
return;
|
|
743
|
+
if (e.origin !== expectedOrigin && !isTrustedOrigin(e.origin, endpoints.sitesBase))
|
|
744
|
+
return;
|
|
745
|
+
const ev = parseEmbedEvent(e.data);
|
|
746
|
+
if (!ev)
|
|
747
|
+
return;
|
|
748
|
+
if (ev.type === "mrcharles:resize") {
|
|
749
|
+
if (opts.autoResize !== false)
|
|
750
|
+
iframe.style.height = `${ev.height}px`;
|
|
751
|
+
opts.onResize?.(ev.height);
|
|
752
|
+
} else {
|
|
753
|
+
opts.onBooked?.(ev);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
window.addEventListener("message", onMessage);
|
|
757
|
+
el.appendChild(iframe);
|
|
758
|
+
return {
|
|
759
|
+
iframe,
|
|
760
|
+
setLang(lang) {
|
|
761
|
+
iframe.src = links.bookRoom({ ...opts, lang, embed: true });
|
|
762
|
+
},
|
|
763
|
+
destroy() {
|
|
764
|
+
window.removeEventListener("message", onMessage);
|
|
765
|
+
iframe.remove();
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return { mountRoomBooking };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/uuid.ts
|
|
773
|
+
function uuid() {
|
|
774
|
+
const c = globalThis.crypto;
|
|
775
|
+
if (c?.randomUUID)
|
|
776
|
+
return c.randomUUID();
|
|
777
|
+
const b = new Uint8Array(16);
|
|
778
|
+
if (c?.getRandomValues)
|
|
779
|
+
c.getRandomValues(b);
|
|
780
|
+
else
|
|
781
|
+
for (let i = 0;i < 16; i++)
|
|
782
|
+
b[i] = Math.floor(Math.random() * 256);
|
|
783
|
+
b[6] = b[6] & 15 | 64;
|
|
784
|
+
b[8] = b[8] & 63 | 128;
|
|
785
|
+
const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
786
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/slots.ts
|
|
790
|
+
function toMinutes(hhmm) {
|
|
791
|
+
const [h, m] = hhmm.split(":").map(Number);
|
|
792
|
+
return (h || 0) * 60 + (m || 0);
|
|
793
|
+
}
|
|
794
|
+
function toHHMM(minutes) {
|
|
795
|
+
const h = Math.floor(minutes / 60);
|
|
796
|
+
const m = minutes % 60;
|
|
797
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
|
798
|
+
}
|
|
799
|
+
function addMinutes(hhmm, minutes) {
|
|
800
|
+
return toHHMM(toMinutes(hhmm) + minutes);
|
|
801
|
+
}
|
|
802
|
+
function startTimes(free, durationMinutes, stepMinutes) {
|
|
803
|
+
const step = Math.max(1, stepMinutes);
|
|
804
|
+
const out = new Set;
|
|
805
|
+
for (const span of free) {
|
|
806
|
+
const s = toMinutes(span.start);
|
|
807
|
+
const e = toMinutes(span.end);
|
|
808
|
+
for (let t = Math.ceil(s / step) * step;t + durationMinutes <= e; t += step)
|
|
809
|
+
out.add(toHHMM(t));
|
|
810
|
+
}
|
|
811
|
+
return [...out].sort((a, b) => toMinutes(a) - toMinutes(b));
|
|
812
|
+
}
|
|
813
|
+
function durationOptions(free, start, minMinutes, maxMinutes, stepMinutes) {
|
|
814
|
+
const s = toMinutes(start);
|
|
815
|
+
const span = free.find((f) => toMinutes(f.start) <= s && s < toMinutes(f.end));
|
|
816
|
+
if (!span)
|
|
817
|
+
return [];
|
|
818
|
+
const room = toMinutes(span.end) - s;
|
|
819
|
+
const out = [];
|
|
820
|
+
for (let d = minMinutes;d <= maxMinutes && d <= room; d += Math.max(1, stepMinutes))
|
|
821
|
+
out.push(d);
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function formatPrice(cents, currency, locale = "es-ES") {
|
|
825
|
+
try {
|
|
826
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(cents / 100);
|
|
827
|
+
} catch {
|
|
828
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function dateKey(date, timeZone) {
|
|
832
|
+
const parts = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date);
|
|
833
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
834
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
835
|
+
}
|
|
836
|
+
function addDays(key, days) {
|
|
837
|
+
const [y, m, d] = key.split("-").map(Number);
|
|
838
|
+
const t = Date.UTC(y, m - 1, d + days);
|
|
839
|
+
return new Date(t).toISOString().slice(0, 10);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/rooms.ts
|
|
843
|
+
function createRoomBooking(http, links, opts, clientLocale) {
|
|
844
|
+
const explicit = opts.locale ?? clientLocale?.();
|
|
845
|
+
let state = {
|
|
846
|
+
step: "slot",
|
|
847
|
+
site: null,
|
|
848
|
+
room: null,
|
|
849
|
+
availability: null,
|
|
850
|
+
hold: null,
|
|
851
|
+
confirmation: null,
|
|
852
|
+
error: null,
|
|
853
|
+
errorMessage: null,
|
|
854
|
+
busy: false,
|
|
855
|
+
locale: normalizeLocale(explicit, "es")
|
|
856
|
+
};
|
|
857
|
+
const listeners = new Set;
|
|
858
|
+
if (opts.onChange)
|
|
859
|
+
listeners.add(opts.onChange);
|
|
860
|
+
let idempotencyKey = uuid();
|
|
861
|
+
let expiryTimer;
|
|
862
|
+
const set = (patch) => {
|
|
863
|
+
state = { ...state, ...patch };
|
|
864
|
+
if ("error" in patch || "locale" in patch)
|
|
865
|
+
state.errorMessage = state.error ? messageFor(state.error, state.locale) : null;
|
|
866
|
+
for (const l of listeners)
|
|
867
|
+
l(state);
|
|
868
|
+
};
|
|
869
|
+
const clearTimer = () => {
|
|
870
|
+
if (expiryTimer)
|
|
871
|
+
clearTimeout(expiryTimer);
|
|
872
|
+
expiryTimer = undefined;
|
|
873
|
+
};
|
|
874
|
+
const armTimer = (hold) => {
|
|
875
|
+
clearTimer();
|
|
876
|
+
const ms = new Date(hold.expiresAt).getTime() - Date.now();
|
|
877
|
+
if (ms <= 0)
|
|
878
|
+
return;
|
|
879
|
+
expiryTimer = setTimeout(() => {
|
|
880
|
+
if (state.step === "verify" || state.step === "pay") {
|
|
881
|
+
idempotencyKey = uuid();
|
|
882
|
+
set({ step: "expired", error: new MrCharlesError("HOLD_EXPIRED", t(state.locale, "error.HOLD_EXPIRED"), 410) });
|
|
883
|
+
}
|
|
884
|
+
}, ms);
|
|
885
|
+
};
|
|
886
|
+
async function run(fn) {
|
|
887
|
+
set({ busy: true });
|
|
888
|
+
try {
|
|
889
|
+
const out = await fn();
|
|
890
|
+
set({ busy: false, error: null });
|
|
891
|
+
return out;
|
|
892
|
+
} catch (e) {
|
|
893
|
+
const err = isMrCharlesError(e) ? e : new MrCharlesError("INTERNAL_ERROR", e?.message ?? "Unexpected error");
|
|
894
|
+
if (err.holdIsDead) {
|
|
895
|
+
clearTimer();
|
|
896
|
+
idempotencyKey = uuid();
|
|
897
|
+
set({ busy: false, error: err, step: "expired", hold: null });
|
|
898
|
+
} else {
|
|
899
|
+
set({ busy: false, error: err });
|
|
900
|
+
}
|
|
901
|
+
throw err;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const api = {
|
|
905
|
+
get state() {
|
|
906
|
+
return state;
|
|
907
|
+
},
|
|
908
|
+
subscribe(fn) {
|
|
909
|
+
listeners.add(fn);
|
|
910
|
+
fn(state);
|
|
911
|
+
return () => {
|
|
912
|
+
listeners.delete(fn);
|
|
913
|
+
};
|
|
914
|
+
},
|
|
915
|
+
loadSite: () => run(async () => {
|
|
916
|
+
const site = await http.get(`/public/sites/${encodeURIComponent(opts.slug)}`);
|
|
917
|
+
const room = site.rooms.find((r) => r.id === opts.roomId) ?? null;
|
|
918
|
+
if (!room)
|
|
919
|
+
throw new MrCharlesError("RESOURCE_NOT_FOUND", `Room ${opts.roomId} is not on site ${opts.slug}`, 404);
|
|
920
|
+
if (!room.bookable)
|
|
921
|
+
throw new MrCharlesError("ROOM_NOT_BOOKABLE", `${room.name} does not take online bookings right now`, 409);
|
|
922
|
+
const locale = explicit ? state.locale : normalizeLocale(site.site?.defaultLocale, state.locale);
|
|
923
|
+
set({ site, room, locale });
|
|
924
|
+
return site;
|
|
925
|
+
}),
|
|
926
|
+
loadAvailability: (window2 = {}) => run(async () => {
|
|
927
|
+
const tz = state.site?.studio.timezone ?? "UTC";
|
|
928
|
+
const from = window2.from ?? dateKey(new Date, tz);
|
|
929
|
+
const to = window2.to ?? addDays(from, Math.min(31, Math.max(1, window2.days ?? 14)) - 1);
|
|
930
|
+
const availability = await http.get(`/public/sites/${encodeURIComponent(opts.slug)}/rooms/${encodeURIComponent(opts.roomId)}/availability`, { from, to });
|
|
931
|
+
set({ availability });
|
|
932
|
+
return availability;
|
|
933
|
+
}),
|
|
934
|
+
startTimesFor(date, durationMinutes) {
|
|
935
|
+
const a = state.availability;
|
|
936
|
+
if (!a)
|
|
937
|
+
return [];
|
|
938
|
+
const day = a.days.find((d) => d.date === date);
|
|
939
|
+
return day ? startTimes(day.free, durationMinutes, a.stepMinutes) : [];
|
|
940
|
+
},
|
|
941
|
+
durationsFor(date, startTime) {
|
|
942
|
+
const a = state.availability;
|
|
943
|
+
if (!a)
|
|
944
|
+
return [];
|
|
945
|
+
const day = a.days.find((d) => d.date === date);
|
|
946
|
+
return day ? durationOptions(day.free, startTime, a.minMinutes, a.maxMinutes, a.stepMinutes) : [];
|
|
947
|
+
},
|
|
948
|
+
hold: (details) => run(async () => {
|
|
949
|
+
if (state.step === "expired")
|
|
950
|
+
set({ step: "slot", error: null });
|
|
951
|
+
const body = {
|
|
952
|
+
roomId: opts.roomId,
|
|
953
|
+
date: details.date,
|
|
954
|
+
startTime: details.startTime,
|
|
955
|
+
durationMinutes: details.durationMinutes,
|
|
956
|
+
guestName: details.guestName,
|
|
957
|
+
channel: details.channel,
|
|
958
|
+
contact: details.contact,
|
|
959
|
+
locale: toHostedLocale(details.locale ?? state.locale),
|
|
960
|
+
idempotencyKey,
|
|
961
|
+
...details.captchaToken ? { captchaToken: details.captchaToken } : {}
|
|
962
|
+
};
|
|
963
|
+
const hold = await http.post(`/public/sites/${encodeURIComponent(opts.slug)}/holds`, body);
|
|
964
|
+
armTimer(hold);
|
|
965
|
+
set({ hold, step: "verify", confirmation: null });
|
|
966
|
+
return hold;
|
|
967
|
+
}),
|
|
968
|
+
verify: (code) => run(async () => {
|
|
969
|
+
const id = requireHold(state).holdId;
|
|
970
|
+
const hold = await http.post(`/public/holds/${id}/verify`, { code: code.replace(/\D/g, "") });
|
|
971
|
+
set({ hold, step: "pay" });
|
|
972
|
+
return hold;
|
|
973
|
+
}),
|
|
974
|
+
resend: () => run(async () => {
|
|
975
|
+
const id = requireHold(state).holdId;
|
|
976
|
+
const hold = await http.post(`/public/holds/${id}/resend`);
|
|
977
|
+
set({ hold });
|
|
978
|
+
return hold;
|
|
979
|
+
}),
|
|
980
|
+
confirm: (req) => run(async () => {
|
|
981
|
+
const h = requireHold(state);
|
|
982
|
+
if (!h.paymentModes.includes(req.paymentMode)) {
|
|
983
|
+
throw new MrCharlesError("PAYMENT_MODE_DENIED", `This room does not accept ${req.paymentMode}`, 409);
|
|
984
|
+
}
|
|
985
|
+
const confirmation = await http.post(`/public/holds/${h.holdId}/confirm`, {
|
|
986
|
+
paymentMode: req.paymentMode,
|
|
987
|
+
notes: req.notes ?? "",
|
|
988
|
+
email: req.email ?? ""
|
|
989
|
+
});
|
|
990
|
+
clearTimer();
|
|
991
|
+
if (!confirmation.manageUrl && confirmation.manageToken)
|
|
992
|
+
confirmation.manageUrl = links.manage(opts.slug, confirmation.manageToken);
|
|
993
|
+
set({ confirmation, step: "done" });
|
|
994
|
+
return confirmation;
|
|
995
|
+
}),
|
|
996
|
+
remainingMs() {
|
|
997
|
+
const h = state.hold;
|
|
998
|
+
if (!h || state.step === "done" || state.step === "expired")
|
|
999
|
+
return 0;
|
|
1000
|
+
return Math.max(0, new Date(h.expiresAt).getTime() - Date.now());
|
|
1001
|
+
},
|
|
1002
|
+
reset() {
|
|
1003
|
+
clearTimer();
|
|
1004
|
+
idempotencyKey = uuid();
|
|
1005
|
+
set({ step: "slot", hold: null, confirmation: null, error: null, busy: false });
|
|
1006
|
+
},
|
|
1007
|
+
t: (key, params) => t(state.locale, key, params),
|
|
1008
|
+
setLocale(locale) {
|
|
1009
|
+
set({ locale: normalizeLocale(locale, state.locale) });
|
|
1010
|
+
},
|
|
1011
|
+
destroy() {
|
|
1012
|
+
clearTimer();
|
|
1013
|
+
listeners.clear();
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
return api;
|
|
1017
|
+
}
|
|
1018
|
+
function requireHold(s) {
|
|
1019
|
+
if (!s.hold)
|
|
1020
|
+
throw new MrCharlesError("INVALID_STATE", "No hold yet — call hold() first", 409);
|
|
1021
|
+
return s.hold;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/format.ts
|
|
1025
|
+
function createFormatter(locale) {
|
|
1026
|
+
const tag = intlTag(locale);
|
|
1027
|
+
const lang = normalizeLocale(locale);
|
|
1028
|
+
const hourAbbr = { es: ["h", "min"], ca: ["h", "min"], en: ["h", "min"], fr: ["h", "min"], pt: ["h", "min"], it: ["h", "min"], de: ["Std.", "Min."] };
|
|
1029
|
+
const [hU, mU] = hourAbbr[lang] ?? ["h", "min"];
|
|
1030
|
+
const safe = (fn, fallback) => {
|
|
1031
|
+
try {
|
|
1032
|
+
return fn();
|
|
1033
|
+
} catch {
|
|
1034
|
+
return fallback;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
return {
|
|
1038
|
+
locale: tag,
|
|
1039
|
+
price: (cents, currency) => safe(() => new Intl.NumberFormat(tag, { style: "currency", currency }).format(cents / 100), `${(cents / 100).toFixed(2)} ${currency}`),
|
|
1040
|
+
date: (key, style = "long") => {
|
|
1041
|
+
const [y, m, d] = key.split("-").map(Number);
|
|
1042
|
+
const dt = new Date(Date.UTC(y, m - 1, d, 12));
|
|
1043
|
+
const opts = style === "long" ? { weekday: "long", day: "numeric", month: "long", timeZone: "UTC" } : style === "medium" ? { weekday: "short", day: "numeric", month: "short", timeZone: "UTC" } : { day: "2-digit", month: "2-digit", timeZone: "UTC" };
|
|
1044
|
+
return safe(() => new Intl.DateTimeFormat(tag, opts).format(dt), key);
|
|
1045
|
+
},
|
|
1046
|
+
time: (hhmm) => {
|
|
1047
|
+
const [h, mi] = hhmm.split(":").map(Number);
|
|
1048
|
+
const dt = new Date(Date.UTC(2000, 0, 1, h, mi));
|
|
1049
|
+
return safe(() => new Intl.DateTimeFormat(tag, { hour: "numeric", minute: "2-digit", timeZone: "UTC" }).format(dt), hhmm);
|
|
1050
|
+
},
|
|
1051
|
+
timeRange(start, minutes) {
|
|
1052
|
+
const [h, mi] = start.split(":").map(Number);
|
|
1053
|
+
const total = h * 60 + mi + minutes;
|
|
1054
|
+
const end = `${String(Math.floor(total / 60) % 24).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
|
|
1055
|
+
return `${this.time(start)} – ${this.time(end)}`;
|
|
1056
|
+
},
|
|
1057
|
+
duration: (minutes) => {
|
|
1058
|
+
const h = Math.floor(minutes / 60);
|
|
1059
|
+
const m = minutes % 60;
|
|
1060
|
+
if (h && m)
|
|
1061
|
+
return `${h} ${hU} ${m} ${mU}`;
|
|
1062
|
+
if (h)
|
|
1063
|
+
return `${h} ${hU}`;
|
|
1064
|
+
return `${m} ${mU}`;
|
|
1065
|
+
},
|
|
1066
|
+
weekday: (dow, style = "short") => {
|
|
1067
|
+
const dt = new Date(Date.UTC(2023, 0, 1 + (dow % 7 + 7) % 7, 12));
|
|
1068
|
+
return safe(() => new Intl.DateTimeFormat(tag, { weekday: style, timeZone: "UTC" }).format(dt), String(dow));
|
|
1069
|
+
},
|
|
1070
|
+
countdown: (ms) => {
|
|
1071
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
1072
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/client.ts
|
|
1078
|
+
function createClient(config = {}) {
|
|
1079
|
+
const endpoints = resolveEndpoints(config);
|
|
1080
|
+
let locale = config.locale ? normalizeLocale(config.locale) : undefined;
|
|
1081
|
+
let formatter;
|
|
1082
|
+
const getLocale = () => locale;
|
|
1083
|
+
const http = createHttp(endpoints.apiBase, config, getLocale);
|
|
1084
|
+
const links = createLinks(endpoints, getLocale);
|
|
1085
|
+
const embed = createEmbed(endpoints, getLocale);
|
|
1086
|
+
const enc = encodeURIComponent;
|
|
1087
|
+
const sites = {
|
|
1088
|
+
get: (slug) => http.get(`/public/sites/${enc(slug)}`),
|
|
1089
|
+
resolve: (host) => http.get("/public/sites/resolve", { host }),
|
|
1090
|
+
availability: (slug, roomId, range) => http.get(`/public/sites/${enc(slug)}/rooms/${enc(roomId)}/availability`, range)
|
|
1091
|
+
};
|
|
1092
|
+
const holds = {
|
|
1093
|
+
create: (slug, req) => http.post(`/public/sites/${enc(slug)}/holds`, req),
|
|
1094
|
+
verify: (holdId, code) => http.post(`/public/holds/${enc(holdId)}/verify`, { code }),
|
|
1095
|
+
resend: (holdId) => http.post(`/public/holds/${enc(holdId)}/resend`),
|
|
1096
|
+
confirm: (holdId, req) => http.post(`/public/holds/${enc(holdId)}/confirm`, { paymentMode: req.paymentMode, notes: req.notes ?? "", email: req.email ?? "" })
|
|
1097
|
+
};
|
|
1098
|
+
const bookings = {
|
|
1099
|
+
get: (manageToken) => http.get(`/public/bookings/manage/${enc(manageToken)}`),
|
|
1100
|
+
cancel: (manageToken, reason = "") => http.post(`/public/bookings/manage/${enc(manageToken)}/cancel`, { reason })
|
|
1101
|
+
};
|
|
1102
|
+
const teachers = {
|
|
1103
|
+
profile: (instructorUserId) => http.get(`/instructors/${enc(instructorUserId)}/teacher-profile`),
|
|
1104
|
+
offers: (instructorUserId) => http.get(`/instructors/${enc(instructorUserId)}/offers`),
|
|
1105
|
+
offer: (offerId) => http.get(`/offers/${enc(offerId)}`),
|
|
1106
|
+
windows: (instructorUserId, range) => http.get(`/availability/${enc(instructorUserId)}/windows`, range),
|
|
1107
|
+
policy: (instructorUserId) => http.get(`/availability/${enc(instructorUserId)}/policy`),
|
|
1108
|
+
slots: (q) => http.get("/bookings/slots", { instructorId: q.instructorUserId, offerId: q.offerId, date: q.date, days: q.days ?? 7 })
|
|
1109
|
+
};
|
|
1110
|
+
return {
|
|
1111
|
+
endpoints,
|
|
1112
|
+
sites,
|
|
1113
|
+
holds,
|
|
1114
|
+
bookings,
|
|
1115
|
+
teachers,
|
|
1116
|
+
links,
|
|
1117
|
+
mountRoomBooking: embed.mountRoomBooking,
|
|
1118
|
+
roomBooking: (opts) => createRoomBooking(http, links, opts, getLocale),
|
|
1119
|
+
get locale() {
|
|
1120
|
+
return locale;
|
|
1121
|
+
},
|
|
1122
|
+
get hostedLocale() {
|
|
1123
|
+
return locale ? toHostedLocale(locale) : "es";
|
|
1124
|
+
},
|
|
1125
|
+
setLocale(next) {
|
|
1126
|
+
locale = next ? normalizeLocale(next) : undefined;
|
|
1127
|
+
formatter = undefined;
|
|
1128
|
+
},
|
|
1129
|
+
t: (key, params) => t(locale ?? "en", key, params),
|
|
1130
|
+
errorMessage: (error) => messageFor(error, locale ?? "en"),
|
|
1131
|
+
get format() {
|
|
1132
|
+
return formatter ??= createFormatter(locale ?? "en");
|
|
1133
|
+
},
|
|
1134
|
+
pick: (localized, defaultLocale) => pick(localized, locale ?? defaultLocale ?? "es", defaultLocale)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/index.ts
|
|
1139
|
+
var VERSION = "0.2.0";
|
|
1140
|
+
export {
|
|
1141
|
+
ENVIRONMENTS,
|
|
1142
|
+
HOSTED_LOCALES,
|
|
1143
|
+
LOCALES,
|
|
1144
|
+
MESSAGES,
|
|
1145
|
+
MrCharlesError,
|
|
1146
|
+
VERSION,
|
|
1147
|
+
addDays,
|
|
1148
|
+
addMinutes,
|
|
1149
|
+
createClient,
|
|
1150
|
+
createEmbed,
|
|
1151
|
+
createFormatter,
|
|
1152
|
+
createLinks,
|
|
1153
|
+
createRoomBooking,
|
|
1154
|
+
dateKey,
|
|
1155
|
+
durationOptions,
|
|
1156
|
+
formatPrice,
|
|
1157
|
+
intlTag,
|
|
1158
|
+
isMrCharlesError,
|
|
1159
|
+
isTrustedOrigin,
|
|
1160
|
+
messageFor,
|
|
1161
|
+
normalizeLocale,
|
|
1162
|
+
parseEmbedEvent,
|
|
1163
|
+
pick,
|
|
1164
|
+
resolveEndpoints,
|
|
1165
|
+
startTimes,
|
|
1166
|
+
t,
|
|
1167
|
+
toHHMM,
|
|
1168
|
+
toHostedLocale,
|
|
1169
|
+
toMinutes,
|
|
1170
|
+
translator,
|
|
1171
|
+
uuid
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
//# debugId=1DCD9FB716D0EA3864756E2164756E21
|
|
1175
|
+
//# sourceMappingURL=mrcharles.esm.js.map
|