@openeventkit/event-site 2.1.18 → 2.1.21

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.
Files changed (86) hide show
  1. package/.github/workflows/jest.yml +1 -1
  2. package/babel.config.json +9 -9
  3. package/gatsby-node.js +67 -125
  4. package/jest.setup.js +2 -0
  5. package/netlify.toml +1 -1
  6. package/package.json +25 -16
  7. package/src/__mocks__/@mdx-js/mdx.js +32 -0
  8. package/src/__mocks__/@mdx-js/react.js +15 -0
  9. package/src/__mocks__/rehype-external-links.js +3 -0
  10. package/src/__mocks__/remark-gfm.js +3 -0
  11. package/src/actions/fetch-entities-actions.js +45 -87
  12. package/src/actions/update-data-actions.js +2 -2
  13. package/src/actions/user-actions.js +578 -430
  14. package/src/cms/config/collections/configurationsCollection/siteSettings/index.js +2 -0
  15. package/src/cms/config/collections/configurationsCollection/siteSettings/typeDefs.js +10 -0
  16. package/src/cms/preview-templates/ContentPagePreview.js +27 -29
  17. package/src/components/AvatarEditorModal/index.js +10 -0
  18. package/src/components/CertificatePDF.js +313 -0
  19. package/src/components/CertificateSection.js +139 -0
  20. package/src/components/FullSchedule.js +83 -66
  21. package/src/components/Mdx.js +39 -0
  22. package/src/components/__tests__/Mdx.test.jsx +70 -0
  23. package/src/content/site-settings/index.json +1 -1
  24. package/src/content/sponsors.json +1 -1
  25. package/src/i18n/locales/en.json +9 -1
  26. package/src/pages/a/[...].js +3 -0
  27. package/src/reducers/user-reducer.js +89 -27
  28. package/src/routes/authorization-callback-route.js +20 -2
  29. package/src/styles/rsvp-page.module.scss +63 -0
  30. package/src/templates/full-profile-page.js +61 -2
  31. package/src/templates/marketing-page-template/MainColumn.js +40 -42
  32. package/src/templates/rsvp-page.js +144 -0
  33. package/src/utils/alerts.js +1 -1
  34. package/src/utils/build-json/BaseAPIRequest.js +25 -0
  35. package/src/utils/build-json/EventsAPIRequest.js +171 -0
  36. package/src/utils/build-json/SpeakersAPIRequest.js +62 -0
  37. package/src/utils/build-json/SummitAPIRequest.js +115 -0
  38. package/src/utils/build-json/constants.js +5 -0
  39. package/src/utils/certificateSettings.js +45 -0
  40. package/src/utils/customErrorHandler.js +40 -1
  41. package/src/utils/rsvpConstants.js +7 -0
  42. package/src/utils/useMarketingSettings.js +48 -1
  43. package/src/utils/useSiteSettings.js +11 -0
  44. package/src/workers/feeds.worker.js +85 -90
  45. package/src/workers/sync_strategies/activity_synch_strategy.js +147 -102
  46. package/src/workers/sync_strategies/speaker_synch_strategy.js +3 -3
  47. package/src/workers/sync_strategies/track_synch_strategy.js +149 -48
  48. package/src/workers/synch.worker.js +123 -88
  49. package/static/fonts/fonts.css +120 -20
  50. package/static/fonts/nunito-sans/nunito-sans-v18-latin-200.woff2 +0 -0
  51. package/static/fonts/nunito-sans/nunito-sans-v18-latin-200italic.ttf +0 -0
  52. package/static/fonts/nunito-sans/nunito-sans-v18-latin-200italic.woff2 +0 -0
  53. package/static/fonts/nunito-sans/nunito-sans-v18-latin-300.woff2 +0 -0
  54. package/static/fonts/nunito-sans/nunito-sans-v18-latin-300italic.ttf +0 -0
  55. package/static/fonts/nunito-sans/nunito-sans-v18-latin-300italic.woff2 +0 -0
  56. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400.ttf +0 -0
  57. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400.woff2 +0 -0
  58. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400italic.ttf +0 -0
  59. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400italic.woff2 +0 -0
  60. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500.ttf +0 -0
  61. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500.woff2 +0 -0
  62. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500italic.ttf +0 -0
  63. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500italic.woff2 +0 -0
  64. package/static/fonts/nunito-sans/nunito-sans-v18-latin-600.woff2 +0 -0
  65. package/static/fonts/nunito-sans/nunito-sans-v18-latin-600italic.woff2 +0 -0
  66. package/static/fonts/nunito-sans/nunito-sans-v18-latin-700.ttf +0 -0
  67. package/static/fonts/nunito-sans/nunito-sans-v18-latin-700.woff2 +0 -0
  68. package/static/fonts/nunito-sans/nunito-sans-v18-latin-700italic.woff2 +0 -0
  69. package/static/fonts/nunito-sans/nunito-sans-v18-latin-800.ttf +0 -0
  70. package/static/fonts/nunito-sans/nunito-sans-v18-latin-800.woff2 +0 -0
  71. package/static/fonts/nunito-sans/nunito-sans-v18-latin-800italic.woff2 +0 -0
  72. package/static/fonts/nunito-sans/nunito-sans-v18-latin-900.ttf +0 -0
  73. package/static/fonts/nunito-sans/nunito-sans-v18-latin-900.woff2 +0 -0
  74. package/static/fonts/nunito-sans/nunito-sans-v18-latin-900italic.woff2 +0 -0
  75. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300.woff +0 -0
  76. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300.woff2 +0 -0
  77. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300italic.woff +0 -0
  78. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300italic.woff2 +0 -0
  79. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600.woff +0 -0
  80. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600.woff2 +0 -0
  81. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600italic.woff +0 -0
  82. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600italic.woff2 +0 -0
  83. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700.woff +0 -0
  84. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700.woff2 +0 -0
  85. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700italic.woff +0 -0
  86. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700italic.woff2 +0 -0
