@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.
- package/.github/workflows/jest.yml +1 -1
- package/babel.config.json +9 -9
- package/gatsby-node.js +67 -125
- package/jest.setup.js +2 -0
- package/netlify.toml +1 -1
- package/package.json +25 -16
- package/src/__mocks__/@mdx-js/mdx.js +32 -0
- package/src/__mocks__/@mdx-js/react.js +15 -0
- package/src/__mocks__/rehype-external-links.js +3 -0
- package/src/__mocks__/remark-gfm.js +3 -0
- package/src/actions/fetch-entities-actions.js +45 -87
- package/src/actions/update-data-actions.js +2 -2
- package/src/actions/user-actions.js +578 -430
- package/src/cms/config/collections/configurationsCollection/siteSettings/index.js +2 -0
- package/src/cms/config/collections/configurationsCollection/siteSettings/typeDefs.js +10 -0
- package/src/cms/preview-templates/ContentPagePreview.js +27 -29
- package/src/components/AvatarEditorModal/index.js +10 -0
- package/src/components/CertificatePDF.js +313 -0
- package/src/components/CertificateSection.js +139 -0
- package/src/components/FullSchedule.js +83 -66
- package/src/components/Mdx.js +39 -0
- package/src/components/__tests__/Mdx.test.jsx +70 -0
- package/src/content/site-settings/index.json +1 -1
- package/src/content/sponsors.json +1 -1
- package/src/i18n/locales/en.json +9 -1
- package/src/pages/a/[...].js +3 -0
- package/src/reducers/user-reducer.js +89 -27
- package/src/routes/authorization-callback-route.js +20 -2
- package/src/styles/rsvp-page.module.scss +63 -0
- package/src/templates/full-profile-page.js +61 -2
- package/src/templates/marketing-page-template/MainColumn.js +40 -42
- package/src/templates/rsvp-page.js +144 -0
- package/src/utils/alerts.js +1 -1
- package/src/utils/build-json/BaseAPIRequest.js +25 -0
- package/src/utils/build-json/EventsAPIRequest.js +171 -0
- package/src/utils/build-json/SpeakersAPIRequest.js +62 -0
- package/src/utils/build-json/SummitAPIRequest.js +115 -0
- package/src/utils/build-json/constants.js +5 -0
- package/src/utils/certificateSettings.js +45 -0
- package/src/utils/customErrorHandler.js +40 -1
- package/src/utils/rsvpConstants.js +7 -0
- package/src/utils/useMarketingSettings.js +48 -1
- package/src/utils/useSiteSettings.js +11 -0
- package/src/workers/feeds.worker.js +85 -90
- package/src/workers/sync_strategies/activity_synch_strategy.js +147 -102
- package/src/workers/sync_strategies/speaker_synch_strategy.js +3 -3
- package/src/workers/sync_strategies/track_synch_strategy.js +149 -48
- package/src/workers/synch.worker.js +123 -88
- package/static/fonts/fonts.css +120 -20
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-200.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-200italic.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-200italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-300.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-300italic.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-300italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-400.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-400.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-400italic.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-400italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-500.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-500.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-500italic.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-500italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-600.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-600italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-700.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-700.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-700italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-800.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-800.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-800italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-900.ttf +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-900.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v18-latin-900italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-300.woff +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-300.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-300italic.woff +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-300italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-600.woff +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-600.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-600italic.woff +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-600italic.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-700.woff +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-700.woff2 +0 -0
- package/static/fonts/nunito-sans/nunito-sans-v12-latin-700italic.woff +0 -0
- 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
|
-
|
|
10
|
-
|
|
9
|
+
// regex to identify Markdown image tags 
|
|
10
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 ``;
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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);
|