@vimine/sdk 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vimine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @vimine/sdk
2
+
3
+ SDK per **Accedi con Vimine** e per l'API Vimine — siti e app di associazioni su vimine.network.
4
+ Zero dipendenze; browser (WebCrypto, fetch) e Node ≥ 18.
5
+
6
+ ```
7
+ npm install @vimine/sdk
8
+ ```
9
+
10
+ ## Nel browser (client pubblico)
11
+
12
+ ```js
13
+ import { Vimine } from '@vimine/sdk';
14
+
15
+ const vimine = new Vimine({
16
+ clientId: 'vmc_…', // dalla console Sviluppatori
17
+ redirectUri: 'https://www.tuosito.it/vimine/callback',
18
+ scopes: ['profile:read', 'membership:read'], // permessi predefiniti
19
+ storage: 'session' // 'session' (per scheda) | 'local' | 'memory'
20
+ });
21
+
22
+ // 1. da un click dell'utente: porta alla pagina di accesso e consenso di Vimine
23
+ button.addEventListener('click', () => vimine.signIn({ returnTo: '/area-soci' }));
24
+
25
+ // 2. nella pagina di callback (redirectUri): scambia il codice e memorizza la sessione
26
+ const result = await vimine.handleRedirect(); // null se l'URL non porta parametri di accesso
27
+ if (result) location.assign(result.returnTo || '/');
28
+
29
+ // 3. usa l'API per conto del socio
30
+ const me = await vimine.me.get();
31
+ const membership = await vimine.membership.get(); // stato, verdetto, scadenza, token della tessera
32
+ await vimine.membership.request({ accept_statuto: { version: 3 } });
33
+ await vimine.membership.acceptStatuto({ version: 3 });
34
+ const card = await vimine.membership.card(); // { token: 'vmt_…', nbf, exp }
35
+ await vimine.connections.revoke('vmc_altra_app');
36
+ const { items } = await vimine.events.list(); // gli eventi dell'associazione (events:read)
37
+ const reg = await vimine.registrations.create('serata-jazz', { registration_type: items[0].registration_types[0].uid });
38
+ // reg.status: 'confirmed' (+ reg.token, il QR) oppure 'reserved' (+ reg.payment_instructions)
39
+
40
+ // 4. uscita: revoca la sessione API; con `everywhere` chiude anche la sessione Vimine
41
+ await vimine.signOut({ everywhere: true });
42
+ ```
43
+
44
+ I token vengono rinnovati da soli (30 s prima della scadenza, una sola richiesta anche con chiamate
45
+ concorrenti); una chiamata che riceve 401 viene ritentata una volta dopo il rinnovo. Gli errori sono
46
+ `VimineError` con `code` stabile (`insufficient_scope`, `validation_failed`, `statuto_outdated`, …), `status`,
47
+ `requestId` ed eventuali `errors[]` per campo. `vimine.onChange(listener)` notifica accesso, rinnovo e uscita.
48
+
49
+ Endpoint pubblici, senza sessione: `vimine.public.verifyCard('vmt_…')`, `vimine.public.association('slug')`,
50
+ `vimine.public.validateFiscalCode({ code, … })`, `vimine.public.cardKeys()`, `vimine.public.events.list('slug')` — o `publicApi()` da solo.
51
+
52
+ ## Con React
53
+
54
+ ```jsx
55
+ import { Vimine } from '@vimine/sdk';
56
+ import { VimineProvider, useVimine, SignInButton } from '@vimine/sdk/react';
57
+
58
+ const vimine = new Vimine({ clientId: 'vmc_…', redirectUri: 'https://www.tuosito.it/callback' });
59
+
60
+ function App() {
61
+ return (
62
+ <VimineProvider client={vimine} onSignedIn={(r) => console.log('socio', r.session.sub)}>
63
+ <Tessera />
64
+ </VimineProvider>
65
+ );
66
+ }
67
+
68
+ function Tessera() {
69
+ const { isSignedIn, client, signOut } = useVimine();
70
+ if (!isSignedIn) return <SignInButton scopes={['membership:read']} />;
71
+ // …client.membership.get()
72
+ }
73
+ ```
74
+
75
+ ## Sul server (client riservato)
76
+
77
+ ```js
78
+ import { VimineServer } from '@vimine/sdk/server';
79
+
80
+ const vimineServer = new VimineServer({ clientId: 'vmc_…', clientSecret: process.env.VIMINE_SECRET, scopes: ['memberships:read'] });
81
+ const status = await vimineServer.memberships.status('uuid-della-tessera'); // { status, verdict, valid_until } — nessun dato personale
82
+ const { members, password } = await vimineServer.sandbox.reset(); // solo client di test
83
+ await vimineServer.signup({ email, first_name, last_name }); // crea l'account: la persona riceve la mail per confermare e scegliere la password
84
+ await vimineServer.enroll({ email, first_name, last_name, codicefiscale, accept_statuto: { version } }); // + richiesta di tessera in attesa (sempre 202 accepted)
85
+ await vimineServer.enroll({ email, first_name, last_name, dob, birthplace, birth_country: 'FR', accept_statuto: { version } }); // chi non ha un codice fiscale: data, luogo e Stato di nascita
86
+ ```
87
+
88
+ Il secret non deve mai raggiungere un browser. Dopo una rotazione dalla console il secret precedente resta
89
+ valido 24 ore.
90
+
91
+ ## Ambienti
92
+
93
+ | | API | Accesso e consenso |
94
+ |---|---|---|
95
+ | Produzione (default) | `https://api.vimine.network` | `https://app.vimine.network` |
96
+
97
+ `apiUrl` e `appUrl` sono configurabili per ambienti diversi. Licenza MIT.
@@ -0,0 +1,37 @@
1
+ import { Vimine, type VimineOptions } from './index.js';
2
+ /** The client the elements use. Call once; pass your own instance to share it with the rest of the page. */
3
+ export declare function defineVimineElements(options: VimineOptions | Vimine): Vimine;
4
+ /** `<vimine-sign-in scope="…" return-to="/area" label="…" handle-redirect>` — a sign-in / sign-out button. */
5
+ export declare class VimineSignIn extends HTMLElement {
6
+ private unsubscribe;
7
+ connectedCallback(): void;
8
+ disconnectedCallback(): void;
9
+ private boot;
10
+ private render;
11
+ }
12
+ /** `<vimine-card>` — the signed-in member's card as SVG (with the live QR); refreshes when the session changes. */
13
+ export declare class VimineCard extends HTMLElement {
14
+ private unsubscribe;
15
+ connectedCallback(): void;
16
+ disconnectedCallback(): void;
17
+ private render;
18
+ }
19
+ /** `<vimine-enroll slug="…" app-url="https://app.vimine.network" height="900">` — the hosted enrolment form. */
20
+ export declare class VimineEnroll extends HTMLElement {
21
+ connectedCallback(): void;
22
+ }
23
+ /**
24
+ * `<vimine-events slug="…" filter="upcoming" limit="10" detail-url="/eventi/{slug}">` — the association's events:
25
+ * the public ones for a visitor (no sign-in), every published one for a signed-in member, each with its registration
26
+ * types (contribution, places left) and a register button (`registrations:write`) — or a sign-in button. Fires
27
+ * `vimine-registration` `{ registration }` after a registration. `detail-url` (with `{slug}`) adds a details link.
28
+ */
29
+ export declare class VimineEvents extends HTMLElement {
30
+ private unsubscribe;
31
+ private state;
32
+ connectedCallback(): void;
33
+ disconnectedCallback(): void;
34
+ private render;
35
+ private card;
36
+ private typeRow;
37
+ }
@@ -0,0 +1,331 @@
1
+ // @vimine/sdk/elements — the low-code tier: three custom elements over the browser client, Shadow DOM,
2
+ // styled through `--vimine-*` CSS custom properties, Italian by default (`lang="en"` on the element or the page
3
+ // switches the strings). Register them once, then use the tags anywhere (CMS pages, static sites):
4
+ //
5
+ // import { defineVimineElements } from '@vimine/sdk/elements';
6
+ // defineVimineElements({ clientId: 'vmc_…', redirectUri: 'https://www.example.org/callback' });
7
+ //
8
+ // <vimine-sign-in scope="membership:read"></vimine-sign-in> a "Sign in with Vimine" button
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)
11
+ // <vimine-events slug="your-slug"></vimine-events> the association's events (public ones for
12
+ // visitors, every one for a signed-in member) with
13
+ // a register button per type
14
+ //
15
+ // The callback page still calls `client.handleRedirect()` (or `<vimine-sign-in>` does it for you when
16
+ // `handle-redirect` is set on the element and the page is the redirect URI).
17
+ import { Vimine } from './index.js';
18
+ let shared = null;
19
+ const STRINGS = {
20
+ it: {
21
+ signIn: 'Accedi con Vimine', signOut: 'Esci', loading: 'Caricamento…', notSignedIn: 'Accedi per vedere la tua tessera.', noMembership: 'Nessuna tessera in questa associazione.', notActive: 'La tessera non è ancora attiva: il codice compare quando lo diventa.', error: 'Si è verificato un errore.',
22
+ noEvents: 'Nessun evento in programma.', free: 'gratuita', places: 'posti', register: 'Iscriviti', signInToRegister: 'Accedi per iscriverti', registered: 'Iscrizione confermata', reserved: 'Posto riservato: in attesa del contributo', details: 'Dettagli',
23
+ reason: { sold_out: 'esaurito', event_full: 'al completo', registration_not_open: 'iscrizioni non ancora aperte', registration_closed: 'iscrizioni chiuse', membership_required: 'riservato ai soci', membership_not_active: 'tessera non valida', membership_not_yet_valid: 'la tessera non è ancora utilizzabile a quella data', membership_type_not_allowed: 'tipo di tessera non ammesso', max_per_user: 'già iscritto', event_past: 'evento concluso', event_cancelled: 'evento annullato', event_not_published: 'non pubblicato', plan_limit: 'non disponibile', invalid_type: 'tipo non valido' }
24
+ },
25
+ en: {
26
+ signIn: 'Sign in with Vimine', signOut: 'Sign out', loading: 'Loading…', notSignedIn: 'Sign in to see your card.', noMembership: 'No membership in this association.', notActive: 'The card is not active yet: its code appears once it is.', error: 'Something went wrong.',
27
+ noEvents: 'No upcoming events.', free: 'free', places: 'places', register: 'Register', signInToRegister: 'Sign in to register', registered: 'Registration confirmed', reserved: 'Place reserved: awaiting the contribution', details: 'Details',
28
+ reason: { sold_out: 'sold out', event_full: 'full', registration_not_open: 'registrations not open yet', registration_closed: 'registrations closed', membership_required: 'members only', membership_not_active: 'membership not valid', membership_not_yet_valid: 'your card is not usable yet on that date', membership_type_not_allowed: 'membership type not admitted', max_per_user: 'already registered', event_past: 'event over', event_cancelled: 'event cancelled', event_not_published: 'not published', plan_limit: 'not available', invalid_type: 'invalid type' }
29
+ }
30
+ };
31
+ const langOf = (el) => {
32
+ const l = (el.getAttribute('lang') || el.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || 'it').slice(0, 2).toLowerCase();
33
+ return l === 'en' ? 'en' : 'it';
34
+ };
35
+ const BASE_CSS = `
36
+ :host { display: inline-block; font-family: var(--vimine-font, system-ui, sans-serif); color: var(--vimine-color, #111); }
37
+ button { cursor: pointer; border: 0; border-radius: var(--vimine-radius, 999px); padding: var(--vimine-padding, 0.65rem 1.1rem);
38
+ background: var(--vimine-accent, #0a0e1a); color: var(--vimine-accent-contrast, #fff); font: inherit; font-weight: 600; }
39
+ button:disabled { opacity: .6; cursor: default; }
40
+ .muted { color: var(--vimine-muted, #666); font-size: .9em; }
41
+ .error { color: var(--vimine-error, #a33); font-size: .9em; }
42
+ `;
43
+ const shadow = (el, css) => {
44
+ const root = el.shadowRoot || el.attachShadow({ mode: 'open' });
45
+ root.innerHTML = `<style>${BASE_CSS}${css}</style>`;
46
+ return root;
47
+ };
48
+ /** 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) {
50
+ shared = options instanceof Vimine ? options : new Vimine(options);
51
+ if (typeof customElements !== 'undefined') {
52
+ if (!customElements.get('vimine-sign-in'))
53
+ customElements.define('vimine-sign-in', VimineSignIn);
54
+ if (!customElements.get('vimine-card'))
55
+ customElements.define('vimine-card', VimineCard);
56
+ if (!customElements.get('vimine-enroll'))
57
+ customElements.define('vimine-enroll', VimineEnroll);
58
+ if (!customElements.get('vimine-events'))
59
+ customElements.define('vimine-events', VimineEvents);
60
+ }
61
+ return shared;
62
+ }
63
+ const client = () => {
64
+ if (!shared)
65
+ throw new Error('Vimine elements: call defineVimineElements({ clientId, redirectUri }) first');
66
+ return shared;
67
+ };
68
+ /** `<vimine-sign-in scope="…" return-to="/area" label="…" handle-redirect>` — a sign-in / sign-out button. */
69
+ export class VimineSignIn extends HTMLElement {
70
+ unsubscribe = null;
71
+ connectedCallback() {
72
+ void this.boot();
73
+ }
74
+ disconnectedCallback() {
75
+ this.unsubscribe?.();
76
+ this.unsubscribe = null;
77
+ }
78
+ async boot() {
79
+ const mp = client();
80
+ if (this.hasAttribute('handle-redirect')) {
81
+ try {
82
+ await mp.handleRedirect();
83
+ }
84
+ catch (err) {
85
+ this.render(err instanceof Error ? err.message : String(err));
86
+ return;
87
+ }
88
+ }
89
+ this.render();
90
+ this.unsubscribe = mp.onChange(() => this.render());
91
+ }
92
+ render(error) {
93
+ const mp = client();
94
+ const t = STRINGS[langOf(this)];
95
+ const root = shadow(this, '');
96
+ const btn = document.createElement('button');
97
+ btn.type = 'button';
98
+ if (mp.isSignedIn()) {
99
+ btn.textContent = this.getAttribute('label-signed-in') || t.signOut;
100
+ btn.addEventListener('click', () => void mp.signOut({ everywhere: this.hasAttribute('everywhere') }));
101
+ }
102
+ else {
103
+ btn.textContent = this.getAttribute('label') || t.signIn;
104
+ btn.addEventListener('click', () => {
105
+ const scopes = (this.getAttribute('scope') || '').split(/[\s,]+/).filter(Boolean);
106
+ void mp.signIn({ ...(scopes.length ? { scopes } : {}), ...(this.getAttribute('return-to') ? { returnTo: this.getAttribute('return-to') } : {}) });
107
+ });
108
+ }
109
+ root.appendChild(btn);
110
+ if (error) {
111
+ const e = document.createElement('div');
112
+ e.className = 'error';
113
+ e.textContent = error;
114
+ root.appendChild(e);
115
+ }
116
+ this.dispatchEvent(new CustomEvent('vimine-change', { detail: { signedIn: mp.isSignedIn() }, bubbles: true }));
117
+ }
118
+ }
119
+ /** `<vimine-card>` — the signed-in member's card as SVG (with the live QR); refreshes when the session changes. */
120
+ export class VimineCard extends HTMLElement {
121
+ unsubscribe = null;
122
+ connectedCallback() {
123
+ void this.render();
124
+ this.unsubscribe = client().onChange(() => void this.render());
125
+ }
126
+ disconnectedCallback() {
127
+ this.unsubscribe?.();
128
+ this.unsubscribe = null;
129
+ }
130
+ async render() {
131
+ const mp = client();
132
+ const t = STRINGS[langOf(this)];
133
+ 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
+ const note = (cls, text) => {
135
+ const d = document.createElement('div');
136
+ d.className = cls;
137
+ d.textContent = text;
138
+ root.appendChild(d);
139
+ };
140
+ if (!mp.isSignedIn())
141
+ return note('muted', t.notSignedIn);
142
+ note('muted', t.loading);
143
+ try {
144
+ const svg = await mp.membership.cardSvg();
145
+ 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('vimine-card', { detail: { loaded: true }, bubbles: true }));
147
+ }
148
+ catch (err) {
149
+ root.innerHTML = `<style>${BASE_CSS}</style>`;
150
+ const code = err?.code;
151
+ // `not_active`: the membership exists but has no card (pending, suspended, revoked) — a state, not a failure.
152
+ const quiet = code === 'not_found' || code === 'not_active';
153
+ note(quiet ? 'muted' : 'error', code === 'not_found' ? t.noMembership : code === 'not_active' ? t.notActive : t.error);
154
+ }
155
+ }
156
+ }
157
+ /** `<vimine-enroll slug="…" app-url="https://app.vimine.network" height="900">` — the hosted enrolment form. */
158
+ export class VimineEnroll extends HTMLElement {
159
+ connectedCallback() {
160
+ const slug = this.getAttribute('slug');
161
+ const root = shadow(this, ':host { display: block; } iframe { width: 100%; border: 0; border-radius: var(--vimine-radius, 16px); background: transparent; }');
162
+ if (!slug) {
163
+ const e = document.createElement('div');
164
+ e.className = 'error';
165
+ e.textContent = 'vimine-enroll: missing slug';
166
+ root.appendChild(e);
167
+ return;
168
+ }
169
+ const appUrl = (this.getAttribute('app-url') || client().options.appUrl || 'https://app.vimine.network').replace(/\/+$/, '');
170
+ const frame = document.createElement('iframe');
171
+ frame.src = `${appUrl}/enroll/${encodeURIComponent(slug)}`;
172
+ frame.title = 'Vimine';
173
+ frame.loading = 'lazy';
174
+ frame.style.height = `${Number(this.getAttribute('height')) || 900}px`;
175
+ frame.setAttribute('sandbox', 'allow-forms allow-scripts allow-same-origin allow-popups');
176
+ root.appendChild(frame);
177
+ }
178
+ }
179
+ const money = (amount, currency, lang) => {
180
+ try {
181
+ return new Intl.NumberFormat(lang === 'en' ? 'en-GB' : 'it-IT', { style: 'currency', currency: currency || 'EUR' }).format(amount);
182
+ }
183
+ catch {
184
+ return `${amount.toFixed(2)} ${currency}`;
185
+ }
186
+ };
187
+ const when = (iso, tz, lang) => {
188
+ try {
189
+ return new Date(iso).toLocaleString(lang === 'en' ? 'en-GB' : 'it-IT', { timeZone: tz, dateStyle: 'full', timeStyle: 'short' });
190
+ }
191
+ catch {
192
+ return new Date(iso).toLocaleString();
193
+ }
194
+ };
195
+ /**
196
+ * `<vimine-events slug="…" filter="upcoming" limit="10" detail-url="/eventi/{slug}">` — the association's events:
197
+ * 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`) — or a sign-in button. Fires
199
+ * `vimine-registration` `{ registration }` after a registration. `detail-url` (with `{slug}`) adds a details link.
200
+ */
201
+ export class VimineEvents extends HTMLElement {
202
+ unsubscribe = null;
203
+ state = {};
204
+ connectedCallback() {
205
+ void this.render();
206
+ this.unsubscribe = client().onChange(() => void this.render());
207
+ }
208
+ disconnectedCallback() {
209
+ this.unsubscribe?.();
210
+ this.unsubscribe = null;
211
+ }
212
+ async render() {
213
+ const mp = client();
214
+ const lang = langOf(this);
215
+ const t = STRINGS[lang];
216
+ const root = shadow(this, `:host { display: block; }
217
+ .event { border: 1px solid var(--vimine-border, #e5e5e5); border-radius: var(--vimine-radius, 16px); padding: var(--vimine-padding, 1rem); margin: 0 0 1rem; }
218
+ .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
+ .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); }`);
221
+ const note = (cls, text) => {
222
+ const d = document.createElement('div');
223
+ d.className = cls;
224
+ d.textContent = text;
225
+ root.appendChild(d);
226
+ };
227
+ note('muted', t.loading);
228
+ const slug = this.getAttribute('slug') || '';
229
+ const filter = (this.getAttribute('filter') || 'upcoming');
230
+ const limit = Number(this.getAttribute('limit')) || undefined;
231
+ let items;
232
+ try {
233
+ const params = { filter, ...(limit ? { limit } : {}) };
234
+ items = mp.isSignedIn() ? (await mp.events.list(params)).items : (await mp.public.events.list(slug, params)).items;
235
+ }
236
+ catch {
237
+ root.innerHTML = `<style>${BASE_CSS}</style>`;
238
+ return note('error', t.error);
239
+ }
240
+ root.innerHTML = root.querySelector('style').outerHTML;
241
+ if (!items.length)
242
+ return note('muted', t.noEvents);
243
+ const detailUrl = this.getAttribute('detail-url');
244
+ for (const ev of items)
245
+ root.appendChild(this.card(ev, mp, lang, detailUrl));
246
+ }
247
+ card(ev, mp, lang, detailUrl) {
248
+ const t = STRINGS[lang];
249
+ const el = document.createElement('article');
250
+ el.className = 'event';
251
+ if (ev.cover_url) {
252
+ const img = document.createElement('img');
253
+ img.src = ev.cover_url;
254
+ img.alt = '';
255
+ el.appendChild(img);
256
+ }
257
+ const h = document.createElement('h3');
258
+ h.textContent = ev.title;
259
+ el.appendChild(h);
260
+ const meta = document.createElement('div');
261
+ meta.className = 'muted';
262
+ meta.textContent = `${when(ev.starts_at, ev.timezone, lang)}${ev.location ? ` · ${ev.location.name || ev.location.address || ''}` : ''}`;
263
+ el.appendChild(meta);
264
+ if (ev.summary) {
265
+ const p = document.createElement('p');
266
+ p.textContent = ev.summary;
267
+ el.appendChild(p);
268
+ }
269
+ if (detailUrl) {
270
+ const a = document.createElement('a');
271
+ a.href = detailUrl.replace('{slug}', encodeURIComponent(ev.slug));
272
+ a.textContent = t.details;
273
+ el.appendChild(a);
274
+ }
275
+ const mine = this.state[ev.id];
276
+ if (mine) {
277
+ const d = document.createElement('div');
278
+ d.className = 'muted';
279
+ d.textContent = mine.status === 'confirmed' ? t.registered : t.reserved;
280
+ el.appendChild(d);
281
+ return el;
282
+ }
283
+ if (ev.status === 'published')
284
+ for (const tp of ev.registration_types)
285
+ el.appendChild(this.typeRow(ev, tp, mp, lang));
286
+ return el;
287
+ }
288
+ typeRow(ev, tp, mp, lang) {
289
+ const t = STRINGS[lang];
290
+ const row = document.createElement('div');
291
+ row.className = 'type';
292
+ const left = document.createElement('div');
293
+ const n = document.createElement('div');
294
+ n.className = 'n';
295
+ n.textContent = `${tp.name} · ${tp.amount > 0 ? money(tp.amount, tp.currency, lang) : t.free}`;
296
+ left.appendChild(n);
297
+ const s = document.createElement('div');
298
+ s.className = 'muted';
299
+ s.textContent = [tp.remaining != null ? `${tp.remaining} ${t.places}` : '', tp.reason ? t.reason[tp.reason] || tp.reason : ''].filter(Boolean).join(' · ');
300
+ left.appendChild(s);
301
+ row.appendChild(left);
302
+ const btn = document.createElement('button');
303
+ btn.type = 'button';
304
+ if (!mp.isSignedIn()) {
305
+ btn.textContent = t.signInToRegister;
306
+ btn.addEventListener('click', () => void mp.signIn({ scopes: ['events:read', 'registrations:read', 'registrations:write'] }));
307
+ }
308
+ else {
309
+ btn.textContent = t.register;
310
+ btn.disabled = !tp.open;
311
+ btn.addEventListener('click', async () => {
312
+ btn.disabled = true;
313
+ try {
314
+ const reg = await mp.registrations.create(ev.slug, { registration_type: tp.uid });
315
+ this.state[ev.id] = reg;
316
+ this.dispatchEvent(new CustomEvent('vimine-registration', { detail: { registration: reg }, bubbles: true }));
317
+ void this.render();
318
+ }
319
+ catch (err) {
320
+ btn.disabled = false;
321
+ const reasons = (err?.errors || []).map((e) => t.reason[e.code || ''] || e.code).filter(Boolean);
322
+ s.textContent = reasons.length ? reasons.join(', ') : t.error;
323
+ s.className = 'error';
324
+ }
325
+ });
326
+ }
327
+ row.appendChild(btn);
328
+ return row;
329
+ }
330
+ }
331
+ //# sourceMappingURL=elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,uGAAuG;AACvG,gHAAgH;AAChH,mGAAmG;AACnG,EAAE;AACF,iEAAiE;AACjE,kGAAkG;AAClG,EAAE;AACF,+FAA+F;AAC/F,uGAAuG;AACvG,6GAA6G;AAC7G,0GAA0G;AAC1G,uHAAuH;AACvH,iGAAiG;AACjG,EAAE;AACF,sGAAsG;AACtG,6EAA6E;AAC7E,OAAO,EAAE,MAAM,EAAsB,MAAM,YAAY,CAAC;AAGxD,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,MAAM,OAAO,GAAG;IACZ,EAAE,EAAE;QACA,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,mCAAmC,EAAE,YAAY,EAAE,yCAAyC,EAAE,SAAS,EAAE,sEAAsE,EAAE,KAAK,EAAE,4BAA4B;QACxS,QAAQ,EAAE,6BAA6B,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,UAAU,EAAE,uBAAuB,EAAE,QAAQ,EAAE,2CAA2C,EAAE,OAAO,EAAE,UAAU;QAC7P,MAAM,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,qBAAqB,EAAE,8BAA8B,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,oDAAoD,EAAE,2BAA2B,EAAE,6BAA6B,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,iBAAiB,EAAE,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAA4B;KACrmB;IACD,EAAE,EAAE;QACA,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,2BAA2B,EAAE,YAAY,EAAE,oCAAoC,EAAE,SAAS,EAAE,0DAA0D,EAAE,KAAK,EAAE,uBAAuB;QAC5Q,QAAQ,EAAE,qBAAqB,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,UAAU,EAAE,wBAAwB,EAAE,QAAQ,EAAE,2CAA2C,EAAE,OAAO,EAAE,SAAS;QAC/O,MAAM,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,cAAc,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,0CAA0C,EAAE,2BAA2B,EAAE,8BAA8B,EAAE,YAAY,EAAE,oBAAoB,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAA4B;KAC7kB;CACJ,CAAC;AAEF,MAAM,MAAM,GAAG,CAAC,EAAe,EAAQ,EAAE;IACrC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACrJ,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACpC,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG;;;;;;;CAOhB,CAAC;AAEF,MAAM,MAAM,GAAG,CAAC,EAAe,EAAE,GAAW,EAAc,EAAE;IACxD,MAAM,IAAI,GAAG,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,IAAI,CAAC,SAAS,GAAG,UAAU,QAAQ,GAAG,GAAG,UAAU,CAAC;IACpD,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF,4GAA4G;AAC5G,MAAM,UAAU,oBAAoB,CAAC,OAA+B;IAChE,MAAM,GAAG,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;IACnE,IAAI,OAAO,cAAc,KAAK,WAAW,EAAE,CAAC;QACxC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,gBAAgB,CAAC;YAAE,cAAc,CAAC,MAAM,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;QACjG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,aAAa,CAAC;YAAE,cAAc,CAAC,MAAM,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;QACzF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC;YAAE,cAAc,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;QAC/F,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC;YAAE,cAAc,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;IACnG,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,GAAW,EAAE;IACxB,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;IAC5G,OAAO,MAAM,CAAC;AAClB,CAAC,CAAC;AAEF,8GAA8G;AAC9G,MAAM,OAAO,YAAa,SAAQ,WAAW;IACjC,WAAW,GAAwB,IAAI,CAAC;IAChD,iBAAiB;QACb,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;IACD,oBAAoB;QAChB,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC5B,CAAC;IACO,KAAK,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;QACpB,IAAI,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC;gBACD,MAAM,EAAE,CAAC,cAAc,EAAE,CAAC;YAC9B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC9D,OAAO;YACX,CAAC;QACL,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,CAAC;IACO,MAAM,CAAC,KAAc;QACzB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC7C,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC;QACpB,IAAI,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC;YAClB,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACpE,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1G,CAAC;aAAM,CAAC;YACJ,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;YACzD,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBAC/B,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAY,CAAC;gBAC7F,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,WAAW,CAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACvJ,CAAC,CAAC,CAAC;QACP,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,KAAK,EAAE,CAAC;YACR,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;YACtB,CAAC,CAAC,WAAW,GAAG,KAAK,CAAC;YACtB,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACxB,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,UAAU,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACnH,CAAC;CACJ;AAED,mHAAmH;AACnH,MAAM,OAAO,UAAW,SAAQ,WAAW;IAC/B,WAAW,GAAwB,IAAI,CAAC;IAChD,iBAAiB;QACb,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,oBAAoB;QAChB,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC5B,CAAC;IACO,KAAK,CAAC,MAAM;QAChB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,kNAAkN,CAAC,CAAC;QAC9O,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,IAAY,EAAE,EAAE;YACvC,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC;YAClB,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACxB,CAAC,CAAC;QACF,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE;YAAE,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC;QAC1D,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QACzB,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YAC1C,IAAI,CAAC,SAAS,GAAG,UAAU,QAAQ,2NAA2N,GAAG,EAAE,CAAC;YACpQ,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACpG,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,IAAI,CAAC,SAAS,GAAG,UAAU,QAAQ,UAAU,CAAC;YAC9C,MAAM,IAAI,GAAI,GAAyB,EAAE,IAAI,CAAC;YAC9C,8GAA8G;YAC9G,MAAM,KAAK,GAAG,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,YAAY,CAAC;YAC5D,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC3H,CAAC;IACL,CAAC;CACJ;AAED,gHAAgH;AAChH,MAAM,OAAO,YAAa,SAAQ,WAAW;IACzC,iBAAiB;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,kIAAkI,CAAC,CAAC;QAC9J,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;YACtB,CAAC,CAAC,WAAW,GAAG,6BAA6B,CAAC;YAC9C,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;YACpB,OAAO;QACX,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,IAAI,4BAA4B,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC7H,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC/C,KAAK,CAAC,GAAG,GAAG,GAAG,MAAM,WAAW,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3D,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC;QACvB,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QACvB,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;QACvE,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,0DAA0D,CAAC,CAAC;QAC1F,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;CACJ;AAED,MAAM,KAAK,GAAG,CAAC,MAAc,EAAE,QAAgB,EAAE,IAAU,EAAU,EAAE;IACnE,IAAI,CAAC;QACD,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvI,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC;IAC9C,CAAC;AACL,CAAC,CAAC;AACF,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAU,EAAE,IAAU,EAAU,EAAE;IACzD,IAAI,CAAC;QACD,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IACpI,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC;IAC1C,CAAC;AACL,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,OAAO,YAAa,SAAQ,WAAW;IACjC,WAAW,GAAwB,IAAI,CAAC;IACxC,KAAK,GAAiC,EAAE,CAAC;IACjD,iBAAiB;QACb,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,oBAAoB;QAChB,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC5B,CAAC;IACO,KAAK,CAAC,MAAM;QAChB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE;;;;mHAIiF,CAAC,CAAC;QAC7G,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,IAAY,EAAQ,EAAE;YAC7C,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC;YAClB,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACxB,CAAC,CAAC;QACF,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,UAAU,CAA8C,CAAC;QACxG,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,SAAS,CAAC;QAC9D,IAAI,KAAc,CAAC;QACnB,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACvD,KAAK,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;QACvH,CAAC;QAAC,MAAM,CAAC;YACL,IAAI,CAAC,SAAS,GAAG,UAAU,QAAQ,UAAU,CAAC;YAC9C,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAE,CAAC,SAAS,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QACpD,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAClD,KAAK,MAAM,EAAE,IAAI,KAAK;YAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC;IACjF,CAAC;IACO,IAAI,CAAC,EAAS,EAAE,EAAU,EAAE,IAAU,EAAE,SAAwB;QACpE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,GAAG,OAAO,CAAC;QACvB,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC1C,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC;YACvB,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC;YACb,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,KAAK,CAAC;QACzB,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACzI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACrB,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACtC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC;YAC3B,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACtB,CAAC;QACD,IAAI,SAAS,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;YACtC,CAAC,CAAC,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,kBAAkB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YAClE,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,OAAO,CAAC;YAC1B,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACtB,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,IAAI,EAAE,CAAC;YACP,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;YACtB,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;YACxE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;YAClB,OAAO,EAAE,CAAC;QACd,CAAC;QACD,IAAI,EAAE,CAAC,MAAM,KAAK,WAAW;YAAE,KAAK,MAAM,EAAE,IAAI,EAAE,CAAC,kBAAkB;gBAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;QACtH,OAAO,EAAE,CAAC;IACd,CAAC;IACO,OAAO,CAAC,EAAS,EAAE,EAAoB,EAAE,EAAU,EAAE,IAAU;QACnE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC1C,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC;QACvB,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC;QAClB,CAAC,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/F,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACpB,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;QACtB,CAAC,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,SAAS,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3J,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACpB,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC7C,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC;QACpB,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC;YACnB,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,gBAAgB,CAAC;YACrC,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,aAAa,EAAE,oBAAoB,EAAE,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;QAClI,CAAC;aAAM,CAAC;YACJ,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,QAAQ,CAAC;YAC7B,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;YACxB,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACrC,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACpB,IAAI,CAAC;oBACD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,iBAAiB,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;oBAClF,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;oBACxB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,qBAAqB,EAAE,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;oBAC7G,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;gBACvB,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACX,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC;oBACrB,MAAM,OAAO,GAAG,CAAE,GAAwC,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;oBACvI,CAAC,CAAC,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;oBAC9D,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;gBAC1B,CAAC;YACL,CAAC,CAAC,CAAC;QACP,CAAC;QACD,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACrB,OAAO,GAAG,CAAC;IACf,CAAC;CACJ","sourcesContent":["// @vimine/sdk/elements — the low-code tier: three custom elements over the browser client, Shadow DOM,\n// styled through `--vimine-*` CSS custom properties, Italian by default (`lang=\"en\"` on the element or the page\n// switches the strings). Register them once, then use the tags anywhere (CMS pages, static sites):\n//\n// import { defineVimineElements } from '@vimine/sdk/elements';\n// defineVimineElements({ clientId: 'vmc_…', redirectUri: 'https://www.example.org/callback' });\n//\n// <vimine-sign-in scope=\"membership:read\"></vimine-sign-in> a \"Sign in with Vimine\" button\n// <vimine-card></vimine-card> the signed-in member's card (SVG + QR)\n// <vimine-enroll slug=\"your-slug\"></vimine-enroll> the hosted enrolment form (iframe, Embed v0)\n// <vimine-events slug=\"your-slug\"></vimine-events> the association's events (public ones for\n// visitors, every one for a signed-in member) with\n// a register button per type\n//\n// The callback page still calls `client.handleRedirect()` (or `<vimine-sign-in>` does it for you when\n// `handle-redirect` is set on the element and the page is the redirect URI).\nimport { Vimine, type VimineOptions } from './index.js';\nimport type { Event, Registration, RegistrationType, Scope } from './types.js';\n\nlet shared: Vimine | null = null;\n\nconst STRINGS = {\n it: {\n signIn: 'Accedi con Vimine', signOut: 'Esci', loading: 'Caricamento…', notSignedIn: 'Accedi per vedere la tua tessera.', noMembership: 'Nessuna tessera in questa associazione.', notActive: 'La tessera non è ancora attiva: il codice compare quando lo diventa.', error: 'Si è verificato un errore.',\n noEvents: 'Nessun evento in programma.', free: 'gratuita', places: 'posti', register: 'Iscriviti', signInToRegister: 'Accedi per iscriverti', registered: 'Iscrizione confermata', reserved: 'Posto riservato: in attesa del contributo', details: 'Dettagli',\n reason: { sold_out: 'esaurito', event_full: 'al completo', registration_not_open: 'iscrizioni non ancora aperte', registration_closed: 'iscrizioni chiuse', membership_required: 'riservato ai soci', membership_not_active: 'tessera non valida', membership_not_yet_valid: 'la tessera non è ancora utilizzabile a quella data', membership_type_not_allowed: 'tipo di tessera non ammesso', max_per_user: 'già iscritto', event_past: 'evento concluso', event_cancelled: 'evento annullato', event_not_published: 'non pubblicato', plan_limit: 'non disponibile', invalid_type: 'tipo non valido' } as Record<string, string>\n },\n en: {\n signIn: 'Sign in with Vimine', signOut: 'Sign out', loading: 'Loading…', notSignedIn: 'Sign in to see your card.', noMembership: 'No membership in this association.', notActive: 'The card is not active yet: its code appears once it is.', error: 'Something went wrong.',\n noEvents: 'No upcoming events.', free: 'free', places: 'places', register: 'Register', signInToRegister: 'Sign in to register', registered: 'Registration confirmed', reserved: 'Place reserved: awaiting the contribution', details: 'Details',\n reason: { sold_out: 'sold out', event_full: 'full', registration_not_open: 'registrations not open yet', registration_closed: 'registrations closed', membership_required: 'members only', membership_not_active: 'membership not valid', membership_not_yet_valid: 'your card is not usable yet on that date', membership_type_not_allowed: 'membership type not admitted', max_per_user: 'already registered', event_past: 'event over', event_cancelled: 'event cancelled', event_not_published: 'not published', plan_limit: 'not available', invalid_type: 'invalid type' } as Record<string, string>\n }\n};\ntype Lang = keyof typeof STRINGS;\nconst langOf = (el: HTMLElement): Lang => {\n const l = (el.getAttribute('lang') || el.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || 'it').slice(0, 2).toLowerCase();\n return l === 'en' ? 'en' : 'it';\n};\n\nconst BASE_CSS = `\n:host { display: inline-block; font-family: var(--vimine-font, system-ui, sans-serif); color: var(--vimine-color, #111); }\nbutton { cursor: pointer; border: 0; border-radius: var(--vimine-radius, 999px); padding: var(--vimine-padding, 0.65rem 1.1rem);\n background: var(--vimine-accent, #0a0e1a); color: var(--vimine-accent-contrast, #fff); font: inherit; font-weight: 600; }\nbutton:disabled { opacity: .6; cursor: default; }\n.muted { color: var(--vimine-muted, #666); font-size: .9em; }\n.error { color: var(--vimine-error, #a33); font-size: .9em; }\n`;\n\nconst shadow = (el: HTMLElement, css: string): ShadowRoot => {\n const root = el.shadowRoot || el.attachShadow({ mode: 'open' });\n root.innerHTML = `<style>${BASE_CSS}${css}</style>`;\n return root;\n};\n\n/** The client the elements use. Call once; pass your own instance to share it with the rest of the page. */\nexport function defineVimineElements(options: VimineOptions | Vimine): Vimine {\n shared = options instanceof Vimine ? options : new Vimine(options);\n if (typeof customElements !== 'undefined') {\n if (!customElements.get('vimine-sign-in')) customElements.define('vimine-sign-in', VimineSignIn);\n if (!customElements.get('vimine-card')) customElements.define('vimine-card', VimineCard);\n if (!customElements.get('vimine-enroll')) customElements.define('vimine-enroll', VimineEnroll);\n if (!customElements.get('vimine-events')) customElements.define('vimine-events', VimineEvents);\n }\n return shared;\n}\n\nconst client = (): Vimine => {\n if (!shared) throw new Error('Vimine elements: call defineVimineElements({ clientId, redirectUri }) first');\n return shared;\n};\n\n/** `<vimine-sign-in scope=\"…\" return-to=\"/area\" label=\"…\" handle-redirect>` — a sign-in / sign-out button. */\nexport class VimineSignIn extends HTMLElement {\n private unsubscribe: (() => void) | null = null;\n connectedCallback(): void {\n void this.boot();\n }\n disconnectedCallback(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n private async boot(): Promise<void> {\n const mp = client();\n if (this.hasAttribute('handle-redirect')) {\n try {\n await mp.handleRedirect();\n } catch (err) {\n this.render(err instanceof Error ? err.message : String(err));\n return;\n }\n }\n this.render();\n this.unsubscribe = mp.onChange(() => this.render());\n }\n private render(error?: string): void {\n const mp = client();\n const t = STRINGS[langOf(this)];\n const root = shadow(this, '');\n const btn = document.createElement('button');\n btn.type = 'button';\n if (mp.isSignedIn()) {\n btn.textContent = this.getAttribute('label-signed-in') || t.signOut;\n btn.addEventListener('click', () => void mp.signOut({ everywhere: this.hasAttribute('everywhere') }));\n } else {\n btn.textContent = this.getAttribute('label') || t.signIn;\n btn.addEventListener('click', () => {\n const scopes = (this.getAttribute('scope') || '').split(/[\\s,]+/).filter(Boolean) as Scope[];\n void mp.signIn({ ...(scopes.length ? { scopes } : {}), ...(this.getAttribute('return-to') ? { returnTo: this.getAttribute('return-to')! } : {}) });\n });\n }\n root.appendChild(btn);\n if (error) {\n const e = document.createElement('div');\n e.className = 'error';\n e.textContent = error;\n root.appendChild(e);\n }\n this.dispatchEvent(new CustomEvent('vimine-change', { detail: { signedIn: mp.isSignedIn() }, bubbles: true }));\n }\n}\n\n/** `<vimine-card>` — the signed-in member's card as SVG (with the live QR); refreshes when the session changes. */\nexport class VimineCard extends HTMLElement {\n private unsubscribe: (() => void) | null = null;\n connectedCallback(): void {\n void this.render();\n this.unsubscribe = client().onChange(() => void this.render());\n }\n disconnectedCallback(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n private async render(): Promise<void> {\n const mp = client();\n const t = STRINGS[langOf(this)];\n 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)); }');\n const note = (cls: string, text: string) => {\n const d = document.createElement('div');\n d.className = cls;\n d.textContent = text;\n root.appendChild(d);\n };\n if (!mp.isSignedIn()) return note('muted', t.notSignedIn);\n note('muted', t.loading);\n try {\n const svg = await mp.membership.cardSvg();\n 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}`;\n this.dispatchEvent(new CustomEvent('vimine-card', { detail: { loaded: true }, bubbles: true }));\n } catch (err) {\n root.innerHTML = `<style>${BASE_CSS}</style>`;\n const code = (err as { code?: string })?.code;\n // `not_active`: the membership exists but has no card (pending, suspended, revoked) — a state, not a failure.\n const quiet = code === 'not_found' || code === 'not_active';\n note(quiet ? 'muted' : 'error', code === 'not_found' ? t.noMembership : code === 'not_active' ? t.notActive : t.error);\n }\n }\n}\n\n/** `<vimine-enroll slug=\"…\" app-url=\"https://app.vimine.network\" height=\"900\">` — the hosted enrolment form. */\nexport class VimineEnroll extends HTMLElement {\n connectedCallback(): void {\n const slug = this.getAttribute('slug');\n const root = shadow(this, ':host { display: block; } iframe { width: 100%; border: 0; border-radius: var(--vimine-radius, 16px); background: transparent; }');\n if (!slug) {\n const e = document.createElement('div');\n e.className = 'error';\n e.textContent = 'vimine-enroll: missing slug';\n root.appendChild(e);\n return;\n }\n const appUrl = (this.getAttribute('app-url') || client().options.appUrl || 'https://app.vimine.network').replace(/\\/+$/, '');\n const frame = document.createElement('iframe');\n frame.src = `${appUrl}/enroll/${encodeURIComponent(slug)}`;\n frame.title = 'Vimine';\n frame.loading = 'lazy';\n frame.style.height = `${Number(this.getAttribute('height')) || 900}px`;\n frame.setAttribute('sandbox', 'allow-forms allow-scripts allow-same-origin allow-popups');\n root.appendChild(frame);\n }\n}\n\nconst money = (amount: number, currency: string, lang: Lang): string => {\n try {\n return new Intl.NumberFormat(lang === 'en' ? 'en-GB' : 'it-IT', { style: 'currency', currency: currency || 'EUR' }).format(amount);\n } catch {\n return `${amount.toFixed(2)} ${currency}`;\n }\n};\nconst when = (iso: string, tz: string, lang: Lang): string => {\n try {\n return new Date(iso).toLocaleString(lang === 'en' ? 'en-GB' : 'it-IT', { timeZone: tz, dateStyle: 'full', timeStyle: 'short' });\n } catch {\n return new Date(iso).toLocaleString();\n }\n};\n\n/**\n * `<vimine-events slug=\"…\" filter=\"upcoming\" limit=\"10\" detail-url=\"/eventi/{slug}\">` — the association's events:\n * the public ones for a visitor (no sign-in), every published one for a signed-in member, each with its registration\n * types (contribution, places left) and a register button (`registrations:write`) — or a sign-in button. Fires\n * `vimine-registration` `{ registration }` after a registration. `detail-url` (with `{slug}`) adds a details link.\n */\nexport class VimineEvents extends HTMLElement {\n private unsubscribe: (() => void) | null = null;\n private state: Record<string, Registration> = {};\n connectedCallback(): void {\n void this.render();\n this.unsubscribe = client().onChange(() => void this.render());\n }\n disconnectedCallback(): void {\n this.unsubscribe?.();\n this.unsubscribe = null;\n }\n private async render(): Promise<void> {\n const mp = client();\n const lang = langOf(this);\n const t = STRINGS[lang];\n const root = shadow(this, `:host { display: block; }\n.event { border: 1px solid var(--vimine-border, #e5e5e5); border-radius: var(--vimine-radius, 16px); padding: var(--vimine-padding, 1rem); margin: 0 0 1rem; }\n.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; }\n.type { display: flex; justify-content: space-between; align-items: center; gap: .75rem; padding: .5rem 0; border-top: 1px solid var(--vimine-border, #eee); }\n.type .n { font-weight: 600; } .type button { padding: .45rem .9rem; } a { color: var(--vimine-accent, #0a0e1a); }`);\n const note = (cls: string, text: string): void => {\n const d = document.createElement('div');\n d.className = cls;\n d.textContent = text;\n root.appendChild(d);\n };\n note('muted', t.loading);\n const slug = this.getAttribute('slug') || '';\n const filter = (this.getAttribute('filter') || 'upcoming') as 'upcoming' | 'past' | 'cancelled' | 'all';\n const limit = Number(this.getAttribute('limit')) || undefined;\n let items: Event[];\n try {\n const params = { filter, ...(limit ? { limit } : {}) };\n items = mp.isSignedIn() ? (await mp.events.list(params)).items : (await mp.public.events.list(slug, params)).items;\n } catch {\n root.innerHTML = `<style>${BASE_CSS}</style>`;\n return note('error', t.error);\n }\n root.innerHTML = root.querySelector('style')!.outerHTML;\n if (!items.length) return note('muted', t.noEvents);\n const detailUrl = this.getAttribute('detail-url');\n for (const ev of items) root.appendChild(this.card(ev, mp, lang, detailUrl));\n }\n private card(ev: Event, mp: Vimine, lang: Lang, detailUrl: string | null): HTMLElement {\n const t = STRINGS[lang];\n const el = document.createElement('article');\n el.className = 'event';\n if (ev.cover_url) {\n const img = document.createElement('img');\n img.src = ev.cover_url;\n img.alt = '';\n el.appendChild(img);\n }\n const h = document.createElement('h3');\n h.textContent = ev.title;\n el.appendChild(h);\n const meta = document.createElement('div');\n meta.className = 'muted';\n meta.textContent = `${when(ev.starts_at, ev.timezone, lang)}${ev.location ? ` · ${ev.location.name || ev.location.address || ''}` : ''}`;\n el.appendChild(meta);\n if (ev.summary) {\n const p = document.createElement('p');\n p.textContent = ev.summary;\n el.appendChild(p);\n }\n if (detailUrl) {\n const a = document.createElement('a');\n a.href = detailUrl.replace('{slug}', encodeURIComponent(ev.slug));\n a.textContent = t.details;\n el.appendChild(a);\n }\n const mine = this.state[ev.id];\n if (mine) {\n const d = document.createElement('div');\n d.className = 'muted';\n d.textContent = mine.status === 'confirmed' ? t.registered : t.reserved;\n el.appendChild(d);\n return el;\n }\n if (ev.status === 'published') for (const tp of ev.registration_types) el.appendChild(this.typeRow(ev, tp, mp, lang));\n return el;\n }\n private typeRow(ev: Event, tp: RegistrationType, mp: Vimine, lang: Lang): HTMLElement {\n const t = STRINGS[lang];\n const row = document.createElement('div');\n row.className = 'type';\n const left = document.createElement('div');\n const n = document.createElement('div');\n n.className = 'n';\n n.textContent = `${tp.name} · ${tp.amount > 0 ? money(tp.amount, tp.currency, lang) : t.free}`;\n left.appendChild(n);\n const s = document.createElement('div');\n s.className = 'muted';\n s.textContent = [tp.remaining != null ? `${tp.remaining} ${t.places}` : '', tp.reason ? t.reason[tp.reason] || tp.reason : ''].filter(Boolean).join(' · ');\n left.appendChild(s);\n row.appendChild(left);\n const btn = document.createElement('button');\n btn.type = 'button';\n if (!mp.isSignedIn()) {\n btn.textContent = t.signInToRegister;\n btn.addEventListener('click', () => void mp.signIn({ scopes: ['events:read', 'registrations:read', 'registrations:write'] }));\n } else {\n btn.textContent = t.register;\n btn.disabled = !tp.open;\n btn.addEventListener('click', async () => {\n btn.disabled = true;\n try {\n const reg = await mp.registrations.create(ev.slug, { registration_type: tp.uid });\n this.state[ev.id] = reg;\n this.dispatchEvent(new CustomEvent('vimine-registration', { detail: { registration: reg }, bubbles: true }));\n void this.render();\n } catch (err) {\n btn.disabled = false;\n const reasons = ((err as { errors?: { code?: string }[] })?.errors || []).map((e) => t.reason[e.code || ''] || e.code).filter(Boolean);\n s.textContent = reasons.length ? reasons.join(', ') : t.error;\n s.className = 'error';\n }\n });\n }\n row.appendChild(btn);\n return row;\n }\n}\n\n"]}
@@ -0,0 +1,51 @@
1
+ import type { FiscalCodeHint, FiscalCodeMismatch } from './types.js';
2
+ /**
3
+ * One entry of a problem's `errors[]`: the field and what is wrong with it. A `codicefiscale_mismatch` entry also says
4
+ * what disagrees with the code and whether the person may confirm it (resend with `codicefiscale_confirmed: true`).
5
+ */
6
+ export interface FieldError {
7
+ field?: string;
8
+ code?: string;
9
+ message?: string;
10
+ /** `codicefiscale_mismatch`: what disagrees with the code (never `gender`: it is not cross-checked there). */
11
+ mismatches?: FiscalCodeMismatch[];
12
+ /** `codicefiscale_mismatch`: `names_swapped` when the code matches once surname and given name are exchanged. */
13
+ hints?: FiscalCodeHint[];
14
+ /** `codicefiscale_mismatch`: `false` when both names and the date disagree — nothing ties the code to the person. */
15
+ confirmable?: boolean;
16
+ [k: string]: unknown;
17
+ }
18
+ export interface ProblemDetails {
19
+ type?: string;
20
+ title?: string;
21
+ status?: number;
22
+ code?: string;
23
+ detail?: string;
24
+ request_id?: string;
25
+ errors?: FieldError[];
26
+ [k: string]: unknown;
27
+ }
28
+ export interface OAuthErrorBody {
29
+ error: string;
30
+ error_description?: string;
31
+ }
32
+ export declare class VimineError extends Error {
33
+ readonly code: string;
34
+ readonly status: number | null;
35
+ readonly requestId: string | null;
36
+ readonly detail: string | null;
37
+ readonly errors: ProblemDetails['errors'];
38
+ readonly cause?: unknown;
39
+ constructor(code: string, message: string, opts?: {
40
+ status?: number | null;
41
+ requestId?: string | null;
42
+ detail?: string | null;
43
+ errors?: ProblemDetails['errors'];
44
+ cause?: unknown;
45
+ });
46
+ /** True for the errors that mean "the session is gone: sign in again". */
47
+ get isAuthError(): boolean;
48
+ static fromProblem(body: ProblemDetails, status: number): VimineError;
49
+ static fromOAuth(body: OAuthErrorBody, status: number): VimineError;
50
+ static fromResponse(res: Response): Promise<VimineError>;
51
+ }