@@ -148,6 +148,7 @@ const siteSettings = {
148
148
  selectField({
149
149
  label: "Font Format",
150
150
  name: "fontFormat",
151
+ hint: "TTF format is required for certificate PDF generation",
151
152
  multiple: false,
152
153
  required: false,
153
154
  options: mapObjectToSelectOptions(FONT_FORMATS)
@@ -167,6 +168,7 @@ const siteSettings = {
167
168
  selectField({
168
169
  label: "Font Format",
169
170
  name: "fontFormat",
171
+ hint: "TTF format is required for certificate PDF generation",
170
172
  multiple: false,
171
173
  required: false,
172
174
  options: mapObjectToSelectOptions(FONT_FORMATS)
@@ -8,6 +8,15 @@ module.exports = `
8
8
  type Favicon {
9
9
  asset: File @fileByRelativePath
10
10
  }
11
+ type FontFile {
12
+ fontFile: String
13
+ fontFormat: String
14
+ }
15
+ type SiteFont {
16
+ fontFamily: String
17
+ regularFont: FontFile
18
+ boldFont: FontFile
19
+ }
11
20
  type Schedule {
12
21
  allowClick: Boolean
13
22
  }
@@ -42,6 +51,7 @@ module.exports = `
42
51
  type SiteSettingsJson implements Node {
43
52
  siteMetadata: SiteMetadata
44
53
  favicon: Favicon
54
+ siteFont: SiteFont
45
55
  widgets: Widgets
46
56
  idpLogo: IdpLogo
47
57
  identityProviderButtons: [IdentityProviderButton]
@@ -1,49 +1,47 @@
1
1
  import React from "react";
2
2
  import PropTypes from "prop-types";
3
- import Mdx from "@mdx-js/runtime";
4
3
  import ContentPageTemplate from "../../templates/content-page/template";
5
4
  import shortcodes from "../../templates/content-page/shortcodes";
5
+ import Mdx from "../../components/Mdx";
6
6
 
7
7
  // function to transform content by replacing relative image URLs with absolute ones
8
8
  const transformContent = (mdx, getAsset) => {
9
- // regex to identify Markdown image tags ![alt](url)
10
- const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
9
+ // regex to identify Markdown image tags ![alt](url)
10
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
11
11
 
12
- return mdx.replace(imageRegex, (match, alt, url) => {
13
- // check if the URL is relative (does not start with http:// or https://)
14
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
15
- const asset = getAsset(url);
16
- if (asset && asset.url) {
17
- return `![${alt}](${asset.url})`;
18
- }
19
- }
20
- return match; // return the original match if it's already an absolute URL
21
- });
12
+ return mdx.replace(imageRegex, (match, alt, url) => {
13
+ // check if the URL is relative (does not start with http:// or https://)
14
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
15
+ const asset = getAsset(url);
16
+ if (asset && asset.url) {
17
+ return `![${alt}](${asset.url})`;
18
+ }
19
+ }
20
+ return match; // return the original match if it's already an absolute URL
21
+ });
22
22
  };
23
23
 
24
24
  // function to render transformed content with Mdx
25
25
  const renderContent = (mdx, getAsset) => (
26
- <Mdx components={shortcodes}>
27
- {transformContent(mdx, getAsset)}
28
- </Mdx>
26
+ <Mdx shortcodes={shortcodes} content={transformContent(mdx, getAsset)}/>
29
27
  );
30
28
 
31
29
  const ContentPagePreview = ({ entry, getAsset }) => {
32
- const title = entry.getIn(["data", "title"]);
33
- const body = entry.getIn(["data", "body"]);
34
- return (
35
- <ContentPageTemplate
36
- title={title}
37
- content={renderContent(body, getAsset)}
38
- />
39
- );
30
+ const title = entry.getIn(["data", "title"]);
31
+ const body = entry.getIn(["data", "body"]);
32
+ return (
33
+ <ContentPageTemplate
34
+ title={title}
35
+ content={renderContent(body, getAsset)}
36
+ />
37
+ );
40
38
  };
41
39
 
42
40
  ContentPagePreview.propTypes = {
43
- entry: PropTypes.shape({
44
- getIn: PropTypes.func.isRequired
45
- }).isRequired,
46
- getAsset: PropTypes.func.isRequired
41
+ entry: PropTypes.shape({
42
+ getIn: PropTypes.func.isRequired
43
+ }).isRequired,
44
+ getAsset: PropTypes.func.isRequired
47
45
  };
48
46
 
49
- export default ContentPagePreview;
47
+ export default ContentPagePreview;
@@ -51,6 +51,7 @@ const AvatarUploadButton = ({
51
51
  const AvatarEditorContent = ({
52
52
  editorRef,
53
53
  image,
54
+ newImageSelected,
54
55
  onUpload,
55
56
  handleSave,
56
57
  handleClose
@@ -60,6 +61,12 @@ const AvatarEditorContent = ({
60
61
  const [rotate, setRotate] = useState(0);
61
62
  const [newImage, setNewImage] = useState(false);
62
63
 
64
+ useEffect(() => {
65
+ if (newImageSelected) {
66
+ setNewImage(true);
67
+ }
68
+ }, [newImageSelected]);
69
+
63
70
  const handleScale = (e, newValue) => {
64
71
  setScale(newValue);
65
72
  setNewImage(true);
@@ -203,6 +210,7 @@ const AvatarEditorModal = ({
203
210
  const fileInputRef = useRef(null);
204
211
 
205
212
  const [image, setImage] = useState(null);
213
+ const [newImageSelected, setNewImageSelected] = useState(false);
206
214
  const [loadingPicture, setLoadingPicture] = useState(false);
207
215
  const [fetchError, setFetchError] = useState(false);
208
216
 
@@ -223,6 +231,7 @@ const AvatarEditorModal = ({
223
231
 
224
232
  const handleNewImage = (e) => {
225
233
  setImage(e.target.files[0]);
234
+ setNewImageSelected(true);
226
235
  setFetchError(false);
227
236
  };
228
237
 
@@ -340,6 +349,7 @@ const AvatarEditorModal = ({
340
349
  <AvatarEditorContent
341
350
  editorRef={editorRef}
342
351
  image={image}
352
+ newImageSelected={newImageSelected}
343
353
  onUpload={() => fileInputRef.current.click()}
344
354
  handleSave={handleSave}
345
355
  handleClose={handleClose}
@@ -0,0 +1,313 @@
1
+ import React from "react";
2
+ import { Document, Page, Text, View, Image, StyleSheet, Font, pdf } from "@react-pdf/renderer";
3
+
4
+ import fontRegular from "../../static/fonts/nunito-sans/nunito-sans-v18-latin-400.ttf";
5
+ import fontBold from "../../static/fonts/nunito-sans/nunito-sans-v18-latin-700.ttf";
6
+
7
+ const registerDefaultFont = () => {
8
+ try {
9
+ Font.register({
10
+ family: "Nunito Sans",
11
+ fonts: [
12
+ {
13
+ src: fontRegular,
14
+ fontWeight: "normal"
15
+ },
16
+ {
17
+ src: fontBold,
18
+ fontWeight: "bold"
19
+ },
20
+ ]
21
+ });
22
+ return true;
23
+ } catch (error) {
24
+ console.error("Failed to register default font:", error);
25
+ return false;
26
+ }
27
+ };
28
+
29
+ // helper to convert relative font paths to absolute URLs
30
+ const getFontUrl = (fontPath) => {
31
+ if (!fontPath) return null;
32
+
33
+ if (fontPath.startsWith("http://") || fontPath.startsWith("https://")) {
34
+ return fontPath;
35
+ }
36
+
37
+ // For relative paths, prepend the origin
38
+ // Site settings fonts are served from /fonts/ (not /static/fonts/)
39
+ if (typeof window !== "undefined") {
40
+ // Remove /static prefix if present since fonts are served at root /fonts/
41
+ const cleanPath = fontPath.replace("/static/fonts/", "/fonts/");
42
+ return `${window.location.origin}${cleanPath}`;
43
+ }
44
+
45
+ return fontPath;
46
+ };
47
+
48
+ // register custom font from site settings if available
49
+ const registerCustomFont = (siteFont) => {
50
+ if (!siteFont || !siteFont.fontFamily || !siteFont.regularFont || !siteFont.boldFont) {
51
+ return false;
52
+ }
53
+
54
+ const fonts = [];
55
+
56
+ if (siteFont.regularFont.fontFile && siteFont.regularFont.fontFormat === "ttf") {
57
+ const fontUrl = getFontUrl(siteFont.regularFont.fontFile);
58
+ if (fontUrl) {
59
+ fonts.push({
60
+ src: fontUrl,
61
+ fontWeight: "normal"
62
+ });
63
+ }
64
+ }
65
+
66
+ if (siteFont.boldFont.fontFile && siteFont.boldFont.fontFormat === "ttf") {
67
+ const fontUrl = getFontUrl(siteFont.boldFont.fontFile);
68
+ if (fontUrl) {
69
+ fonts.push({
70
+ src: fontUrl,
71
+ fontWeight: "bold"
72
+ });
73
+ }
74
+ }
75
+
76
+ if (fonts.length > 0) {
77
+ try {
78
+ Font.register({
79
+ family: siteFont.fontFamily,
80
+ fonts: fonts
81
+ });
82
+ return true;
83
+ } catch (error) {
84
+ console.warn("Failed to register custom font:", error);
85
+ return false;
86
+ }
87
+ }
88
+
89
+ return false;
90
+ };
91
+
92
+ const calculateOptimalFontSize = (text, maxWidth = 650, initialFontSize = 48, minFontSize = 24) => {
93
+ // estimate average character width based on font size
94
+ // for most fonts, character width is roughly 0.5-0.6 times the font size
95
+ const avgCharWidthRatio = 0.55;
96
+
97
+ // start with initial font size
98
+ let fontSize = initialFontSize;
99
+
100
+ // calculate the approximate text width
101
+ const estimatedWidth = text.length * fontSize * avgCharWidthRatio;
102
+
103
+ // if text fits within maxWidth, use initial font size
104
+ if (estimatedWidth <= maxWidth) {
105
+ return fontSize;
106
+ }
107
+
108
+ // calculate optimal font size to fit within maxWidth
109
+ fontSize = Math.floor(maxWidth / (text.length * avgCharWidthRatio));
110
+
111
+ // ensure font size is within bounds
112
+ fontSize = Math.max(minFontSize, Math.min(initialFontSize, fontSize));
113
+
114
+ // round to common font sizes for better appearance
115
+ const commonSizes = [48, 44, 40, 36, 32, 28, 24];
116
+ const finalSize = commonSizes.find(size => size <= fontSize) || minFontSize;
117
+
118
+ return finalSize;
119
+ };
120
+
121
+ const CertificatePDF = ({
122
+ attendee,
123
+ summit,
124
+ settings,
125
+ isCheckedIn = true
126
+ }) => {
127
+
128
+ const role = attendee.role || "Attendee";
129
+ const position = attendee.jobTitle || "";
130
+ const company = attendee.company || "";
131
+
132
+ const fullName = `${attendee.firstName} ${attendee.lastName}`;
133
+
134
+ const nameFontSize = calculateOptimalFontSize(fullName);
135
+
136
+
137
+ let fontFamily = "Nunito Sans";
138
+
139
+ if (settings?.siteFont && settings.siteFont.fontFamily) {
140
+ const customFontRegistered = registerCustomFont(settings.siteFont);
141
+ if (customFontRegistered) {
142
+ fontFamily = settings.siteFont.fontFamily;
143
+ } else {
144
+ registerDefaultFont();
145
+ }
146
+ } else {
147
+ registerDefaultFont();
148
+ }
149
+
150
+ const styles = StyleSheet.create({
151
+ page: {
152
+ width: settings.width || "11in",
153
+ height: settings.height || "8.5in",
154
+ backgroundColor: settings.mainColor || settings.colorAccent || "#ff5e32",
155
+ fontFamily: fontFamily,
156
+ display: "flex",
157
+ alignItems: "center",
158
+ justifyContent: "center",
159
+ padding: 40,
160
+ },
161
+ whiteCard: {
162
+ width: "100%",
163
+ maxWidth: 680,
164
+ height: "100%",
165
+ maxHeight: 502,
166
+ backgroundColor: "#ffffff",
167
+ borderRadius: 4,
168
+ display: "flex",
169
+ alignItems: "center",
170
+ justifyContent: "center",
171
+ },
172
+ content: {
173
+ position: "relative",
174
+ width: "100%",
175
+ height: "100%",
176
+ padding: 60,
177
+ display: "flex",
178
+ flexDirection: "column",
179
+ alignItems: "center",
180
+ justifyContent: "center",
181
+ },
182
+ logo: {
183
+ maxWidth: settings.logoWidth || 250,
184
+ ...(settings.logoHeight && { maxHeight: settings.logoHeight }),
185
+ marginBottom: 25,
186
+ objectFit: "contain",
187
+ },
188
+ title: {
189
+ fontSize: 20,
190
+ fontWeight: "normal",
191
+ color: "#000000",
192
+ textAlign: "center",
193
+ letterSpacing: 0.15,
194
+ lineHeight: "160%",
195
+ },
196
+ summitName: {
197
+ fontSize: 24,
198
+ fontWeight: "bold",
199
+ color: settings.mainColor || settings.colorAccent || "#ff5e32",
200
+ textAlign: "center",
201
+ textTransform: "uppercase",
202
+ letterSpacing: 0,
203
+ lineHeight: "133%",
204
+ },
205
+ name: {
206
+ fontSize: nameFontSize,
207
+ fontWeight: "normal",
208
+ color: "#000000",
209
+ textAlign: "center",
210
+ letterSpacing: 0,
211
+ lineHeight: "117%",
212
+ marginTop: 36,
213
+ },
214
+ nameUnderline: {
215
+ width: 500,
216
+ height: 1.2,
217
+ backgroundColor: "#000000",
218
+ marginTop: 8,
219
+ marginBottom: 20,
220
+ },
221
+ details: {
222
+ fontSize: 14,
223
+ fontWeight: "normal",
224
+ color: "#000000",
225
+ textAlign: "center",
226
+ letterSpacing: 0.4,
227
+ lineHeight: "166%",
228
+ },
229
+ role: {
230
+ fontSize: 14,
231
+ fontWeight: "normal",
232
+ color: "#000000",
233
+ textAlign: "center",
234
+ letterSpacing: 0.4,
235
+ lineHeight: "166%",
236
+ marginTop: 6,
237
+ },
238
+ });
239
+
240
+ return (
241
+ <Document>
242
+ <Page size="LETTER" orientation="landscape" style={styles.page}>
243
+ {/* White Card Container */}
244
+ <View style={styles.whiteCard}>
245
+ <View style={styles.content}>
246
+ {/* Logo */}
247
+ {(settings.logo || summit.logo) && (
248
+ <Image
249
+ src={settings.logo || summit.logo}
250
+ style={styles.logo}
251
+ />
252
+ )}
253
+
254
+ {/* Title */}
255
+ <Text style={styles.title}>
256
+ {settings.titleText || "CERTIFICATE OF ATTENDANCE"}
257
+ </Text>
258
+
259
+ {/* Event Name */}
260
+ <Text style={styles.summitName}>
261
+ {settings.summitName || summit.name || "EVENT NAME"}
262
+ </Text>
263
+
264
+ {/* Attendee Name */}
265
+ <Text style={styles.name}>
266
+ {fullName}
267
+ </Text>
268
+
269
+ {/* Underline */}
270
+ <View style={styles.nameUnderline} />
271
+
272
+ {/* Position and Company */}
273
+ {(position || company) && (
274
+ <Text style={styles.details}>
275
+ {position}{position && company ? ", " : ""}{company}
276
+ </Text>
277
+ )}
278
+
279
+ {/* Role */}
280
+ {settings.showRole && (
281
+ <Text style={styles.role}>
282
+ {role}
283
+ </Text>
284
+ )}
285
+ </View>
286
+ </View>
287
+ </Page>
288
+ </Document>
289
+ );
290
+ };
291
+
292
+ // helper function to generate and download the certificate
293
+ export const generateCertificatePDF = async (attendee, summit, settings) => {
294
+ try {
295
+ const doc = <CertificatePDF attendee={attendee} summit={summit} settings={settings} />;
296
+ const blob = await pdf(doc).toBlob();
297
+
298
+ // create download link
299
+ const url = URL.createObjectURL(blob);
300
+ const link = document.createElement("a");
301
+ link.href = url;
302
+ link.download = `certificate-${attendee.firstName}-${attendee.lastName}.pdf`.toLowerCase().replace(/\s+/g, "-");
303
+ document.body.appendChild(link);
304
+ link.click();
305
+ document.body.removeChild(link);
306
+ URL.revokeObjectURL(url);
307
+ } catch (error) {
308
+ console.error("Error generating certificate PDF:", error);
309
+ throw error;
310
+ }
311
+ };
312
+
313
+ export default CertificatePDF;
@@ -0,0 +1,139 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { connect } from 'react-redux';
3
+ import { generateCertificatePDF } from './CertificatePDF';
4
+ import { useCertificateSettings } from '../utils/certificateSettings';
5
+ import useMarketingSettings from '../utils/useMarketingSettings';
6
+ import { MARKETING_SETTINGS_KEYS, DISPLAY_OPTIONS } from '../utils/useMarketingSettings';
7
+ import useSiteSettings from '../utils/useSiteSettings';
8
+ import styles from '../styles/full-profile.module.scss';
9
+
10
+ const USER_ROLES = {
11
+ SPEAKER: 'Speaker',
12
+ ATTENDEE: 'Attendee'
13
+ };
14
+
15
+ const CertificateSection = ({
16
+ user,
17
+ summit,
18
+ freshTickets = []
19
+ }) => {
20
+ const [error, setError] = useState(null);
21
+ const { getSettingByKey } = useMarketingSettings();
22
+ const siteSettings = useSiteSettings();
23
+
24
+ // Get certificate settings
25
+ const certificateSettings = useCertificateSettings(summit, siteSettings?.siteFont);
26
+
27
+ // Check if certificates are enabled
28
+ const certificatesEnabled = getSettingByKey(MARKETING_SETTINGS_KEYS.certificateEnabled) !== DISPLAY_OPTIONS.hide;
29
+
30
+ if (!certificatesEnabled) {
31
+ return null;
32
+ }
33
+
34
+ // Filter tickets that are checked in
35
+ const checkedInTickets = freshTickets.filter(ticket => {
36
+ const isCheckedIn = ticket.owner?.summit_hall_checked_in === true;
37
+ const isValidTicket =
38
+ ticket.status !== 'Cancelled' &&
39
+ ticket.status !== 'RefundRequested' &&
40
+ ticket.owner !== null;
41
+
42
+ return isCheckedIn && isValidTicket;
43
+ });
44
+
45
+ if (checkedInTickets.length === 0) {
46
+ return (
47
+ <div>
48
+ <h3 className={styles.header}>Certificate of Attendance</h3>
49
+ <div style={{ padding: '15px 0', color: '#666' }}>
50
+ Check-in required to download certificate.
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ // Get user role from tickets/badges - prioritize Speaker role across all tickets
57
+ const getUserRole = (allTickets) => {
58
+ // Check if any ticket has speaker role
59
+ const hasSpeakerTicket = allTickets.some(ticket => {
60
+ // Check badge type name for speaker
61
+ const badgeType = summit?.badge_types?.find(bt => bt.id === ticket.badge?.type_id);
62
+ if (badgeType?.name?.toLowerCase().includes('speaker')) {
63
+ return true;
64
+ }
65
+
66
+ // Check badge features for speaker
67
+ const featureIds = ticket.badge?.features || [];
68
+ const badgeFeaturesTypes = summit?.badge_features_types || [];
69
+
70
+ return featureIds.some(featureId => {
71
+ const feature = badgeFeaturesTypes.find(f => f.id === featureId);
72
+ return feature?.name?.toLowerCase().includes('speaker');
73
+ });
74
+ });
75
+
76
+ return hasSpeakerTicket ? USER_ROLES.SPEAKER : USER_ROLES.ATTENDEE;
77
+ };
78
+
79
+ // Determine the user's role across all tickets (prioritize Speaker)
80
+ const userRole = getUserRole(checkedInTickets);
81
+
82
+ const handleDownloadCertificate = async (ticket) => {
83
+ setError(null);
84
+
85
+ try {
86
+ const attendeeData = {
87
+ firstName: user.first_name || user.given_name || ticket.owner?.first_name,
88
+ lastName: user.last_name || user.family_name || ticket.owner?.last_name,
89
+ company: user.company || ticket.owner?.company || '',
90
+ jobTitle: user.job_title || user.jobTitle || '',
91
+ email: user.email || ticket.owner?.email,
92
+ role: userRole // Use the prioritized role across all tickets
93
+ };
94
+
95
+ const summitData = {
96
+ name: summit.name,
97
+ logo: summit.logo,
98
+ start_date: summit.start_date,
99
+ end_date: summit.end_date
100
+ };
101
+
102
+ await generateCertificatePDF(attendeeData, summitData, certificateSettings);
103
+ } catch (err) {
104
+ console.error('Error downloading certificate:', err);
105
+ setError('Failed to download certificate. Please try again.');
106
+ }
107
+ };
108
+
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>
131
+ );
132
+ };
133
+
134
+ const mapStateToProps = ({ summitState, userState }) => ({
135
+ summit: summitState.summit,
136
+ user: userState.userProfile || userState.idpProfile
137
+ });
138
+
139
+ export default connect(mapStateToProps)(CertificateSection);