@openeventkit/event-site 2.1.20 → 2.1.22

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 (49) hide show
  1. package/package.json +3 -2
  2. package/src/cms/config/collections/configurationsCollection/siteSettings/index.js +2 -0
  3. package/src/cms/config/collections/configurationsCollection/siteSettings/typeDefs.js +10 -0
  4. package/src/components/CertificatePDF.js +313 -0
  5. package/src/components/CertificateSection.js +139 -0
  6. package/src/templates/full-profile-page.js +61 -2
  7. package/src/utils/certificateSettings.js +45 -0
  8. package/src/utils/schedule.js +8 -4
  9. package/src/utils/useMarketingSettings.js +48 -1
  10. package/src/utils/useSiteSettings.js +11 -0
  11. package/src/workers/feeds.worker.js +85 -90
  12. package/static/fonts/fonts.css +120 -20
  13. package/static/fonts/nunito-sans/nunito-sans-v18-latin-200.woff2 +0 -0
  14. package/static/fonts/nunito-sans/nunito-sans-v18-latin-200italic.ttf +0 -0
  15. package/static/fonts/nunito-sans/nunito-sans-v18-latin-200italic.woff2 +0 -0
  16. package/static/fonts/nunito-sans/nunito-sans-v18-latin-300.woff2 +0 -0
  17. package/static/fonts/nunito-sans/nunito-sans-v18-latin-300italic.ttf +0 -0
  18. package/static/fonts/nunito-sans/nunito-sans-v18-latin-300italic.woff2 +0 -0
  19. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400.ttf +0 -0
  20. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400.woff2 +0 -0
  21. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400italic.ttf +0 -0
  22. package/static/fonts/nunito-sans/nunito-sans-v18-latin-400italic.woff2 +0 -0
  23. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500.ttf +0 -0
  24. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500.woff2 +0 -0
  25. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500italic.ttf +0 -0
  26. package/static/fonts/nunito-sans/nunito-sans-v18-latin-500italic.woff2 +0 -0
  27. package/static/fonts/nunito-sans/nunito-sans-v18-latin-600.woff2 +0 -0
  28. package/static/fonts/nunito-sans/nunito-sans-v18-latin-600italic.woff2 +0 -0
  29. package/static/fonts/nunito-sans/nunito-sans-v18-latin-700.ttf +0 -0
  30. package/static/fonts/nunito-sans/nunito-sans-v18-latin-700.woff2 +0 -0
  31. package/static/fonts/nunito-sans/nunito-sans-v18-latin-700italic.woff2 +0 -0
  32. package/static/fonts/nunito-sans/nunito-sans-v18-latin-800.ttf +0 -0
  33. package/static/fonts/nunito-sans/nunito-sans-v18-latin-800.woff2 +0 -0
  34. package/static/fonts/nunito-sans/nunito-sans-v18-latin-800italic.woff2 +0 -0
  35. package/static/fonts/nunito-sans/nunito-sans-v18-latin-900.ttf +0 -0
  36. package/static/fonts/nunito-sans/nunito-sans-v18-latin-900.woff2 +0 -0
  37. package/static/fonts/nunito-sans/nunito-sans-v18-latin-900italic.woff2 +0 -0
  38. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300.woff +0 -0
  39. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300.woff2 +0 -0
  40. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300italic.woff +0 -0
  41. package/static/fonts/nunito-sans/nunito-sans-v12-latin-300italic.woff2 +0 -0
  42. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600.woff +0 -0
  43. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600.woff2 +0 -0
  44. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600italic.woff +0 -0
  45. package/static/fonts/nunito-sans/nunito-sans-v12-latin-600italic.woff2 +0 -0
  46. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700.woff +0 -0
  47. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700.woff2 +0 -0
  48. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700italic.woff +0 -0
  49. package/static/fonts/nunito-sans/nunito-sans-v12-latin-700italic.woff2 +0 -0
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.20",
4
+ "version": "2.1.22",
5
5
  "author": "Tipit LLC",
