@vimine/sdk 0.2.0 → 0.3.3
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 +1 -1
- package/dist/elements.d.ts +217 -5
- package/dist/elements.js +627 -123
- package/dist/elements.js.map +1 -1
- package/dist/generated.d.ts +64 -9
- package/dist/generated.js +3 -1
- package/dist/generated.js.map +1 -1
- package/dist/public.d.ts +12 -4
- package/dist/public.js +3 -2
- package/dist/public.js.map +1 -1
- package/dist/types.d.ts +74 -5
- package/dist/types.js.map +1 -1
- package/package.json +6 -1
package/dist/elements.js
CHANGED
|
@@ -7,32 +7,340 @@
|
|
|
7
7
|
//
|
|
8
8
|
// <vimine-sign-in scope="membership:read"></vimine-sign-in> a "Sign in with Vimine" button
|
|
9
9
|
// <vimine-card></vimine-card> the signed-in member's card (SVG + QR)
|
|
10
|
-
// <vimine-enroll slug="your-slug"></vimine-enroll> the hosted enrolment form (iframe, Embed v0)
|
|
10
|
+
// <vimine-enroll slug="your-slug"></vimine-enroll> the hosted enrolment form (iframe, Embed v0), in the
|
|
11
|
+
// element's language; someone who already has a
|
|
12
|
+
// Vimine account is offered a hand-off to the hosted
|
|
13
|
+
// page, where they join in one step
|
|
11
14
|
// <vimine-events slug="your-slug"></vimine-events> the association's events (public ones for
|
|
12
15
|
// visitors, every one for a signed-in member) with
|
|
13
16
|
// a register button per type
|
|
14
17
|
//
|
|
15
18
|
// The callback page still calls `client.handleRedirect()` (or `<vimine-sign-in>` does it for you when
|
|
16
19
|
// `handle-redirect` is set on the element and the page is the redirect URI).
|
|
17
|
-
import { Vimine } from
|
|
20
|
+
import { Vimine } from "./index.js";
|
|
18
21
|
let shared = null;
|
|
19
22
|
const STRINGS = {
|
|
20
23
|
it: {
|
|
21
|
-
signIn:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
signIn: "Accedi con Vimine",
|
|
25
|
+
signOut: "Esci",
|
|
26
|
+
loading: "Caricamento…",
|
|
27
|
+
notSignedIn: "Accedi per vedere la tua tessera.",
|
|
28
|
+
noMembership: "Nessuna tessera in questa associazione.",
|
|
29
|
+
notActive: "La tessera non è ancora attiva: il codice compare quando lo diventa.",
|
|
30
|
+
error: "Si è verificato un errore.",
|
|
31
|
+
noEvents: "Nessun evento in programma.",
|
|
32
|
+
free: "gratuita",
|
|
33
|
+
places: "posti",
|
|
34
|
+
register: "Iscriviti",
|
|
35
|
+
signInToRegister: "Accedi per iscriverti",
|
|
36
|
+
joinAndBook: "Iscriviti all’associazione e prenota",
|
|
37
|
+
alreadyMember: "Sono già socio: accedi",
|
|
38
|
+
registered: "Iscrizione confermata",
|
|
39
|
+
reserved: "Posto riservato: in attesa del contributo",
|
|
40
|
+
details: "Dettagli",
|
|
41
|
+
reason: {
|
|
42
|
+
sold_out: "esaurito",
|
|
43
|
+
event_full: "al completo",
|
|
44
|
+
registration_not_open: "iscrizioni non ancora aperte",
|
|
45
|
+
registration_closed: "iscrizioni chiuse",
|
|
46
|
+
membership_required: "riservato ai soci",
|
|
47
|
+
membership_not_active: "tessera non valida",
|
|
48
|
+
membership_not_yet_valid: "la tessera non è ancora utilizzabile a quella data",
|
|
49
|
+
membership_type_not_allowed: "tipo di tessera non ammesso",
|
|
50
|
+
max_per_user: "già iscritto",
|
|
51
|
+
event_past: "evento concluso",
|
|
52
|
+
event_cancelled: "evento annullato",
|
|
53
|
+
event_not_published: "non pubblicato",
|
|
54
|
+
plan_limit: "non disponibile",
|
|
55
|
+
invalid_type: "tipo non valido",
|
|
56
|
+
},
|
|
24
57
|
},
|
|
25
58
|
en: {
|
|
26
|
-
signIn:
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
signIn: "Sign in with Vimine",
|
|
60
|
+
signOut: "Sign out",
|
|
61
|
+
loading: "Loading…",
|
|
62
|
+
notSignedIn: "Sign in to see your card.",
|
|
63
|
+
noMembership: "No membership in this association.",
|
|
64
|
+
notActive: "The card is not active yet: its code appears once it is.",
|
|
65
|
+
error: "Something went wrong.",
|
|
66
|
+
noEvents: "No upcoming events.",
|
|
67
|
+
free: "free",
|
|
68
|
+
places: "places",
|
|
69
|
+
register: "Register",
|
|
70
|
+
signInToRegister: "Sign in to register",
|
|
71
|
+
joinAndBook: "Join the association and book",
|
|
72
|
+
alreadyMember: "Already a member? Sign in",
|
|
73
|
+
registered: "Registration confirmed",
|
|
74
|
+
reserved: "Place reserved: awaiting the contribution",
|
|
75
|
+
details: "Details",
|
|
76
|
+
reason: {
|
|
77
|
+
sold_out: "sold out",
|
|
78
|
+
event_full: "full",
|
|
79
|
+
registration_not_open: "registrations not open yet",
|
|
80
|
+
registration_closed: "registrations closed",
|
|
81
|
+
membership_required: "members only",
|
|
82
|
+
membership_not_active: "membership not valid",
|
|
83
|
+
membership_not_yet_valid: "your card is not usable yet on that date",
|
|
84
|
+
membership_type_not_allowed: "membership type not admitted",
|
|
85
|
+
max_per_user: "already registered",
|
|
86
|
+
event_past: "event over",
|
|
87
|
+
event_cancelled: "event cancelled",
|
|
88
|
+
event_not_published: "not published",
|
|
89
|
+
plan_limit: "not available",
|
|
90
|
+
invalid_type: "invalid type",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
"zh-Hans": {
|
|
94
|
+
signIn: "使用 Vimine 登录",
|
|
95
|
+
signOut: "退出",
|
|
96
|
+
loading: "加载中…",
|
|
97
|
+
notSignedIn: "请登录以查看您的会员卡。",
|
|
98
|
+
noMembership: "您在这个协会没有会员卡。",
|
|
99
|
+
notActive: "会员卡尚未生效:生效后会显示二维码。",
|
|
100
|
+
error: "出现错误。",
|
|
101
|
+
noEvents: "暂无安排的活动。",
|
|
102
|
+
free: "免费",
|
|
103
|
+
places: "个名额",
|
|
104
|
+
register: "报名",
|
|
105
|
+
signInToRegister: "登录后报名",
|
|
106
|
+
joinAndBook: "加入协会并预约",
|
|
107
|
+
alreadyMember: "已是会员?请登录",
|
|
108
|
+
registered: "报名已确认",
|
|
109
|
+
reserved: "名额已保留:等待缴纳参与费",
|
|
110
|
+
details: "详情",
|
|
111
|
+
reason: {
|
|
112
|
+
sold_out: "名额已满",
|
|
113
|
+
event_full: "已满员",
|
|
114
|
+
registration_not_open: "报名尚未开始",
|
|
115
|
+
registration_closed: "报名已截止",
|
|
116
|
+
membership_required: "仅限会员",
|
|
117
|
+
membership_not_active: "会员卡无效",
|
|
118
|
+
membership_not_yet_valid: "在该日期您的会员卡尚不可用",
|
|
119
|
+
membership_type_not_allowed: "该会员卡类型不在允许范围内",
|
|
120
|
+
max_per_user: "已报名",
|
|
121
|
+
event_past: "活动已结束",
|
|
122
|
+
event_cancelled: "活动已取消",
|
|
123
|
+
event_not_published: "未发布",
|
|
124
|
+
plan_limit: "不可用",
|
|
125
|
+
invalid_type: "类型无效",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
"zh-Hant": {
|
|
129
|
+
signIn: "使用 Vimine 登入",
|
|
130
|
+
signOut: "退出",
|
|
131
|
+
loading: "載入中…",
|
|
132
|
+
notSignedIn: "請登入以檢視您的會員卡。",
|
|
133
|
+
noMembership: "您在這個協會沒有會員卡。",
|
|
134
|
+
notActive: "會員卡尚未生效:生效後會顯示二維碼。",
|
|
135
|
+
error: "出現錯誤。",
|
|
136
|
+
noEvents: "暫無安排的活動。",
|
|
137
|
+
free: "免費",
|
|
138
|
+
places: "個名額",
|
|
139
|
+
register: "報名",
|
|
140
|
+
signInToRegister: "登入後報名",
|
|
141
|
+
joinAndBook: "加入協會並預約",
|
|
142
|
+
alreadyMember: "已是會員?請登入",
|
|
143
|
+
registered: "報名已確認",
|
|
144
|
+
reserved: "名額已保留:等待繳納參與費",
|
|
145
|
+
details: "詳情",
|
|
146
|
+
reason: {
|
|
147
|
+
sold_out: "名額已滿",
|
|
148
|
+
event_full: "已滿員",
|
|
149
|
+
registration_not_open: "報名尚未開始",
|
|
150
|
+
registration_closed: "報名已截止",
|
|
151
|
+
membership_required: "僅限會員",
|
|
152
|
+
membership_not_active: "會員卡無效",
|
|
153
|
+
membership_not_yet_valid: "在該日期您的會員卡尚不可用",
|
|
154
|
+
membership_type_not_allowed: "該會員卡類型不在允許範圍內",
|
|
155
|
+
max_per_user: "已報名",
|
|
156
|
+
event_past: "活動已結束",
|
|
157
|
+
event_cancelled: "活動已取消",
|
|
158
|
+
event_not_published: "未釋出",
|
|
159
|
+
plan_limit: "不可用",
|
|
160
|
+
invalid_type: "類型無效",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
"fr": {
|
|
164
|
+
signIn: "Se connecter avec Vimine",
|
|
165
|
+
signOut: "Se déconnecter",
|
|
166
|
+
loading: "Chargement…",
|
|
167
|
+
notSignedIn: "Connectez-vous pour voir votre carte.",
|
|
168
|
+
noMembership: "Aucune carte dans cette association.",
|
|
169
|
+
notActive: "La carte n'est pas encore active : le code apparaîtra lorsqu'elle le sera.",
|
|
170
|
+
error: "Une erreur s'est produite.",
|
|
171
|
+
noEvents: "Aucun événement prévu.",
|
|
172
|
+
free: "gratuite",
|
|
173
|
+
places: "places",
|
|
174
|
+
register: "S'inscrire",
|
|
175
|
+
signInToRegister: "Se connecter pour s'inscrire",
|
|
176
|
+
joinAndBook: "Adhérer à l'association et réserver",
|
|
177
|
+
alreadyMember: "Déjà membre : se connecter",
|
|
178
|
+
registered: "Inscription confirmée",
|
|
179
|
+
reserved: "Place réservée : en attente de la contribution",
|
|
180
|
+
details: "Détails",
|
|
181
|
+
reason: {
|
|
182
|
+
sold_out: "plus de places",
|
|
183
|
+
event_full: "complet",
|
|
184
|
+
registration_not_open: "inscriptions pas encore ouvertes",
|
|
185
|
+
registration_closed: "inscriptions closes",
|
|
186
|
+
membership_required: "réservé aux membres",
|
|
187
|
+
membership_not_active: "adhésion non valable",
|
|
188
|
+
membership_not_yet_valid: "la carte n'est pas encore utilisable à cette date",
|
|
189
|
+
membership_type_not_allowed: "type d'adhésion non admis",
|
|
190
|
+
max_per_user: "déjà inscrit",
|
|
191
|
+
event_past: "événement terminé",
|
|
192
|
+
event_cancelled: "événement annulé",
|
|
193
|
+
event_not_published: "non publié",
|
|
194
|
+
plan_limit: "non disponible",
|
|
195
|
+
invalid_type: "type non valide",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
"es": {
|
|
199
|
+
signIn: "Iniciar sesión con Vimine",
|
|
200
|
+
signOut: "Cerrar sesión",
|
|
201
|
+
loading: "Cargando…",
|
|
202
|
+
notSignedIn: "Inicia sesión para ver tu carné.",
|
|
203
|
+
noMembership: "No tienes carné en esta asociación.",
|
|
204
|
+
notActive: "El carné aún no está activo: el código aparece cuando lo esté.",
|
|
205
|
+
error: "Se ha producido un error.",
|
|
206
|
+
noEvents: "No hay eventos programados.",
|
|
207
|
+
free: "gratuita",
|
|
208
|
+
places: "plazas",
|
|
209
|
+
register: "Inscríbete",
|
|
210
|
+
signInToRegister: "Inicia sesión para inscribirte",
|
|
211
|
+
joinAndBook: "Hazte socio y reserva",
|
|
212
|
+
alreadyMember: "¿Ya eres socio? Inicia sesión",
|
|
213
|
+
registered: "Inscripción confirmada",
|
|
214
|
+
reserved: "Plaza reservada: pendiente de la contribución",
|
|
215
|
+
details: "Detalles",
|
|
216
|
+
reason: {
|
|
217
|
+
sold_out: "agotado",
|
|
218
|
+
event_full: "completo",
|
|
219
|
+
registration_not_open: "inscripciones aún no abiertas",
|
|
220
|
+
registration_closed: "inscripciones cerradas",
|
|
221
|
+
membership_required: "solo para socios",
|
|
222
|
+
membership_not_active: "carné no válido",
|
|
223
|
+
membership_not_yet_valid: "el carné aún no se puede usar en esa fecha",
|
|
224
|
+
membership_type_not_allowed: "tipo de carné no admitido",
|
|
225
|
+
max_per_user: "ya inscrito",
|
|
226
|
+
event_past: "evento finalizado",
|
|
227
|
+
event_cancelled: "evento cancelado",
|
|
228
|
+
event_not_published: "no publicado",
|
|
229
|
+
plan_limit: "no disponible",
|
|
230
|
+
invalid_type: "tipo no válido",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
"de": {
|
|
234
|
+
signIn: "Mit Vimine anmelden",
|
|
235
|
+
signOut: "Abmelden",
|
|
236
|
+
loading: "Wird geladen…",
|
|
237
|
+
notSignedIn: "Melden Sie sich an, um Ihren Ausweis zu sehen.",
|
|
238
|
+
noMembership: "Keine Mitgliedschaft in diesem Verein.",
|
|
239
|
+
notActive: "Der Ausweis ist noch nicht aktiv: Der Code erscheint, sobald er es ist.",
|
|
240
|
+
error: "Ein Fehler ist aufgetreten.",
|
|
241
|
+
noEvents: "Keine Veranstaltungen geplant.",
|
|
242
|
+
free: "kostenlos",
|
|
243
|
+
places: "Plätze",
|
|
244
|
+
register: "Teilnehmen",
|
|
245
|
+
signInToRegister: "Zum Teilnehmen anmelden",
|
|
246
|
+
joinAndBook: "Dem Verein beitreten und Platz reservieren",
|
|
247
|
+
alreadyMember: "Schon Mitglied? Anmelden",
|
|
248
|
+
registered: "Anmeldung bestätigt",
|
|
249
|
+
reserved: "Platz reserviert: Beitrag ausstehend",
|
|
250
|
+
details: "Details",
|
|
251
|
+
reason: {
|
|
252
|
+
sold_out: "ausgebucht",
|
|
253
|
+
event_full: "voll",
|
|
254
|
+
registration_not_open: "Anmeldung noch nicht offen",
|
|
255
|
+
registration_closed: "Anmeldung geschlossen",
|
|
256
|
+
membership_required: "nur für Mitglieder",
|
|
257
|
+
membership_not_active: "Mitgliedschaft nicht gültig",
|
|
258
|
+
membership_not_yet_valid: "Ihr Ausweis ist an diesem Datum noch nicht nutzbar",
|
|
259
|
+
membership_type_not_allowed: "Mitgliedsart nicht zugelassen",
|
|
260
|
+
max_per_user: "bereits angemeldet",
|
|
261
|
+
event_past: "Veranstaltung vorbei",
|
|
262
|
+
event_cancelled: "Veranstaltung abgesagt",
|
|
263
|
+
event_not_published: "nicht veröffentlicht",
|
|
264
|
+
plan_limit: "nicht verfügbar",
|
|
265
|
+
invalid_type: "ungültige Art",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
"pt-BR": {
|
|
269
|
+
signIn: "Entrar com Vimine",
|
|
270
|
+
signOut: "Sair",
|
|
271
|
+
loading: "Carregando…",
|
|
272
|
+
notSignedIn: "Entre para ver sua carteirinha.",
|
|
273
|
+
noMembership: "Nenhuma carteirinha nesta associação.",
|
|
274
|
+
notActive: "A carteirinha ainda não está ativa: o código aparece quando ela for ativada.",
|
|
275
|
+
error: "Ocorreu um erro.",
|
|
276
|
+
noEvents: "Nenhum evento programado.",
|
|
277
|
+
free: "gratuita",
|
|
278
|
+
places: "vagas",
|
|
279
|
+
register: "Inscrever-se",
|
|
280
|
+
signInToRegister: "Entre para se inscrever",
|
|
281
|
+
joinAndBook: "Associe-se e reserve",
|
|
282
|
+
alreadyMember: "Já sou associado: entrar",
|
|
283
|
+
registered: "Inscrição confirmada",
|
|
284
|
+
reserved: "Vaga reservada: aguardando a contribuição",
|
|
285
|
+
details: "Detalhes",
|
|
286
|
+
reason: {
|
|
287
|
+
sold_out: "esgotado",
|
|
288
|
+
event_full: "lotado",
|
|
289
|
+
registration_not_open: "inscrições ainda não abertas",
|
|
290
|
+
registration_closed: "inscrições encerradas",
|
|
291
|
+
membership_required: "exclusivo para associados",
|
|
292
|
+
membership_not_active: "carteirinha inválida",
|
|
293
|
+
membership_not_yet_valid: "a carteirinha ainda não pode ser usada nessa data",
|
|
294
|
+
membership_type_not_allowed: "tipo de carteirinha não admitido",
|
|
295
|
+
max_per_user: "já inscrito",
|
|
296
|
+
event_past: "evento encerrado",
|
|
297
|
+
event_cancelled: "evento cancelado",
|
|
298
|
+
event_not_published: "não publicado",
|
|
299
|
+
plan_limit: "indisponível",
|
|
300
|
+
invalid_type: "tipo inválido",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
// Languages drafted and not yet read by a native speaker — as on the platform, which serves a draft only where its preview
|
|
305
|
+
// switch names it. Their strings ship, but an element speaks one only on a page that opted in
|
|
306
|
+
// (`defineVimineElements(options, { previewLanguages: true })`); elsewhere it speaks Italian, as the platform would.
|
|
307
|
+
// A language leaves this list with the platform, in an SDK release. Empty since 2026-10-20: the platform enabled its six
|
|
308
|
+
// drafted languages ahead of their review (the owner's decision), and the elements speak them with it.
|
|
309
|
+
const DRAFT_LANGS = [];
|
|
310
|
+
let previewLanguages = false;
|
|
311
|
+
// The page's language → the elements' own: the tag itself, Chinese by script (zh-TW, zh-HK → Traditional; zh, zh-CN →
|
|
312
|
+
// Simplified), any other variant to the language's (en-GB → en, pt-PT → pt-BR); Italian otherwise. The platform's rule.
|
|
313
|
+
const LANGS = Object.keys(STRINGS);
|
|
314
|
+
/** The elements' language for a page's tag, or null when they do not speak it (`preview`: drafts included). */
|
|
315
|
+
export const matchLang = (tag, preview = previewLanguages) => {
|
|
316
|
+
const served = LANGS.filter((l) => preview || !DRAFT_LANGS.includes(l));
|
|
317
|
+
const lower = tag.trim().replace(/_/g, "-").toLowerCase();
|
|
318
|
+
const exact = served.find((l) => l.toLowerCase() === lower);
|
|
319
|
+
if (exact)
|
|
320
|
+
return exact;
|
|
321
|
+
const [lang, ...rest] = lower.split("-");
|
|
322
|
+
if (lang === "zh") {
|
|
323
|
+
const script = rest.some((p) => ["hant", "tw", "hk", "mo"].includes(p)) ? "zh-Hant" : "zh-Hans";
|
|
324
|
+
return served.includes(script) ? script : null;
|
|
29
325
|
}
|
|
326
|
+
return served.find((l) => l.toLowerCase().split("-")[0] === lang) || null;
|
|
30
327
|
};
|
|
31
|
-
const langOf = (el) =>
|
|
32
|
-
|
|
33
|
-
|
|
328
|
+
const langOf = (el) => matchLang(el.getAttribute("lang") ||
|
|
329
|
+
el.closest("[lang]")?.getAttribute("lang") ||
|
|
330
|
+
document.documentElement.lang ||
|
|
331
|
+
"it") || "it";
|
|
332
|
+
// Dates and amounts in the reader's conventions: English with day-month order, Chinese naming its script.
|
|
333
|
+
const INTL = {
|
|
334
|
+
it: "it-IT",
|
|
335
|
+
en: "en-GB",
|
|
336
|
+
"zh-Hans": "zh-Hans-CN",
|
|
337
|
+
"zh-Hant": "zh-Hant-TW",
|
|
338
|
+
fr: "fr-FR",
|
|
339
|
+
es: "es-ES",
|
|
340
|
+
de: "de-DE",
|
|
341
|
+
"pt-BR": "pt-BR",
|
|
34
342
|
};
|
|
35
|
-
const BASE_CSS = `
|
|
343
|
+
const BASE_CSS = /* CSS */ `
|
|
36
344
|
:host { display: inline-block; font-family: var(--vimine-font, system-ui, sans-serif); color: var(--vimine-color, #111); }
|
|
37
345
|
button { cursor: pointer; border: 0; border-radius: var(--vimine-radius, 999px); padding: var(--vimine-padding, 0.65rem 1.1rem);
|
|
38
346
|
background: var(--vimine-accent, #0a0e1a); color: var(--vimine-accent-contrast, #fff); font: inherit; font-weight: 600; }
|
|
@@ -41,28 +349,29 @@ button:disabled { opacity: .6; cursor: default; }
|
|
|
41
349
|
.error { color: var(--vimine-error, #a33); font-size: .9em; }
|
|
42
350
|
`;
|
|
43
351
|
const shadow = (el, css) => {
|
|
44
|
-
const root = el.shadowRoot || el.attachShadow({ mode:
|
|
352
|
+
const root = el.shadowRoot || el.attachShadow({ mode: "open" });
|
|
45
353
|
root.innerHTML = `<style>${BASE_CSS}${css}</style>`;
|
|
46
354
|
return root;
|
|
47
355
|
};
|
|
48
356
|
/** The client the elements use. Call once; pass your own instance to share it with the rest of the page. */
|
|
49
|
-
export function defineVimineElements(options) {
|
|
357
|
+
export function defineVimineElements(options, elements = {}) {
|
|
50
358
|
shared = options instanceof Vimine ? options : new Vimine(options);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
359
|
+
previewLanguages = !!elements.previewLanguages;
|
|
360
|
+
if (typeof customElements !== "undefined") {
|
|
361
|
+
if (!customElements.get("vimine-sign-in"))
|
|
362
|
+
customElements.define("vimine-sign-in", VimineSignIn);
|
|
363
|
+
if (!customElements.get("vimine-card"))
|
|
364
|
+
customElements.define("vimine-card", VimineCard);
|
|
365
|
+
if (!customElements.get("vimine-enroll"))
|
|
366
|
+
customElements.define("vimine-enroll", VimineEnroll);
|
|
367
|
+
if (!customElements.get("vimine-events"))
|
|
368
|
+
customElements.define("vimine-events", VimineEvents);
|
|
60
369
|
}
|
|
61
370
|
return shared;
|
|
62
371
|
}
|
|
63
372
|
const client = () => {
|
|
64
373
|
if (!shared)
|
|
65
|
-
throw new Error(
|
|
374
|
+
throw new Error("Vimine elements: call defineVimineElements({ clientId, redirectUri }) first");
|
|
66
375
|
return shared;
|
|
67
376
|
};
|
|
68
377
|
/** `<vimine-sign-in scope="…" return-to="/area" label="…" handle-redirect>` — a sign-in / sign-out button. */
|
|
@@ -77,7 +386,7 @@ export class VimineSignIn extends HTMLElement {
|
|
|
77
386
|
}
|
|
78
387
|
async boot() {
|
|
79
388
|
const mp = client();
|
|
80
|
-
if (this.hasAttribute(
|
|
389
|
+
if (this.hasAttribute("handle-redirect")) {
|
|
81
390
|
try {
|
|
82
391
|
await mp.handleRedirect();
|
|
83
392
|
}
|
|
@@ -92,28 +401,38 @@ export class VimineSignIn extends HTMLElement {
|
|
|
92
401
|
render(error) {
|
|
93
402
|
const mp = client();
|
|
94
403
|
const t = STRINGS[langOf(this)];
|
|
95
|
-
const root = shadow(this,
|
|
96
|
-
const btn = document.createElement(
|
|
97
|
-
btn.type =
|
|
404
|
+
const root = shadow(this, "");
|
|
405
|
+
const btn = document.createElement("button");
|
|
406
|
+
btn.type = "button";
|
|
98
407
|
if (mp.isSignedIn()) {
|
|
99
|
-
btn.textContent = this.getAttribute(
|
|
100
|
-
btn.addEventListener(
|
|
408
|
+
btn.textContent = this.getAttribute("label-signed-in") || t.signOut;
|
|
409
|
+
btn.addEventListener("click", () => void mp.signOut({ everywhere: this.hasAttribute("everywhere") }));
|
|
101
410
|
}
|
|
102
411
|
else {
|
|
103
|
-
btn.textContent = this.getAttribute(
|
|
104
|
-
btn.addEventListener(
|
|
105
|
-
const scopes = (this.getAttribute(
|
|
106
|
-
|
|
412
|
+
btn.textContent = this.getAttribute("label") || t.signIn;
|
|
413
|
+
btn.addEventListener("click", () => {
|
|
414
|
+
const scopes = (this.getAttribute("scope") || "")
|
|
415
|
+
.split(/[\s,]+/)
|
|
416
|
+
.filter(Boolean);
|
|
417
|
+
void mp.signIn({
|
|
418
|
+
...(scopes.length ? { scopes } : {}),
|
|
419
|
+
...(this.getAttribute("return-to")
|
|
420
|
+
? { returnTo: this.getAttribute("return-to") }
|
|
421
|
+
: {}),
|
|
422
|
+
});
|
|
107
423
|
});
|
|
108
424
|
}
|
|
109
425
|
root.appendChild(btn);
|
|
110
426
|
if (error) {
|
|
111
|
-
const e = document.createElement(
|
|
112
|
-
e.className =
|
|
427
|
+
const e = document.createElement("div");
|
|
428
|
+
e.className = "error";
|
|
113
429
|
e.textContent = error;
|
|
114
430
|
root.appendChild(e);
|
|
115
431
|
}
|
|
116
|
-
this.dispatchEvent(new CustomEvent(
|
|
432
|
+
this.dispatchEvent(new CustomEvent("vimine-change", {
|
|
433
|
+
detail: { signedIn: mp.isSignedIn() },
|
|
434
|
+
bubbles: true,
|
|
435
|
+
}));
|
|
117
436
|
}
|
|
118
437
|
}
|
|
119
438
|
/** `<vimine-card>` — the signed-in member's card as SVG (with the live QR); refreshes when the session changes. */
|
|
@@ -130,55 +449,168 @@ export class VimineCard extends HTMLElement {
|
|
|
130
449
|
async render() {
|
|
131
450
|
const mp = client();
|
|
132
451
|
const t = STRINGS[langOf(this)];
|
|
133
|
-
const root = shadow(this,
|
|
452
|
+
const root = shadow(this, ":host { display: block; max-width: var(--vimine-card-width, 600px); } svg { width: 100%; height: auto; display: block; border-radius: 24px; box-shadow: var(--vimine-card-shadow, 0 8px 30px rgba(0,0,0,.12)); }");
|
|
134
453
|
const note = (cls, text) => {
|
|
135
|
-
const d = document.createElement(
|
|
454
|
+
const d = document.createElement("div");
|
|
136
455
|
d.className = cls;
|
|
137
456
|
d.textContent = text;
|
|
138
457
|
root.appendChild(d);
|
|
139
458
|
};
|
|
140
459
|
if (!mp.isSignedIn())
|
|
141
|
-
return note(
|
|
142
|
-
note(
|
|
460
|
+
return note("muted", t.notSignedIn);
|
|
461
|
+
note("muted", t.loading);
|
|
143
462
|
try {
|
|
144
463
|
const svg = await mp.membership.cardSvg();
|
|
145
464
|
root.innerHTML = `<style>${BASE_CSS}:host { display: block; max-width: var(--vimine-card-width, 600px); } svg { width: 100%; height: auto; display: block; border-radius: 24px; box-shadow: var(--vimine-card-shadow, 0 8px 30px rgba(0,0,0,.12)); }</style>${svg}`;
|
|
146
|
-
this.dispatchEvent(new CustomEvent(
|
|
465
|
+
this.dispatchEvent(new CustomEvent("vimine-card", {
|
|
466
|
+
detail: { loaded: true },
|
|
467
|
+
bubbles: true,
|
|
468
|
+
}));
|
|
147
469
|
}
|
|
148
470
|
catch (err) {
|
|
149
471
|
root.innerHTML = `<style>${BASE_CSS}</style>`;
|
|
150
472
|
const code = err?.code;
|
|
151
473
|
// `not_active`: the membership exists but has no card (pending, suspended, revoked) — a state, not a failure.
|
|
152
|
-
const quiet = code ===
|
|
153
|
-
note(quiet ?
|
|
474
|
+
const quiet = code === "not_found" || code === "not_active";
|
|
475
|
+
note(quiet ? "muted" : "error", code === "not_found"
|
|
476
|
+
? t.noMembership
|
|
477
|
+
: code === "not_active"
|
|
478
|
+
? t.notActive
|
|
479
|
+
: t.error);
|
|
154
480
|
}
|
|
155
481
|
}
|
|
156
482
|
}
|
|
157
|
-
/**
|
|
483
|
+
/** The faces the hosted page offers — served by Vimine itself — or `system`, the device's own. They set everything. */
|
|
484
|
+
export const TYPEFACES = [
|
|
485
|
+
"system",
|
|
486
|
+
"inter",
|
|
487
|
+
"roboto",
|
|
488
|
+
"open-sans",
|
|
489
|
+
"montserrat",
|
|
490
|
+
"nunito",
|
|
491
|
+
"lora",
|
|
492
|
+
"merriweather",
|
|
493
|
+
"roboto-slab",
|
|
494
|
+
];
|
|
495
|
+
const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
496
|
+
const appearanceQuery = (a) => {
|
|
497
|
+
if (!a)
|
|
498
|
+
return "";
|
|
499
|
+
const out = [];
|
|
500
|
+
if (a.scheme === "light" || a.scheme === "dark")
|
|
501
|
+
out.push(`scheme=${a.scheme}`);
|
|
502
|
+
if (a.accent && HEX.test(a.accent))
|
|
503
|
+
out.push(`accent=${encodeURIComponent(a.accent.replace(/^#/, ""))}`);
|
|
504
|
+
if (a.background === "transparent")
|
|
505
|
+
out.push("background=transparent");
|
|
506
|
+
else if (a.background && HEX.test(a.background))
|
|
507
|
+
out.push(`background=${encodeURIComponent(a.background.replace(/^#/, ""))}`);
|
|
508
|
+
if (a.font && TYPEFACES.includes(a.font))
|
|
509
|
+
out.push(`font=${a.font}`);
|
|
510
|
+
return out.length ? `&${out.join("&")}` : "";
|
|
511
|
+
};
|
|
512
|
+
export const enrollUrl = (appUrl, slug, lang, appearance) => `${appUrl.replace(/\/+$/, "")}/enroll/${encodeURIComponent(slug)}?locale=${encodeURIComponent(lang)}${appearanceQuery(appearance)}`;
|
|
513
|
+
/**
|
|
514
|
+
* The same page, opened FROM an event: the person joins the association and asks for a place at the event in one go.
|
|
515
|
+
* The request carries the event; the place is confirmed, or held until the membership is active.
|
|
516
|
+
*/
|
|
517
|
+
export const eventEnrollUrl = (appUrl, slug, event, lang) => `${enrollUrl(appUrl, slug, lang)}&event=${encodeURIComponent(event)}`;
|
|
518
|
+
/** Any CSS colour (a name, `rgb()`, `hsl()`, a hex) as `#rrggbb`, resolved by the browser; null when it is none. */
|
|
519
|
+
const hexColour = (root, value) => {
|
|
520
|
+
const v = value.trim();
|
|
521
|
+
if (!v)
|
|
522
|
+
return null;
|
|
523
|
+
const probe = document.createElement("span");
|
|
524
|
+
probe.style.color = v;
|
|
525
|
+
if (!probe.style.color)
|
|
526
|
+
return null;
|
|
527
|
+
root.appendChild(probe);
|
|
528
|
+
const m = /rgba?\(([^)]+)\)/.exec(getComputedStyle(probe).color);
|
|
529
|
+
probe.remove();
|
|
530
|
+
const parts = (m?.[1] ?? "").split(/[\s,/]+/).filter(Boolean).map(Number);
|
|
531
|
+
if (parts.length < 3)
|
|
532
|
+
return null;
|
|
533
|
+
const [r = 0, g = 0, b = 0, alpha] = parts;
|
|
534
|
+
if (alpha === 0)
|
|
535
|
+
return null;
|
|
536
|
+
return `#${[r, g, b].map((n) => Math.round(n).toString(16).padStart(2, "0")).join("")}`;
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* The page's look from the host's CSS: `--vimine-scheme` (light | dark), `--vimine-accent` and `--vimine-background` (a
|
|
540
|
+
* colour, or `transparent`), `--vimine-typeface` (one of TYPEFACES) — custom properties inherit, so one rule on `:root`
|
|
541
|
+
* styles every Vimine element at once.
|
|
542
|
+
*/
|
|
543
|
+
const appearanceOf = (el, root) => {
|
|
544
|
+
const css = getComputedStyle(el);
|
|
545
|
+
const read = (name) => css.getPropertyValue(name).trim();
|
|
546
|
+
const out = {};
|
|
547
|
+
const scheme = read("--vimine-scheme");
|
|
548
|
+
if (scheme === "light" || scheme === "dark")
|
|
549
|
+
out.scheme = scheme;
|
|
550
|
+
const accent = hexColour(root, read("--vimine-accent"));
|
|
551
|
+
if (accent)
|
|
552
|
+
out.accent = accent;
|
|
553
|
+
const background = read("--vimine-background");
|
|
554
|
+
if (background === "transparent")
|
|
555
|
+
out.background = "transparent";
|
|
556
|
+
else {
|
|
557
|
+
const bg = hexColour(root, background);
|
|
558
|
+
if (bg)
|
|
559
|
+
out.background = bg;
|
|
560
|
+
}
|
|
561
|
+
const typeface = read("--vimine-typeface");
|
|
562
|
+
if (TYPEFACES.includes(typeface))
|
|
563
|
+
out.font = typeface;
|
|
564
|
+
return out;
|
|
565
|
+
};
|
|
566
|
+
/**
|
|
567
|
+
* `<vimine-enroll slug="…" app-url="https://app.vimine.network" height="900" lang="en">` — the hosted enrolment form.
|
|
568
|
+
*
|
|
569
|
+
* Styled like the other elements, through CSS custom properties on the element or any ancestor:
|
|
570
|
+
*
|
|
571
|
+
* vimine-enroll { --vimine-scheme: light; --vimine-accent: #1e3a5f; --vimine-background: transparent; }
|
|
572
|
+
*
|
|
573
|
+
* `--vimine-scheme` the page's scheme; `--vimine-accent` its buttons, links and the association's name (moved towards
|
|
574
|
+
* the page's ink when it would not read on its cards); `--vimine-background` the ground behind the cards;
|
|
575
|
+
* `--vimine-typeface` the face of the whole page, fields included — one of TYPEFACES, served by Vimine (a framed page
|
|
576
|
+
* cannot use the host's fonts). The frame is styled once, when the element connects.
|
|
577
|
+
*
|
|
578
|
+
* The form opens in the element's language (`lang` on the element or the page; Italian by default). Someone who already
|
|
579
|
+
* has a Vimine account need not fill it: the form offers *Continua su Vimine*, which opens the hosted page in a new
|
|
580
|
+
* tab — there their session is recognised and they join in one step (the association's statuto and privacy notice, and
|
|
581
|
+
* nothing they have already told Vimine). It is a hand-off and not a sign-in inside the frame because a browser keeps a
|
|
582
|
+
* framed page's session apart from the same site opened directly; the frame's sandbox lets that tab open as an ordinary
|
|
583
|
+
* page (`allow-popups-to-escape-sandbox`).
|
|
584
|
+
*/
|
|
158
585
|
export class VimineEnroll extends HTMLElement {
|
|
159
586
|
connectedCallback() {
|
|
160
|
-
const slug = this.getAttribute(
|
|
161
|
-
const root = shadow(this,
|
|
587
|
+
const slug = this.getAttribute("slug");
|
|
588
|
+
const root = shadow(this, ":host { display: block; } iframe { width: 100%; border: 0; border-radius: var(--vimine-radius, 16px); background: transparent; }");
|
|
162
589
|
if (!slug) {
|
|
163
|
-
const e = document.createElement(
|
|
164
|
-
e.className =
|
|
165
|
-
e.textContent =
|
|
590
|
+
const e = document.createElement("div");
|
|
591
|
+
e.className = "error";
|
|
592
|
+
e.textContent = "vimine-enroll: missing slug";
|
|
166
593
|
root.appendChild(e);
|
|
167
594
|
return;
|
|
168
595
|
}
|
|
169
|
-
const appUrl = (this.getAttribute(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
frame
|
|
173
|
-
frame.
|
|
174
|
-
frame.
|
|
175
|
-
frame.
|
|
596
|
+
const appUrl = (this.getAttribute("app-url") ||
|
|
597
|
+
client().options.appUrl ||
|
|
598
|
+
"https://app.vimine.network").replace(/\/+$/, "");
|
|
599
|
+
const frame = document.createElement("iframe");
|
|
600
|
+
frame.src = enrollUrl(appUrl, slug, langOf(this), appearanceOf(this, root));
|
|
601
|
+
frame.title = "Vimine";
|
|
602
|
+
frame.loading = "lazy";
|
|
603
|
+
frame.style.height = `${Number(this.getAttribute("height")) || 900}px`;
|
|
604
|
+
frame.setAttribute("sandbox", "allow-forms allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox");
|
|
176
605
|
root.appendChild(frame);
|
|
177
606
|
}
|
|
178
607
|
}
|
|
179
608
|
const money = (amount, currency, lang) => {
|
|
180
609
|
try {
|
|
181
|
-
return new Intl.NumberFormat(lang
|
|
610
|
+
return new Intl.NumberFormat(INTL[lang], {
|
|
611
|
+
style: "currency",
|
|
612
|
+
currency: currency || "EUR",
|
|
613
|
+
}).format(amount);
|
|
182
614
|
}
|
|
183
615
|
catch {
|
|
184
616
|
return `${amount.toFixed(2)} ${currency}`;
|
|
@@ -186,7 +618,11 @@ const money = (amount, currency, lang) => {
|
|
|
186
618
|
};
|
|
187
619
|
const when = (iso, tz, lang) => {
|
|
188
620
|
try {
|
|
189
|
-
return new Date(iso).toLocaleString(lang
|
|
621
|
+
return new Date(iso).toLocaleString(INTL[lang], {
|
|
622
|
+
timeZone: tz,
|
|
623
|
+
dateStyle: "full",
|
|
624
|
+
timeStyle: "short",
|
|
625
|
+
});
|
|
190
626
|
}
|
|
191
627
|
catch {
|
|
192
628
|
return new Date(iso).toLocaleString();
|
|
@@ -195,8 +631,13 @@ const when = (iso, tz, lang) => {
|
|
|
195
631
|
/**
|
|
196
632
|
* `<vimine-events slug="…" filter="upcoming" limit="10" detail-url="/eventi/{slug}">` — the association's events:
|
|
197
633
|
* the public ones for a visitor (no sign-in), every published one for a signed-in member, each with its registration
|
|
198
|
-
* types (contribution, places left) and a register button (`registrations:write`)
|
|
199
|
-
* `
|
|
634
|
+
* types (contribution, places left) and a register button (`registrations:write`). Fires `vimine-registration`
|
|
635
|
+
* `{ registration }` after a registration. `detail-url` (with `{slug}`) adds a details link.
|
|
636
|
+
*
|
|
637
|
+
* A VISITOR is offered a way in, not a bare sign-in: *Iscriviti all’associazione e prenota* opens the association's
|
|
638
|
+
* hosted enrolment page with the event (a new tab; `app-url` as on `<vimine-enroll>`) — they join and ask for the place
|
|
639
|
+
* in one go — and *Sono già socio: accedi* signs a member in here. The first is shown while the event has a registration
|
|
640
|
+
* that can be had and the association takes enrolment requests; otherwise the sign-in alone.
|
|
200
641
|
*/
|
|
201
642
|
export class VimineEvents extends HTMLElement {
|
|
202
643
|
unsubscribe = null;
|
|
@@ -213,117 +654,180 @@ export class VimineEvents extends HTMLElement {
|
|
|
213
654
|
const mp = client();
|
|
214
655
|
const lang = langOf(this);
|
|
215
656
|
const t = STRINGS[lang];
|
|
216
|
-
const root = shadow(this,
|
|
657
|
+
const root = shadow(this,
|
|
658
|
+
/* CSS */ `:host { display: block; }
|
|
217
659
|
.event { border: 1px solid var(--vimine-border, #e5e5e5); border-radius: var(--vimine-radius, 16px); padding: var(--vimine-padding, 1rem); margin: 0 0 1rem; }
|
|
218
660
|
.event h3 { margin: 0 0 .25rem; font-size: 1.15em; } .event img { width: 100%; border-radius: var(--vimine-radius, 16px); margin-bottom: .75rem; display: block; }
|
|
219
661
|
.type { display: flex; justify-content: space-between; align-items: center; gap: .75rem; padding: .5rem 0; border-top: 1px solid var(--vimine-border, #eee); }
|
|
220
|
-
.type .n { font-weight: 600; } .type button { padding: .45rem .9rem; } a { color: var(--vimine-accent, #0a0e1a); }
|
|
662
|
+
.type .n { font-weight: 600; } .type button { padding: .45rem .9rem; } a { color: var(--vimine-accent, #0a0e1a); }
|
|
663
|
+
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1rem; padding-top: .75rem; border-top: 1px solid var(--vimine-border, #eee); }
|
|
664
|
+
a.button { display: inline-block; text-decoration: none; border-radius: var(--vimine-radius, 999px); padding: var(--vimine-padding, 0.65rem 1.1rem);
|
|
665
|
+
background: var(--vimine-accent, #0a0e1a); color: var(--vimine-accent-contrast, #fff); font-weight: 600; }
|
|
666
|
+
button.link { background: none; color: var(--vimine-accent, #0a0e1a); padding: 0; border-radius: 0; text-decoration: underline; font-weight: 400; }`);
|
|
221
667
|
const note = (cls, text) => {
|
|
222
|
-
const d = document.createElement(
|
|
668
|
+
const d = document.createElement("div");
|
|
223
669
|
d.className = cls;
|
|
224
670
|
d.textContent = text;
|
|
225
671
|
root.appendChild(d);
|
|
226
672
|
};
|
|
227
|
-
note(
|
|
228
|
-
const slug = this.getAttribute(
|
|
229
|
-
const filter = (this.getAttribute(
|
|
230
|
-
const limit = Number(this.getAttribute(
|
|
673
|
+
note("muted", t.loading);
|
|
674
|
+
const slug = this.getAttribute("slug") || "";
|
|
675
|
+
const filter = (this.getAttribute("filter") || "upcoming");
|
|
676
|
+
const limit = Number(this.getAttribute("limit")) || undefined;
|
|
231
677
|
let items;
|
|
678
|
+
// For a visitor: does the association take enrolment requests? Only then is joining a way to a place. A side
|
|
679
|
+
// read — a failure leaves the sign-in alone.
|
|
680
|
+
let joinable = false;
|
|
232
681
|
try {
|
|
233
682
|
const params = { filter, ...(limit ? { limit } : {}) };
|
|
234
|
-
items = mp.isSignedIn()
|
|
683
|
+
items = mp.isSignedIn()
|
|
684
|
+
? (await mp.events.list(params)).items
|
|
685
|
+
: (await mp.public.events.list(slug, params)).items;
|
|
686
|
+
if (!mp.isSignedIn() && slug)
|
|
687
|
+
joinable = await mp.public
|
|
688
|
+
.association(slug)
|
|
689
|
+
.then((config) => config.aps.accepting_requests === true)
|
|
690
|
+
.catch(() => false);
|
|
235
691
|
}
|
|
236
692
|
catch {
|
|
237
693
|
root.innerHTML = `<style>${BASE_CSS}</style>`;
|
|
238
|
-
return note(
|
|
694
|
+
return note("error", t.error);
|
|
239
695
|
}
|
|
240
|
-
root.innerHTML = root.querySelector(
|
|
696
|
+
root.innerHTML = root.querySelector("style").outerHTML;
|
|
241
697
|
if (!items.length)
|
|
242
|
-
return note(
|
|
243
|
-
const detailUrl = this.getAttribute(
|
|
698
|
+
return note("muted", t.noEvents);
|
|
699
|
+
const detailUrl = this.getAttribute("detail-url");
|
|
700
|
+
const join = joinable
|
|
701
|
+
? {
|
|
702
|
+
slug,
|
|
703
|
+
appUrl: (this.getAttribute("app-url") ||
|
|
704
|
+
mp.options.appUrl ||
|
|
705
|
+
"https://app.vimine.network").replace(/\/+$/, ""),
|
|
706
|
+
}
|
|
707
|
+
: null;
|
|
244
708
|
for (const ev of items)
|
|
245
|
-
root.appendChild(this.card(ev, mp, lang, detailUrl));
|
|
709
|
+
root.appendChild(this.card(ev, mp, lang, detailUrl, join));
|
|
246
710
|
}
|
|
247
|
-
card(ev, mp, lang, detailUrl) {
|
|
711
|
+
card(ev, mp, lang, detailUrl, join) {
|
|
248
712
|
const t = STRINGS[lang];
|
|
249
|
-
const el = document.createElement(
|
|
250
|
-
el.className =
|
|
713
|
+
const el = document.createElement("article");
|
|
714
|
+
el.className = "event";
|
|
251
715
|
if (ev.cover_url) {
|
|
252
|
-
const img = document.createElement(
|
|
716
|
+
const img = document.createElement("img");
|
|
253
717
|
img.src = ev.cover_url;
|
|
254
|
-
img.alt =
|
|
718
|
+
img.alt = "";
|
|
255
719
|
el.appendChild(img);
|
|
256
720
|
}
|
|
257
|
-
const h = document.createElement(
|
|
721
|
+
const h = document.createElement("h3");
|
|
258
722
|
h.textContent = ev.title;
|
|
259
723
|
el.appendChild(h);
|
|
260
|
-
const meta = document.createElement(
|
|
261
|
-
meta.className =
|
|
262
|
-
meta.textContent = `${when(ev.starts_at, ev.timezone, lang)}${ev.location ? ` · ${ev.location.name || ev.location.address ||
|
|
724
|
+
const meta = document.createElement("div");
|
|
725
|
+
meta.className = "muted";
|
|
726
|
+
meta.textContent = `${when(ev.starts_at, ev.timezone, lang)}${ev.location ? ` · ${ev.location.name || ev.location.address || ""}` : ""}`;
|
|
263
727
|
el.appendChild(meta);
|
|
264
728
|
if (ev.summary) {
|
|
265
|
-
const p = document.createElement(
|
|
729
|
+
const p = document.createElement("p");
|
|
266
730
|
p.textContent = ev.summary;
|
|
267
731
|
el.appendChild(p);
|
|
268
732
|
}
|
|
269
733
|
if (detailUrl) {
|
|
270
|
-
const a = document.createElement(
|
|
271
|
-
a.href = detailUrl.replace(
|
|
734
|
+
const a = document.createElement("a");
|
|
735
|
+
a.href = detailUrl.replace("{slug}", encodeURIComponent(ev.slug));
|
|
272
736
|
a.textContent = t.details;
|
|
273
737
|
el.appendChild(a);
|
|
274
738
|
}
|
|
275
739
|
const mine = this.state[ev.id];
|
|
276
740
|
if (mine) {
|
|
277
|
-
const d = document.createElement(
|
|
278
|
-
d.className =
|
|
279
|
-
d.textContent = mine.status ===
|
|
741
|
+
const d = document.createElement("div");
|
|
742
|
+
d.className = "muted";
|
|
743
|
+
d.textContent = mine.status === "confirmed" ? t.registered : t.reserved;
|
|
280
744
|
el.appendChild(d);
|
|
281
745
|
return el;
|
|
282
746
|
}
|
|
283
|
-
if (ev.status ===
|
|
747
|
+
if (ev.status === "published")
|
|
284
748
|
for (const tp of ev.registration_types)
|
|
285
749
|
el.appendChild(this.typeRow(ev, tp, mp, lang));
|
|
750
|
+
// A visitor: one call for the event, under its registrations — join and book on the hosted page, or sign in here.
|
|
751
|
+
if (ev.status === "published" &&
|
|
752
|
+
!mp.isSignedIn() &&
|
|
753
|
+
ev.registration_types.some((tp) => tp.open)) {
|
|
754
|
+
const actions = document.createElement("div");
|
|
755
|
+
actions.className = "actions";
|
|
756
|
+
const signIn = document.createElement("button");
|
|
757
|
+
signIn.type = "button";
|
|
758
|
+
signIn.addEventListener("click", () => void mp.signIn({
|
|
759
|
+
scopes: [
|
|
760
|
+
"events:read",
|
|
761
|
+
"registrations:read",
|
|
762
|
+
"registrations:write",
|
|
763
|
+
],
|
|
764
|
+
}));
|
|
765
|
+
if (join) {
|
|
766
|
+
const a = document.createElement("a");
|
|
767
|
+
a.className = "button";
|
|
768
|
+
a.href = eventEnrollUrl(join.appUrl, join.slug, ev.slug, lang);
|
|
769
|
+
a.target = "_blank";
|
|
770
|
+
a.rel = "noopener";
|
|
771
|
+
a.textContent = t.joinAndBook;
|
|
772
|
+
actions.appendChild(a);
|
|
773
|
+
signIn.className = "link";
|
|
774
|
+
signIn.textContent = t.alreadyMember;
|
|
775
|
+
}
|
|
776
|
+
else
|
|
777
|
+
signIn.textContent = t.signInToRegister;
|
|
778
|
+
actions.appendChild(signIn);
|
|
779
|
+
el.appendChild(actions);
|
|
780
|
+
}
|
|
286
781
|
return el;
|
|
287
782
|
}
|
|
288
783
|
typeRow(ev, tp, mp, lang) {
|
|
289
784
|
const t = STRINGS[lang];
|
|
290
|
-
const row = document.createElement(
|
|
291
|
-
row.className =
|
|
292
|
-
const left = document.createElement(
|
|
293
|
-
const n = document.createElement(
|
|
294
|
-
n.className =
|
|
785
|
+
const row = document.createElement("div");
|
|
786
|
+
row.className = "type";
|
|
787
|
+
const left = document.createElement("div");
|
|
788
|
+
const n = document.createElement("div");
|
|
789
|
+
n.className = "n";
|
|
295
790
|
n.textContent = `${tp.name} · ${tp.amount > 0 ? money(tp.amount, tp.currency, lang) : t.free}`;
|
|
296
791
|
left.appendChild(n);
|
|
297
|
-
const s = document.createElement(
|
|
298
|
-
s.className =
|
|
299
|
-
s.textContent = [
|
|
792
|
+
const s = document.createElement("div");
|
|
793
|
+
s.className = "muted";
|
|
794
|
+
s.textContent = [
|
|
795
|
+
tp.remaining != null ? `${tp.remaining} ${t.places}` : "",
|
|
796
|
+
tp.reason ? t.reason[tp.reason] || tp.reason : "",
|
|
797
|
+
]
|
|
798
|
+
.filter(Boolean)
|
|
799
|
+
.join(" · ");
|
|
300
800
|
left.appendChild(s);
|
|
301
801
|
row.appendChild(left);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
btn.disabled =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
802
|
+
// A visitor has the event's own call below the registrations (`card`): no button per registration.
|
|
803
|
+
if (!mp.isSignedIn())
|
|
804
|
+
return row;
|
|
805
|
+
const btn = document.createElement("button");
|
|
806
|
+
btn.type = "button";
|
|
807
|
+
btn.textContent = t.register;
|
|
808
|
+
btn.disabled = !tp.open;
|
|
809
|
+
btn.addEventListener("click", async () => {
|
|
810
|
+
btn.disabled = true;
|
|
811
|
+
try {
|
|
812
|
+
const reg = await mp.registrations.create(ev.slug, {
|
|
813
|
+
registration_type: tp.uid,
|
|
814
|
+
});
|
|
815
|
+
this.state[ev.id] = reg;
|
|
816
|
+
this.dispatchEvent(new CustomEvent("vimine-registration", {
|
|
817
|
+
detail: { registration: reg },
|
|
818
|
+
bubbles: true,
|
|
819
|
+
}));
|
|
820
|
+
void this.render();
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
btn.disabled = false;
|
|
824
|
+
const reasons = (err?.errors || [])
|
|
825
|
+
.map((e) => t.reason[e.code || ""] || e.code)
|
|
826
|
+
.filter(Boolean);
|
|
827
|
+
s.textContent = reasons.length ? reasons.join(", ") : t.error;
|
|
828
|
+
s.className = "error";
|
|
829
|
+
}
|
|
830
|
+
});
|
|
327
831
|
row.appendChild(btn);
|
|
328
832
|
return row;
|
|
329
833
|
}
|