@openeventkit/event-site 2.1.21 → 2.1.23

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openeventkit/event-site",
3
3
  "description": "Event Site",
4
- "version": "2.1.21",
4
+ "version": "2.1.23",
5
5
  "author": "Tipit LLC",
6
6
  "dependencies": {
7
7
  "@emotion/server": "^11.11.0",
@@ -89,6 +89,7 @@ const registerCustomFont = (siteFont) => {
89
89
  return false;
90
90
  };
91
91
 
92
+
92
93
  const calculateOptimalFontSize = (text, maxWidth = 650, initialFontSize = 48, minFontSize = 24) => {
93
94
  // estimate average character width based on font size
94
95
  // for most fonts, character width is roughly 0.5-0.6 times the font size
@@ -118,13 +119,33 @@ const calculateOptimalFontSize = (text, maxWidth = 650, initialFontSize = 48, mi
118
119
  return finalSize;
119
120
  };
120
121
 
122
+ // Validate image URL before PDF generation
123
+ const validateImageUrl = async (url) => {
124
+ if (!url) return null;
125
+
126
+ try {
127
+ const response = await fetch(url, {
128
+ method: 'GET',
129
+ mode: 'cors'
130
+ });
131
+
132
+ if (response.ok) {
133
+ return url;
134
+ }
135
+ return null;
136
+ } catch (error) {
137
+ console.warn("Image validation failed:", error);
138
+ return null;
139
+ }
140
+ };
141
+
121
142
  const CertificatePDF = ({
122
143
  attendee,
123
144
  summit,
124
145
  settings,
125
- isCheckedIn = true
146
+ isCheckedIn = true,
147
+ logoUrl = null
126
148
  }) => {
127
-
128
149
  const role = attendee.role || "Attendee";
129
150
  const position = attendee.jobTitle || "";
130
151
  const company = attendee.company || "";
@@ -181,7 +202,7 @@ const CertificatePDF = ({
181
202
  },
182
203
  logo: {
183
204
  maxWidth: settings.logoWidth || 250,
184
- ...(settings.logoHeight && { maxHeight: settings.logoHeight }),
205
+ maxHeight: settings.logoHeight || 150,
185
206
  marginBottom: 25,
186
207
  objectFit: "contain",
187
208
  },
@@ -244,10 +265,11 @@ const CertificatePDF = ({
244
265
  <View style={styles.whiteCard}>
245
266
  <View style={styles.content}>
246
267
  {/* Logo */}
247
- {(settings.logo || summit.logo) && (
268
+ {logoUrl && (
248
269
  <Image
249
- src={settings.logo || summit.logo}
250
- style={styles.logo}
270
+ src={logoUrl}
271
+ style={styles.logo}
272
+ debug={false} // When true, shows a visible error placeholder if image fails to load
251
273
  />
252
274
  )}
253
275
 
@@ -292,7 +314,16 @@ const CertificatePDF = ({
292
314
  // helper function to generate and download the certificate
293
315
  export const generateCertificatePDF = async (attendee, summit, settings) => {
294
316
  try {
295
- const doc = <CertificatePDF attendee={attendee} summit={summit} settings={settings} />;
317
+ // Validate logo URL before generating PDF
318
+ const logoUrlToValidate = settings.logo || summit.logo;
319
+ const validatedLogoUrl = await validateImageUrl(logoUrlToValidate);
320
+
321
+ const doc = <CertificatePDF
322
+ attendee={attendee}
323
+ summit={summit}
324
+ settings={settings}
325
+ logoUrl={validatedLogoUrl}
326
+ />;
296
327
  const blob = await pdf(doc).toBlob();
297
328
 
298
329
  // create download link
@@ -22,7 +22,7 @@ const CertificateSection = ({
22
22
  const siteSettings = useSiteSettings();
23
23
 
24
24
  // Get certificate settings
25
- const certificateSettings = useCertificateSettings(summit, siteSettings?.siteFont);
25
+ const certificateSettings = useCertificateSettings(siteSettings?.siteFont);
26
26
 
27
27
  // Check if certificates are enabled
28
28
  const certificatesEnabled = getSettingByKey(MARKETING_SETTINGS_KEYS.certificateEnabled) !== DISPLAY_OPTIONS.hide;
@@ -107,27 +107,29 @@ const CertificateSection = ({
107
107
  };
108
108
 
109
109
  return (
110
- <div style={{ marginTop: '20px' }}>
111
- <button
112
- className="button is-large"
113
- onClick={() => handleDownloadCertificate(checkedInTickets[0])}
114
- style={{
115
- width: '100%',
116
- height: '5.5rem',
117
- color: 'var(--color_input_text_color)',
118
- backgroundColor: 'var(--color_input_background_color)',
119
- borderColor: 'var(--color_input_border_color)'
120
- }}
121
- >
122
- Download Certificate
123
- </button>
124
-
125
- {error && (
126
- <div style={{ color: '#d32f2f', fontSize: '14px', marginTop: '10px' }}>
127
- {error}
128
- </div>
129
- )}
130
- </div>
110
+ <>
111
+ <div style={{ marginTop: '20px' }}>
112
+ <button
113
+ className={`button is-large ${styles.certificateButton}`}
114
+ onClick={() => handleDownloadCertificate(checkedInTickets[0])}
115
+ style={{
116
+ width: '100%',
117
+ height: '5.5rem',
118
+ color: 'var(--color_input_text_color)',
119
+ backgroundColor: 'var(--color_input_background_color)',
120
+ borderColor: 'var(--color_input_border_color)'
121
+ }}
122
+ >
123
+ Download Certificate of Attendance
124
+ </button>
125
+
126
+ {error && (
127
+ <div style={{ color: '#d32f2f', fontSize: '14px', marginTop: '10px' }}>
128
+ {error}
129
+ </div>
130
+ )}
131
+ </div>
132
+ </>
131
133
  );
132
134
  };
133
135
 
@@ -161,4 +161,9 @@
161
161
  left: 47%;
162
162
  }
163
163
  }
164
+ }
165
+
166
+ // Certificate button focus state fix
167
+ .certificateButton:focus {
168
+ color: var(--color_input_text_color) !important;
164
169
  }
@@ -15,6 +15,10 @@ class AblyRealTimeStrategy extends AbstractRealTimeStrategy {
15
15
  console.log('AblyRealTimeStrategy::constructor');
16
16
  this._client = null;
17
17
  this._wsError = false;
18
+ this._closing = false;
19
+ this._channel = null;
20
+ this._onConn = null;
21
+ this._onMessage = null;
18
22
  }
19
23
 
20
24
  /**
@@ -29,12 +33,12 @@ class AblyRealTimeStrategy extends AbstractRealTimeStrategy {
29
33
  const key = getEnvVariable(ABLY_API_KEY);
30
34
 
31
35
  if(this._wsError) {
32
- console.log('AblyRealTimeStrategy::create error state');
36
+ console.warn('AblyRealTimeStrategy::create error state');
33
37
  return;
34
38
  }
35
39
 
36
40
  if(!key){
37
- console.log('AblyRealTimeStrategy::create ABLY_KEY is not set');
41
+ console.warn('AblyRealTimeStrategy::create ABLY_KEY is not set');
38
42
  this._wsError = true;
39
43
  return;
40
44
  }
@@ -50,22 +54,17 @@ class AblyRealTimeStrategy extends AbstractRealTimeStrategy {
50
54
  this._client.close();
51
55
  }
52
56
 
53
- this._client = new Ably.Realtime({ key });
54
-
55
- // start listening for event
56
-
57
- const channel = this._client.channels.get(`${summitId}:*:*`);
58
-
59
- channel.subscribe((message) => {
60
- const { data : payload } = message;
61
- console.log('AblyRealTimeStrategy::create Change received', payload)
62
- this._callback(payload);
57
+ this._client = new Ably.Realtime({
58
+ key,
59
+ // see https://faqs.ably.com/ably-js-page-unload-behaviour
60
+ closeOnUnload: false,
63
61
  });
64
62
 
65
63
  // connect handler
66
- this._client.connection.on((stateChange) => {
67
- const { current: state } = stateChange;
68
- console.log(`AblyRealTimeStrategy::connection WS ${state}`);
64
+
65
+ this._onConn = (stateChange) => {
66
+ const { current: state, reason } = stateChange;
67
+ console.log(`AblyRealTimeStrategy::connection WS ${state}`, reason || '');
69
68
  if(state === 'connected') {
70
69
  this._wsError = false;
71
70
  // RELOAD
@@ -76,29 +75,80 @@ class AblyRealTimeStrategy extends AbstractRealTimeStrategy {
76
75
  this.stopUsingFallback();
77
76
  return;
78
77
  }
79
- if(state === 'suspended') {
78
+
79
+ if ((state === 'suspended' || state === 'failed') && !this._closing) {
80
80
  if(!this._wsError) {
81
81
  this._wsError = true;
82
82
  this.startUsingFallback(summitId);
83
83
  }
84
84
  return;
85
85
  }
86
- if(state === 'failed') {
87
- if(!this._wsError) {
88
- this._wsError = true;
89
- this.startUsingFallback(summitId);
90
- }
91
- return;
86
+ if (state === 'closed') {
87
+ // Expected on unmount, don’t start fallback, don’t log as error
88
+ this._wsError = false;
92
89
  }
93
- });
90
+ };
91
+
92
+ this._client.connection.on(this._onConn);
93
+
94
+ // start listening for event
95
+
96
+ this._channel = this._client.channels.get(`${summitId}:*:*`);
97
+
98
+ this._onMessage = (message) => {
99
+ try {
100
+ const {data: payload} = message;
101
+ console.log('AblyRealTimeStrategy::create Change received', payload)
102
+ this._callback(payload);
103
+ }
104
+ catch (e) {
105
+ console.error('AblyRealTimeStrategy::message handler failed', e);
106
+ }
107
+ };
108
+
109
+ this._channel.subscribe(this._onMessage);
110
+
94
111
  }
95
112
 
96
113
  close() {
114
+ console.log("AblyRealTimeStrategy::close");
97
115
  super.close();
98
- if(this._client){
99
- console.log("AblyRealTimeStrategy::close");
100
- this._client.close();
116
+ this._closing = true;
117
+ try { this.stopUsingFallback(); } catch {}
118
+
119
+ try { this._onMessage && this._channel?.unsubscribe(this._onMessage); }
120
+ catch (e){ console.warn('AblyRealTimeStrategy::close channel.unsubscribe',e); }
121
+ try { this._channel?.off(); }
122
+ catch(e) { console.warn('AblyRealTimeStrategy::close channel.off', e);}
123
+ try { this._onConn && this._client?.connection.off(this._onConn); }
124
+ catch(e) { console.warn('AblyRealTimeStrategy::close AblyRealTimeStrategy::close.off', e); }
125
+ this._onMessage = null;
126
+ this._onConn = null;
127
+
128
+ const client = this._client;
129
+ const channel = this._channel;
130
+
131
+ const tryRelease = () => {
132
+ try {
133
+ const releasable = ['initialized','detached','failed'].includes(channel?.state);
134
+ if (releasable) client?.channels.release(channel.name);
135
+ } catch {}
136
+ try { client?.close(); } catch(e) {
137
+ console.warn('AblyRealTimeStrategy::close client.close', e);
138
+ }
101
139
  this._client = null;
140
+ this._channel = null;
141
+ this._closing = false;
142
+ this._wsError = false;
143
+ };
144
+
145
+ if (client && channel && client.connection.state !== 'closed' &&
146
+ ['attached','attaching','detaching'].includes(channel.state)) {
147
+ // Detach asynchronously, then release if allowed
148
+ channel.detach(() => tryRelease());
149
+ } else {
150
+ // Already detached/failed/closed (or no channel) → just release if possible and close
151
+ tryRelease();
102
152
  }
103
153
  }
104
154
  }
@@ -5,6 +5,8 @@ import { isString } from "lodash";
5
5
  import { getEnvVariable, SCHEDULE_EXCLUDING_TAGS } from "./envVariables";
6
6
  import {getUserAccessLevelIds, isAuthorizedUser} from './authorizedGroups';
7
7
  import {uniq} from "lodash";
8
+ import * as Sentry from "@sentry/react";
9
+
8
10
 
9
11
  const groupByDay = (events) => {
10
12
  let groupedEvents = [];
@@ -61,13 +63,15 @@ export const filterEventsByTags = (events) => {
61
63
 
62
64
  export const filterEventsByTicket = (events, user) => {
63
65
  const assignedTickets = user?.summit_tickets || [];
64
- const ticketTypeIds = uniq(assignedTickets.map(t => t.ticket_type?.id));
66
+ const ticketTypeIds = uniq(assignedTickets?.map(t => t?.ticket_type?.id));
65
67
 
66
68
  return events.filter(ev => {
67
- const hasEventRestriction = ev.allowed_ticket_types.length > 0;
68
- const typeAllowed = ev.type.allowed_ticket_types.length === 0 || ev.type.allowed_ticket_types.some(att => ticketTypeIds.includes(att));
69
+ const hasEventRestriction = ev?.allowed_ticket_types?.length > 0;
70
+ if(!ev?.allowed_ticket_types){
71
+ Sentry?.captureMessage(`event ${ev.id} has not set allowed_ticket_types collection`);
72
+ }
73
+ const typeAllowed = ev?.type?.allowed_ticket_types.length === 0 || ev?.type?.allowed_ticket_types.some(att => ticketTypeIds.includes(att));
69
74
  const eventAllowed = !hasEventRestriction || ev.allowed_ticket_types.some(att => ticketTypeIds.includes(att));
70
-
71
75
  return hasEventRestriction ? eventAllowed : typeAllowed;
72
76
  });
73
77
  };