6
6
  "dependencies": {
7
7
  "@emotion/server": "^11.11.0",
@@ -55,7 +55,7 @@
55
55
  "font-awesome": "^4.7.0",
56
56
  "formik": "^2.4.6",
57
57
  "fs-extra": "^11.3.0",
58
- "full-schedule-widget": "3.1.0-beta.10",
58
+ "full-schedule-widget": "3.1.1-beta.2",
59
59
  "gatsby": "^5.13.5",
60
60
  "gatsby-alias-imports": "^1.0.6",
61
61
  "gatsby-plugin-decap-cms": "^4.0.4",
@@ -78,6 +78,7 @@
78
78
  "immutability-helper": "2.9.1",
79
79
  "immutable": "^5.0.0-beta.5",
80
80
  "js-cookie": "^3.0.5",
81
+ "jsdom": "^24",
81
82
  "klaro": "^0.7.21",
82
83
  "lite-schedule-widget": "3.0.3",
83
84
  "live-event-widget": "4.0.4",
@@ -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]
@@ -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);
@@ -17,6 +17,11 @@ import LiteScheduleComponent from '../components/LiteScheduleComponent'
17
17
  import AvatarEditorModal from '../components/AvatarEditorModal'
18
18
  import ChangePasswordComponent from '../components/ChangePasswordComponent';
19
19
  import AccessTracker from "../components/AttendeeToAttendeeWidgetComponent";
20
+ import CertificateSection from '../components/CertificateSection';
21
+ import useMarketingSettings from '../utils/useMarketingSettings';
22
+ import { MARKETING_SETTINGS_KEYS, DISPLAY_OPTIONS } from '../utils/useMarketingSettings';
23
+ import { getAccessTokenSafely } from '../utils/loginUtils';
24
+ import { getEnvVariable, SUMMIT_API_BASE_URL } from '../utils/envVariables';
20
25
 
21
26
  import { updateProfilePicture, updateProfile, getIDPProfile, updatePassword } from '../actions/user-actions'
22
27
 
@@ -24,9 +29,12 @@ import styles from '../styles/full-profile.module.scss'
24
29
 
25
30
  import "openstack-uicore-foundation/lib/css/components/inputs/datetimepicker.css";
26
31
 
27
- export const FullProfilePageTemplate = ({ user, getIDPProfile, updateProfile, updateProfilePicture, updatePassword }) => {
32
+ export const FullProfilePageTemplate = ({ user, getIDPProfile, updateProfile, updateProfilePicture, updatePassword, summit }) => {
28
33
 
29
34
  const [showProfile, setShowProfile] = useState(false);
35
+ const [freshTickets, setFreshTickets] = useState([]);
36
+ const [ticketsFetched, setTicketsFetched] = useState(false);
37
+ const { getSettingByKey } = useMarketingSettings();
30
38
  const [personalProfile, setPersonalProfile] = useState({
31
39
  firstName: '',
32
40
  lastName: '',
@@ -193,6 +201,51 @@ export const FullProfilePageTemplate = ({ user, getIDPProfile, updateProfile, up
193
201
  navigate('/a/my-schedule')
194
202
  };
195
203
 
204
+ // Fetch fresh tickets for certificate validation
205
+ const fetchFreshTickets = async () => {
206
+ try {
207
+ const accessToken = await getAccessTokenSafely();
208
+ if (!accessToken || !summit) return;
209
+
210
+ const params = new URLSearchParams({
211
+ access_token: accessToken,
212
+ fields: 'id,status',
213
+ expand: 'owner'
214
+ });
215
+
216
+ const apiBaseUrl = getEnvVariable(SUMMIT_API_BASE_URL);
217
+ const url = `${apiBaseUrl}/api/v1/summits/${summit.id}/orders/all/tickets/me?${params}`;
218
+
219
+ const response = await fetch(url);
220
+
221
+ if (response.ok) {
222
+ const data = await response.json();
223
+ setFreshTickets(data.data || []);
224
+ }
225
+ } catch (err) {
226
+ console.error('Profile page - Error fetching tickets:', err);
227
+ } finally {
228
+ setTicketsFetched(true);
229
+ }
230
+ };
231
+
232
+ useEffect(() => {
233
+ if (summit && user.idpProfile && !ticketsFetched) {
234
+ fetchFreshTickets();
235
+ }
236
+ }, [summit, user.idpProfile, ticketsFetched]);
237
+
238
+ // Check if certificates are enabled and user has checked-in tickets
239
+ const certificatesEnabled = getSettingByKey(MARKETING_SETTINGS_KEYS.certificateEnabled) !== DISPLAY_OPTIONS.hide;
240
+ const checkedInTickets = freshTickets.filter(ticket => {
241
+ const isCheckedIn = ticket.owner?.summit_hall_checked_in === true;
242
+ console.log(ticket)
243
+ const isValidTicket = ticket.status === 'Paid';
244
+ return isCheckedIn && isValidTicket;
245
+ });
246
+
247
+ const showCertificate = certificatesEnabled && checkedInTickets.length > 0;
248
+
196
249
  const discardChanges = (state) => {
197
250
  switch (state) {
198
251
  case 'profile':
@@ -264,6 +317,9 @@ export const FullProfilePageTemplate = ({ user, getIDPProfile, updateProfile, up
264
317
  <h4>
265
318
  @{user.idpProfile?.nickname}
266
319
  </h4>
320
+ {showCertificate && (
321
+ <CertificateSection freshTickets={freshTickets} />
322
+ )}
267
323
  <ChangePasswordComponent updatePassword={handlePasswordUpdate} />
268
324
  </div>
269
325
  <div className="column">
@@ -661,6 +717,7 @@ const FullProfilePage = (
661
717
  {
662
718
  location,
663
719
  user,
720
+ summit,
664
721
  getIDPProfile,
665
722
  updateProfile,
666
723
  updateProfilePicture,
@@ -671,6 +728,7 @@ const FullProfilePage = (
671
728
  <Layout location={location}>
672
729
  <OrchestedTemplate
673
730
  user={user}
731
+ summit={summit}
674
732
  getIDPProfile={getIDPProfile}
675
733
  updateProfile={updateProfile}
676
734
  updateProfilePicture={updateProfilePicture}
@@ -695,8 +753,9 @@ FullProfilePageTemplate.propTypes = {
695
753
  updatePassword: PropTypes.func
696
754
  };
697
755
 
698
- const mapStateToProps = ({ userState }) => ({
756
+ const mapStateToProps = ({ userState, summitState }) => ({
699
757
  user: userState,
758
+ summit: summitState.summit,
700
759
  });
701
760
 
702
761
  export default connect(mapStateToProps,
@@ -0,0 +1,45 @@
1
+ import useMarketingSettings, { MARKETING_SETTINGS_KEYS, DISPLAY_OPTIONS } from './useMarketingSettings';
2
+
3
+ export const useCertificateSettings = (siteFont = null) => {
4
+ const { getSettingByKey } = useMarketingSettings();
5
+
6
+ const certificateKeys = {
7
+ // general summit keys
8
+ colorAccent: MARKETING_SETTINGS_KEYS.colorAccent,
9
+ colorPrimary: MARKETING_SETTINGS_KEYS.colorPrimary,
10
+ colorPrimaryContrast: MARKETING_SETTINGS_KEYS.colorPrimaryContrast,
11
+ colorSecondary: MARKETING_SETTINGS_KEYS.colorSecondary,
12
+ colorTextDark: MARKETING_SETTINGS_KEYS.colorTextDark,
13
+ colorTextLight: MARKETING_SETTINGS_KEYS.colorTextLight,
14
+ // certificate specific
15
+ enabled: MARKETING_SETTINGS_KEYS.certificateEnabled,
16
+ height: MARKETING_SETTINGS_KEYS.certificateHeight,
17
+ width: MARKETING_SETTINGS_KEYS.certificateWidth,
18
+ mainColor: MARKETING_SETTINGS_KEYS.certificateMainColor,
19
+ logo: MARKETING_SETTINGS_KEYS.certificateLogo,
20
+ logoWidth: MARKETING_SETTINGS_KEYS.certificateLogoWidth,
21
+ logoHeight: MARKETING_SETTINGS_KEYS.certificateLogoHeight,
22
+ titleText: MARKETING_SETTINGS_KEYS.certificateTitleText,
23
+ summitName: MARKETING_SETTINGS_KEYS.certificateSummitName,
24
+ showRole: MARKETING_SETTINGS_KEYS.certificateShowRole,
25
+ };
26
+
27
+ const certificateSettings = {};
28
+
29
+ Object.entries(certificateKeys).forEach(([propName, key]) => {
30
+ const value = getSettingByKey(key);
31
+ if (value !== undefined) {
32
+ certificateSettings[propName] = value;
33
+ }
34
+ });
35
+
36
+ certificateSettings.enabled = certificateSettings.enabled !== DISPLAY_OPTIONS.hide;
37
+ certificateSettings.showRole = certificateSettings.showRole !== DISPLAY_OPTIONS.hide;
38
+
39
+ // Pass through the site font information if available
40
+ if (siteFont) {
41
+ certificateSettings.siteFont = siteFont;
42
+ }
43
+
44
+ return certificateSettings;
45
+ };
@@ -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
  };
@@ -1,6 +1,11 @@
1
1
  import * as React from "react";
2
2
  import { graphql, useStaticQuery } from "gatsby";
3
3
 
4
+ export const DISPLAY_OPTIONS = {
5
+ show: "SHOW",
6
+ hide: "HIDE"
7
+ };
8
+
4
9
  export const MARKETING_SETTINGS_KEYS = {
5
10
  disqusThreadsBy: "disqus_threads_by",
6
11
  disqusExcludeEvents: "disqus_exclude_events",
@@ -22,9 +27,51 @@ export const MARKETING_SETTINGS_KEYS = {
22
27
  regLiteOrderComplete2ndParagraph: "REG_LITE_ORDER_COMPLETE_STEP_2ND_PARAGRAPH",
23
28
  regLiteOrderCompleteButton: "REG_LITE_ORDER_COMPLETE_BTN_LABEL",
24
29
  regLiteNoAllowedTicketsMessage: "REG_LITE_NO_ALLOWED_TICKETS_MESSAGE",
30
+ // Color settings (from scssUtils/defaults)
31
+ colorAccent: "color_accent",
32
+ colorAlerts: "color_alerts",
33
+ colorBackgroundLight: "color_background_light",
34
+ colorBackgroundDark: "color_background_dark",
35
+ colorButtonBackgroundColor: "color_button_background_color",
36
+ colorButtonColor: "color_button_color",
37
+ colorGrayLighter: "color_gray_lighter",
38
+ colorGrayLight: "color_gray_light",
39
+ colorGrayDark: "color_gray_dark",
40
+ colorGrayDarker: "color_gray_darker",
41
+ colorHorizontalRuleLight: "color_horizontal_rule_light",
42
+ colorHorizontalRuleDark: "color_horizontal_rule_dark",
43
+ colorIconLight: "color_icon_light",
44
+ colorInputBackgroundColorLight: "color_input_background_color_light",
45
+ colorInputBackgroundColorDark: "color_input_background_color_dark",
46
+ colorInputBorderColorLight: "color_input_border_color_light",
47
+ colorInputBorderColorDark: "color_input_border_color_dark",
48
+ colorInputTextColorLight: "color_input_text_color_light",
49
+ colorInputTextColorDark: "color_input_text_color_dark",
50
+ colorInputTextColorDisabledLight: "color_input_text_color_disabled_light",
51
+ colorInputTextColorDisabledDark: "color_input_text_color_disabled_dark",
52
+ colorPrimary: "color_primary",
53
+ colorPrimaryContrast: "color_primary_contrast",
54
+ colorSecondary: "color_secondary",
55
+ colorSecondaryContrast: "color_secondary_contrast",
56
+ colorTextLight: "color_text_light",
57
+ colorTextMed: "color_text_med",
58
+ colorTextDark: "color_text_dark",
59
+ colorTextInputHintsLight: "color_text_input_hints_light",
60
+ colorTextInputHintsDark: "color_text_input_hints_dark",
61
+ colorTextInputHints: "color_text_input_hints",
62
+ // Certificate of Attendance settings
63
+ certificateEnabled: "CERTIFICATE_ENABLED",
64
+ certificateHeight: "CERTIFICATE_HEIGHT",
65
+ certificateWidth: "CERTIFICATE_WIDTH",
66
+ certificateMainColor: "CERTIFICATE_MAIN_COLOR",
67
+ certificateLogo: "CERTIFICATE_LOGO",
68
+ certificateLogoWidth: "CERTIFICATE_LOGO_WIDTH",
69
+ certificateLogoHeight: "CERTIFICATE_LOGO_HEIGHT",
70
+ certificateTitleText: "CERTIFICATE_TITLE_TEXT",
71
+ certificateSummitName: "CERTIFICATE_SUMMIT_NAME",
72
+ certificateShowRole: "CERTIFICATE_SHOW_ROLE",
25
73
  }
26
74
 
27
-
28
75
  const marketingSettingsQuery = graphql`
29
76
  query {
30
77
  allMarketingSettingsJson {
@@ -13,6 +13,17 @@ const siteSettingsQuery = graphql`
13
13
  publicURL
14
14
  }
15
15
  }
16
+ siteFont {
17
+ fontFamily
18
+ regularFont {
19
+ fontFile
20
+ fontFormat
21
+ }
22
+ boldFont {
23
+ fontFile
24
+ fontFormat
25
+ }
26
+ }
16
27
  widgets {
17
28
  chat {
18
29
  enabled
@@ -6,101 +6,96 @@ import speakersBuildJson from "data/speakers.json";
6
6
  import speakersIDXBuildJson from "data/speakers.idx.json";
7
7
 
8
8
  import {
9
- bucket_getSummit,
10
- bucket_getEvents,
11
- bucket_getEventsIDX,
12
- bucket_getSpeakers,
13
- bucket_getSpeakersIDX
9
+ bucket_getSummit,
10
+ bucket_getEvents,
11
+ bucket_getEventsIDX,
12
+ bucket_getSpeakers,
13
+ bucket_getSpeakersIDX
14
14
  } from "../actions/update-data-actions";
15
15
 
16
16
  import {
17
- SUMMIT_FILE_PATH,
18
- EVENTS_FILE_PATH,
19
- EVENTS_IDX_FILE_PATH,
20
- SPEAKERS_FILE_PATH,
21
- SPEAKERS_IDX_FILE_PATH
17
+ SUMMIT_FILE_PATH,
18
+ EVENTS_FILE_PATH,
19
+ EVENTS_IDX_FILE_PATH,
20
+ SPEAKERS_FILE_PATH,
21
+ SPEAKERS_IDX_FILE_PATH
22
22
  } from "../utils/filePath";
23
23
 
24
+ const isNonEmptyObject = (v) =>
25
+ v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0;
26
+
27
+ const isNonEmptyArray = (v) => Array.isArray(v) && v.length > 0;
28
+
29
+ const pick = (result, expect) => {
30
+ if (!result || typeof result !== "object") {
31
+ return {accepted: false, data: null, lastModified: 0};
32
+ }
33
+ const {file, lastModified} = result;
34
+ const ok =
35
+ expect === "object" ? isNonEmptyObject(file) :
36
+ expect === "array" ? isNonEmptyArray(file) : false;
37
+
38
+ return ok
39
+ ? {accepted: true, data: file, lastModified: lastModified}
40
+ : {accepted: false, data: null, lastModified: 0};
41
+ };
42
+
24
43
  /* eslint-disable-next-line no-restricted-globals */
25
44
  self.onmessage = async ({data: {summitId, staticJsonFilesBuildTime}}) => {
26
- staticJsonFilesBuildTime = JSON.parse(staticJsonFilesBuildTime);
27
-
28
- console.log(`feeds worker running for ${summitId} ....`)
29
- const calls = [];
30
-
31
- // events
32
- let buildTime = staticJsonFilesBuildTime.find(e => e.file === EVENTS_FILE_PATH).build_time;
33
-
34
- calls.push(bucket_getEvents(summitId, buildTime));
35
-
36
- buildTime = staticJsonFilesBuildTime.find(e => e.file === EVENTS_IDX_FILE_PATH).build_time;
37
- calls.push(bucket_getEventsIDX(summitId, buildTime));
38
-
39
- // summit
40
- buildTime = staticJsonFilesBuildTime.find(e => e.file === SUMMIT_FILE_PATH).build_time;
41
- calls.push(bucket_getSummit(summitId, buildTime));
42
-
43
- //speakers
44
- buildTime = staticJsonFilesBuildTime.find(e => e.file === SPEAKERS_FILE_PATH).build_time;
45
- calls.push(bucket_getSpeakers(summitId, buildTime));
46
-
47
- buildTime = staticJsonFilesBuildTime.find(e => e.file === SPEAKERS_IDX_FILE_PATH).build_time;
48
- calls.push(bucket_getSpeakersIDX(summitId, buildTime));
49
-
50
- Promise.all(calls)
51
- .then((values) => {
52
- let lastModified = settings.lastBuild;
53
- let eventsData = values[0];
54
- let eventsIDXData = values[1];
55
- let summitData = values[2];
56
- let speakersData = values[3];
57
- let speakersIXData = values[4];
58
-
59
- // if null , then set the SSR content
60
- // summit
61
- if (summitData && summitData?.file){
62
- if(summitData.lastModified > lastModified)
63
- lastModified = summitData.lastModified;
64
- summitData = summitData.file;
65
- }
66
- else
67
- summitData = summitBuildJson;
68
- // events
69
- if (eventsData && eventsData?.file){
70
- if(eventsData.lastModified > lastModified)
71
- lastModified = eventsData.lastModified;
72
- eventsData = eventsData.file;
73
- }
74
- else
75
- eventsData = eventsBuildJson;
76
- // events idx
77
- if (eventsIDXData && eventsIDXData?.file){
78
- if(eventsIDXData.lastModified > lastModified)
79
- lastModified = eventsIDXData.lastModified;
80
- eventsIDXData = eventsIDXData.file;
81
- }
82
- else
83
- eventsIDXData = eventsIDXBuildJson;
84
- // speakers
85
- if (speakersData && speakersData?.file){
86
- if(speakersData.lastModified > lastModified)
87
- lastModified = speakersData.lastModified;
88
- speakersData = speakersData.file;
89
- }
90
- else
91
- speakersData = speakersBuildJson;
92
- // speakers idx
93
- if (speakersIXData && speakersIXData?.file){
94
- if(speakersIXData.lastModified > lastModified)
95
- lastModified = speakersIXData.lastModified;
96
- speakersIXData = speakersIXData.file;
97
- }
98
- else
99
- speakersIXData = speakersIDXBuildJson;
100
-
101
- /* eslint-disable-next-line no-restricted-globals */
102
- self.postMessage({
103
- eventsData, summitData, speakersData, eventsIDXData, speakersIXData, lastModified
104
- });
105
- });
45
+ staticJsonFilesBuildTime = JSON.parse(staticJsonFilesBuildTime);
46
+
47
+ console.log(`feeds worker running for ${summitId} ....`)
48
+ const calls = [];
49
+
50
+ // events
51
+ let buildTime = staticJsonFilesBuildTime.find(e => e.file === EVENTS_FILE_PATH).build_time;
52
+
53
+ calls.push(bucket_getEvents(summitId, buildTime));
54
+
55
+ buildTime = staticJsonFilesBuildTime.find(e => e.file === EVENTS_IDX_FILE_PATH).build_time;
56
+ calls.push(bucket_getEventsIDX(summitId, buildTime));
57
+
58
+ // summit
59
+ buildTime = staticJsonFilesBuildTime.find(e => e.file === SUMMIT_FILE_PATH).build_time;
60
+ calls.push(bucket_getSummit(summitId, buildTime));
61
+
62
+ //speakers
63
+ buildTime = staticJsonFilesBuildTime.find(e => e.file === SPEAKERS_FILE_PATH).build_time;
64
+ calls.push(bucket_getSpeakers(summitId, buildTime));
65
+
66
+ buildTime = staticJsonFilesBuildTime.find(e => e.file === SPEAKERS_IDX_FILE_PATH).build_time;
67
+ calls.push(bucket_getSpeakersIDX(summitId, buildTime));
68
+
69
+ Promise.all(calls)
70
+ .then((values) => {
71
+ let lastModified = settings.lastBuild;
72
+ let eventsData = values[0];
73
+ let eventsIDXData = values[1];
74
+ let summitData = values[2];
75
+ let speakersData = values[3];
76
+ let speakersIXData = values[4];
77
+
78
+ // if null , then set the SSR content
79
+ // summit
80
+ const summitDataPicked = pick(summitData, "object");
81
+ summitData = summitDataPicked.accepted && summitDataPicked.lastModified > lastModified ? summitDataPicked.data : summitBuildJson;
82
+ // events
83
+ const eventsDataPicked = pick(eventsData, "array");
84
+ eventsData = eventsDataPicked.accepted && eventsDataPicked.lastModified > lastModified ? eventsDataPicked.data : eventsBuildJson;
85
+ // events idx
86
+ const eventsIDXDataPicked = pick(eventsIDXData, "object");
87
+ eventsIDXData = eventsIDXDataPicked.accepted && eventsIDXDataPicked.lastModified > lastModified ? eventsIDXDataPicked.data : eventsIDXBuildJson;
88
+ // speakers
89
+ const speakersDataPicked = pick(speakersData, "array");
90
+ speakersData = speakersDataPicked.accepted && speakersDataPicked.lastModified > lastModified ? speakersDataPicked.data : speakersBuildJson;
91
+ // speakers idx
92
+ const speakersIXDataPicked = pick(speakersIXData, "object");
93
+ speakersIXData = speakersIXDataPicked.accepted && speakersIXDataPicked.lastModified > lastModified ? speakersIXDataPicked.data : speakersIDXBuildJson;
94
+
95
+
96
+ /* eslint-disable-next-line no-restricted-globals */
97
+ self.postMessage({
98
+ eventsData, summitData, speakersData, eventsIDXData, speakersIXData, lastModified
99
+ });
100
+ });
106
101
  };
@@ -1,65 +1,165 @@
1
1
  /**
2
- * Generated using google-webfonts-helper
3
- * @see http://google-webfonts-helper.herokuapp.com/fonts/nunito-sans?subsets=latin
2
+ * Nunito Sans Font Family - Version 18
3
+ * Weights: 200, 300, 400, 500, 600, 700, 800, 900
4
+ * Styles: normal, italic
4
5
  */
5
6
 
7
+ /* nunito-sans-200 - latin */
8
+ @font-face {
9
+ font-family: "Nunito Sans";
10
+ font-style: normal;
11
+ font-weight: 200;
12
+ font-display: swap;
13
+ src: local(""),
14
+ url("nunito-sans/nunito-sans-v18-latin-200.woff2") format("woff2");
15
+ }
16
+
17
+ /* nunito-sans-200italic - latin */
18
+ @font-face {
19
+ font-family: "Nunito Sans";
20
+ font-style: italic;
21
+ font-weight: 200;
22
+ font-display: swap;
23
+ src: local(""),
24
+ url("nunito-sans/nunito-sans-v18-latin-200italic.woff2") format("woff2");
25
+ }
26
+
6
27
  /* nunito-sans-300 - latin */
7
28
  @font-face {
8
29
  font-family: "Nunito Sans";
9
30
  font-style: normal;
10
31
  font-weight: 300;
32
+ font-display: swap;
11
33
  src: local(""),
12
- url("nunito-sans/nunito-sans-v12-latin-300.woff2") format("woff2"),
13
- /* Chrome 26+, Opera 23+, Firefox 39+ */
14
- url("nunito-sans/nunito-sans-v12-latin-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
34
+ url("nunito-sans/nunito-sans-v18-latin-300.woff2") format("woff2");
15
35
  }
36
+
16
37
  /* nunito-sans-300italic - latin */
17
38
  @font-face {
18
39
  font-family: "Nunito Sans";
19
40
  font-style: italic;
20
41
  font-weight: 300;
42
+ font-display: swap;
43
+ src: local(""),
44
+ url("nunito-sans/nunito-sans-v18-latin-300italic.woff2") format("woff2");
45
+ }
46
+
47
+ /* nunito-sans-400 - latin */
48
+ @font-face {
49
+ font-family: "Nunito Sans";
50
+ font-style: normal;
51
+ font-weight: 400;
52
+ font-display: swap;
53
+ src: local(""),
54
+ url("nunito-sans/nunito-sans-v18-latin-400.woff2") format("woff2");
55
+ }
56
+
57
+ /* nunito-sans-400italic - latin */
58
+ @font-face {
59
+ font-family: "Nunito Sans";
60
+ font-style: italic;
61
+ font-weight: 400;
62
+ font-display: swap;
63
+ src: local(""),
64
+ url("nunito-sans/nunito-sans-v18-latin-400italic.woff2") format("woff2");
65
+ }
66
+
67
+ /* nunito-sans-500 - latin */
68
+ @font-face {
69
+ font-family: "Nunito Sans";
70
+ font-style: normal;
71
+ font-weight: 500;
72
+ font-display: swap;
73
+ src: local(""),
74
+ url("nunito-sans/nunito-sans-v18-latin-500.woff2") format("woff2");
75
+ }
76
+
77
+ /* nunito-sans-500italic - latin */
78
+ @font-face {
79
+ font-family: "Nunito Sans";
80
+ font-style: italic;
81
+ font-weight: 500;
82
+ font-display: swap;
21
83
  src: local(""),
22
- url("nunito-sans/nunito-sans-v12-latin-300italic.woff2") format("woff2"),
23
- /* Chrome 26+, Opera 23+, Firefox 39+ */
24
- url("nunito-sans/nunito-sans-v12-latin-300italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
84
+ url("nunito-sans/nunito-sans-v18-latin-500italic.woff2") format("woff2");
25
85
  }
86
+
26
87
  /* nunito-sans-600 - latin */
27
88
  @font-face {
28
89
  font-family: "Nunito Sans";
29
90
  font-style: normal;
30
91
  font-weight: 600;
92
+ font-display: swap;
31
93
  src: local(""),
32
- url("nunito-sans/nunito-sans-v12-latin-600.woff2") format("woff2"),
33
- /* Chrome 26+, Opera 23+, Firefox 39+ */
34
- url("nunito-sans/nunito-sans-v12-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
94
+ url("nunito-sans/nunito-sans-v18-latin-600.woff2") format("woff2");
35
95
  }
96
+
36
97
  /* nunito-sans-600italic - latin */
37
98
  @font-face {
38
99
  font-family: "Nunito Sans";
39
100
  font-style: italic;
40
101
  font-weight: 600;
102
+ font-display: swap;
41
103
  src: local(""),
42
- url("nunito-sans/nunito-sans-v12-latin-600italic.woff2") format("woff2"),
43
- /* Chrome 26+, Opera 23+, Firefox 39+ */
44
- url("nunito-sans/nunito-sans-v12-latin-600italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
104
+ url("nunito-sans/nunito-sans-v18-latin-600italic.woff2") format("woff2");
45
105
  }
106
+
46
107
  /* nunito-sans-700 - latin */
47
108
  @font-face {
48
109
  font-family: "Nunito Sans";
49
110
  font-style: normal;
50
111
  font-weight: 700;
112
+ font-display: swap;
51
113
  src: local(""),
52
- url("nunito-sans/nunito-sans-v12-latin-700.woff2") format("woff2"),
53
- /* Chrome 26+, Opera 23+, Firefox 39+ */
54
- url("nunito-sans/nunito-sans-v12-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
114
+ url("nunito-sans/nunito-sans-v18-latin-700.woff2") format("woff2");
55
115
  }
116
+
56
117
  /* nunito-sans-700italic - latin */
57
118
  @font-face {
58
119
  font-family: "Nunito Sans";
59
120
  font-style: italic;
60
121
  font-weight: 700;
122
+ font-display: swap;
123
+ src: local(""),
124
+ url("nunito-sans/nunito-sans-v18-latin-700italic.woff2") format("woff2");
125
+ }
126
+
127
+ /* nunito-sans-800 - latin */
128
+ @font-face {
129
+ font-family: "Nunito Sans";
130
+ font-style: normal;
131
+ font-weight: 800;
132
+ font-display: swap;
133
+ src: local(""),
134
+ url("nunito-sans/nunito-sans-v18-latin-800.woff2") format("woff2");
135
+ }
136
+
137
+ /* nunito-sans-800italic - latin */
138
+ @font-face {
139
+ font-family: "Nunito Sans";
140
+ font-style: italic;
141
+ font-weight: 800;
142
+ font-display: swap;
143
+ src: local(""),
144
+ url("nunito-sans/nunito-sans-v18-latin-800italic.woff2") format("woff2");
145
+ }
146
+
147
+ /* nunito-sans-900 - latin */
148
+ @font-face {
149
+ font-family: "Nunito Sans";
150
+ font-style: normal;
151
+ font-weight: 900;
152
+ font-display: swap;
61
153
  src: local(""),
62
- url("nunito-sans/nunito-sans-v12-latin-700italic.woff2") format("woff2"),
63
- /* Chrome 26+, Opera 23+, Firefox 39+ */
64
- url("nunito-sans/nunito-sans-v12-latin-700italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
154
+ url("nunito-sans/nunito-sans-v18-latin-900.woff2") format("woff2");
65
155
  }
156
+
157
+ /* nunito-sans-900italic - latin */
158
+ @font-face {
159
+ font-family: "Nunito Sans";
160
+ font-style: italic;
161
+ font-weight: 900;
162
+ font-display: swap;
163
+ src: local(""),
164
+ url("nunito-sans/nunito-sans-v18-latin-900italic.woff2") format("woff2");
165
+ }