From cc6eecb5e07215ec564332daeeac9c08982d6352 Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 17:36:34 +0200 Subject: [PATCH 01/17] feat(banner): add company banner functionality with additional user validation --- src/components/BannerForm.js | 136 +++++++++++++++++++++++++++++++---- src/styles/globals.css | 43 +++++++++++ 2 files changed, 164 insertions(+), 15 deletions(-) diff --git a/src/components/BannerForm.js b/src/components/BannerForm.js index 6c49c6a6..ec149763 100644 --- a/src/components/BannerForm.js +++ b/src/components/BannerForm.js @@ -65,6 +65,8 @@ const BackgroundPreview = ({ src, backgroundColor }) => { const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { const [options, setOptions] = useState({ username: '', + additionalUsers: [], + isCompanyBanner: false, backgroundColor: '#5badd6', backgroundImageUrl: '', displayBadgeCount: true, @@ -151,6 +153,7 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { setIsGenerating(true); setShowOptions(false); + // Validate primary username const usernameFormatResult = validateUsernameFormat(options.username.toLowerCase()); if (!usernameFormatResult.valid) { setMainError(new Error(usernameFormatResult.message)); @@ -159,20 +162,66 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { return; } - const usernameApiResult = await validateUsernameWithApi(options.username.toLowerCase()); + // Validate additional usernames if company banner is enabled + if (options.isCompanyBanner && options.additionalUsers.length > 0) { + const additionalUsernameErrors = []; + for (let i = 0; i < options.additionalUsers.length; i++) { + const username = options.additionalUsers[i]; + if (username) { + const formatResult = validateUsernameFormat(username.toLowerCase()); + if (!formatResult.valid) { + additionalUsernameErrors.push(`Team member ${i + 1}: ${formatResult.message}`); + } + } + } + + if (additionalUsernameErrors.length > 0) { + const error = new Error(`Validation failed: ${additionalUsernameErrors.join('. ')}`); + setMainError(error); + onValidationError(error, options); + setIsGenerating(false); + return; + } + + // Validate all usernames with API + const allUsernames = [options.username, ...options.additionalUsers.filter((u) => u)]; + const apiValidations = await Promise.all( + allUsernames.map((username) => validateUsernameWithApi(username.toLowerCase())) + ); + + const apiErrors = apiValidations + .map((result, index) => (!result.valid ? `${allUsernames[index]}: ${result.message}` : null)) + .filter((error) => error); + + if (apiErrors.length > 0) { + const error = new Error(`API validation failed: ${apiErrors.join('. ')}`); + setMainError(error); + onValidationError(error, options); + setIsGenerating(false); + return; + } + } else { + // Single user validation + const usernameApiResult = await validateUsernameWithApi(options.username.toLowerCase()); + if (!usernameApiResult.valid) { + const error = new Error(`Validation failed: ${usernameApiResult.message}`); + setMainError(error); + onValidationError(error, options); + setIsGenerating(false); + return; + } + } + + // Validate background image if using custom URL const imageUrlValidation = options.backgroundKind === 'customUrl' ? await validateImageUrl(options.customBackgroundImageUrl) : { valid: true }; - if (!usernameApiResult.valid || !imageUrlValidation.valid) { - const errorMessages = []; - if (!usernameApiResult.valid) errorMessages.push(usernameApiResult.message); - if (!imageUrlValidation.valid) errorMessages.push(imageUrlValidation.message); - - const validationError = new Error(`Validation failed: ${errorMessages.join('. And ')}`); - setMainError(validationError); - onValidationError(validationError, options); + if (!imageUrlValidation.valid) { + const error = new Error(`Validation failed: ${imageUrlValidation.message}`); + setMainError(error); + onValidationError(error, options); setIsGenerating(false); return; } @@ -184,6 +233,8 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { backgroundImageUrl, lastXCertifications: options.lastXCertifications ? parseInt(options.lastXCertifications) : undefined, lastXSuperbadges: options.lastXSuperbadges ? parseInt(options.lastXSuperbadges) : undefined, + // Clean up additionalUsers by removing empty strings + additionalUsers: options.additionalUsers.filter((username) => username.trim()), }); setIsGenerating(false); @@ -197,23 +248,23 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { type='text' value={options.username} onChange={handleUsernameChange} - onBlur={handleUsernameBlur} // Add onBlur event to validate username - placeholder='Enter Trailhead username' // Add placeholder + onBlur={handleUsernameBlur} + placeholder='Enter primary Trailhead username' required className={`input ${validationResult?.state === 'invalid' ? 'input-error' : ''} ${validationResult?.state === 'private' ? 'input-warning' : ''} ${validationResult?.state === 'ok' ? 'input-success' : ''}`} name='trailhead-username' autoComplete='off' - data-lpignore='true' // LastPass specific attribute to ignore + data-lpignore='true' data-form-type='other' /> {validationResult && (
{validationResult.state === 'ok' ? ( - // Checkmark + ) : validationResult.state === 'private' ? ( - // Yellow warning + ) : ( - // Red cross + )}
)} @@ -223,6 +274,61 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { )} + +
+ +
+ + {options.isCompanyBanner && ( +
+

Additional Team Members

+ {options.additionalUsers.map((user, index) => ( +
+ { + const newUsers = [...options.additionalUsers]; + newUsers[index] = e.target.value; + setOptions({ ...options, additionalUsers: newUsers }); + }} + placeholder={`Enter team member ${index + 1} username`} + className='input' + /> + +
+ ))} + +
+ )} + {!isGenerating && ( - - ))} - + + {options.displayCompanyLogo && ( +
+ +

+ The logo should be a PNG or JPEG file with a transparent or white background. +

+
+ )} + +
+

Additional Team Members

+ {options.additionalUsers.map((user, index) => ( +
+ { + const newUsers = [...options.additionalUsers]; + newUsers[index] = e.target.value; + setOptions({ ...options, additionalUsers: newUsers }); + }} + placeholder={`Enter team member ${index + 1} username`} + className='input' + /> + +
+ ))} + +
)} diff --git a/src/styles/globals.css b/src/styles/globals.css index 7c87e1f7..15985761 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1250,7 +1250,7 @@ select:focus { } .additional-users h4 { - margin: 0 0 1rem 0; + margin: 0 0 1rem; color: var(--text-contrast); } diff --git a/src/utils/generateImage.js b/src/utils/generateImage.js index 6f3376a2..b24f8c96 100644 --- a/src/utils/generateImage.js +++ b/src/utils/generateImage.js @@ -121,8 +121,18 @@ export const generateImage = async (options) => { } // Rank Logo + let rankLogoBuffer; + console.log('Company Banner:', options.isCompanyBanner); + console.log('Company Logo URL:', options.companyLogoUrl); + if (options.isCompanyBanner && options.companyLogoUrl) { + if (!(await isValidImageType(options.companyLogoUrl))) { + throw new Error('Unsupported image type for company logo'); + } + rankLogoBuffer = await loadImage(options.companyLogoUrl); + } else { + rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); + } try { - const rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); const rankLogo = await loadImage(rankLogoBuffer); rankLogoHeight = canvas.height * top_part * 1; rankLogoWidth = (rankLogo.width / rankLogo.height) * rankLogoHeight; // Maintain aspect ratio diff --git a/src/utils/trailblazerUtils.js b/src/utils/trailblazerUtils.js index aa8d082d..d869ab40 100644 --- a/src/utils/trailblazerUtils.js +++ b/src/utils/trailblazerUtils.js @@ -13,10 +13,7 @@ function mergeTrailblazerData(trailblazerDataArray) { trails: 0, completedTrailCount: 0, earnedPointsSum: 0, - rank: { - imageUrl: - 'https://res.cloudinary.com/trailhead/image/upload/public-trailhead/assets/images/ranks/triple-star-ranger.png', - }, + rank: {}, learnerStatusLevels: [], }, certificationsData: { From b6ad4b051a753655cf86dc23c34df98bde02672a Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 20:10:24 +0200 Subject: [PATCH 09/17] fix(generateImage): adjust rank logo handling logic --- src/utils/generateImage.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/utils/generateImage.js b/src/utils/generateImage.js index b24f8c96..4df0ea5a 100644 --- a/src/utils/generateImage.js +++ b/src/utils/generateImage.js @@ -73,7 +73,7 @@ const isValidImageType = async (url) => { export const generateImage = async (options) => { // Options logging - logOptions(options); + // logOptions(options); // Warning const warnings = []; @@ -122,15 +122,13 @@ export const generateImage = async (options) => { // Rank Logo let rankLogoBuffer; - console.log('Company Banner:', options.isCompanyBanner); - console.log('Company Logo URL:', options.companyLogoUrl); - if (options.isCompanyBanner && options.companyLogoUrl) { + if (!options.isCompanyBanner && options.companyLogoUrl) { + rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); + } else if (options.displayCompanyLogo && options.companyLogoUrl) { if (!(await isValidImageType(options.companyLogoUrl))) { throw new Error('Unsupported image type for company logo'); } rankLogoBuffer = await loadImage(options.companyLogoUrl); - } else { - rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); } try { const rankLogo = await loadImage(rankLogoBuffer); From cea97fc26b498bd0659a385f9b7365c131923c3a Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 20:15:15 +0200 Subject: [PATCH 10/17] fix(generateImage): initialize rankLogoBuffer to null and set default dimensions for missing rank logo --- src/utils/generateImage.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/utils/generateImage.js b/src/utils/generateImage.js index 4df0ea5a..97a1928e 100644 --- a/src/utils/generateImage.js +++ b/src/utils/generateImage.js @@ -121,7 +121,7 @@ export const generateImage = async (options) => { } // Rank Logo - let rankLogoBuffer; + let rankLogoBuffer = null; if (!options.isCompanyBanner && options.companyLogoUrl) { rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); } else if (options.displayCompanyLogo && options.companyLogoUrl) { @@ -129,20 +129,26 @@ export const generateImage = async (options) => { throw new Error('Unsupported image type for company logo'); } rankLogoBuffer = await loadImage(options.companyLogoUrl); + } else { + rankLogoWidth = 100; + rankLogoHeight = 40; + console.warn('No rank logo found, using default dimensions.'); } - try { - const rankLogo = await loadImage(rankLogoBuffer); - rankLogoHeight = canvas.height * top_part * 1; - rankLogoWidth = (rankLogo.width / rankLogo.height) * rankLogoHeight; // Maintain aspect ratio - const rankLogoScalingFactor = 1.2; - if (options.displayRankLogo) { - ctx.drawImage(rankLogo, 0, 0, rankLogoWidth * rankLogoScalingFactor, rankLogoHeight * rankLogoScalingFactor); + if (rankLogoBuffer !== null) { + try { + const rankLogo = await loadImage(rankLogoBuffer); + rankLogoHeight = canvas.height * top_part * 1; + rankLogoWidth = (rankLogo.width / rankLogo.height) * rankLogoHeight; // Maintain aspect ratio + const rankLogoScalingFactor = 1.2; + if (options.displayRankLogo) { + ctx.drawImage(rankLogo, 0, 0, rankLogoWidth * rankLogoScalingFactor, rankLogoHeight * rankLogoScalingFactor); + } + } catch (error) { + rankLogoWidth = 100; + rankLogoHeight = 40; + console.error(`Error loading rank logo ${options.rankData.rank.imageUrl}:`, error); + warnings.push(`Error loading rank logo ${options.rankData.rank.imageUrl}: ${error.message}`); } - } catch (error) { - rankLogoWidth = 180; - rankLogoHeight = 40; - console.error(`Error loading rank logo ${options.rankData.rank.imageUrl}:`, error); - warnings.push(`Error loading rank logo ${options.rankData.rank.imageUrl}: ${error.message}`); } // Counters From 84904684c6be3a8f28a4934fff971693bc0c5639 Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 20:16:07 +0200 Subject: [PATCH 11/17] fix(generateImage): set default dimensions for rank logo and remove redundant width/height assignments --- src/utils/generateImage.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/utils/generateImage.js b/src/utils/generateImage.js index 97a1928e..858040d1 100644 --- a/src/utils/generateImage.js +++ b/src/utils/generateImage.js @@ -21,8 +21,8 @@ require('./fonts'); const top_part = 1 / 4; const bottom_part = 3 / 4; const right_part = 7 / 10; -let rankLogoWidth; -let rankLogoHeight; +let rankLogoWidth = 120; +let rankLogoHeight = 40; const isValidImageType = async (url) => { try { @@ -129,10 +129,6 @@ export const generateImage = async (options) => { throw new Error('Unsupported image type for company logo'); } rankLogoBuffer = await loadImage(options.companyLogoUrl); - } else { - rankLogoWidth = 100; - rankLogoHeight = 40; - console.warn('No rank logo found, using default dimensions.'); } if (rankLogoBuffer !== null) { try { @@ -144,8 +140,6 @@ export const generateImage = async (options) => { ctx.drawImage(rankLogo, 0, 0, rankLogoWidth * rankLogoScalingFactor, rankLogoHeight * rankLogoScalingFactor); } } catch (error) { - rankLogoWidth = 100; - rankLogoHeight = 40; console.error(`Error loading rank logo ${options.rankData.rank.imageUrl}:`, error); warnings.push(`Error loading rank logo ${options.rankData.rank.imageUrl}: ${error.message}`); } From a73ace4bbe606d00db860b6beca4d30f13b7d7bd Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 23:07:02 +0200 Subject: [PATCH 12/17] feat(BannerForm): add company logo options with select input for logo type and upload functionality --- src/components/BannerForm.js | 51 +++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/components/BannerForm.js b/src/components/BannerForm.js index 05635b34..828cd7fc 100644 --- a/src/components/BannerForm.js +++ b/src/components/BannerForm.js @@ -68,7 +68,8 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { additionalUsers: [], isCompanyBanner: false, companyLogoUrl: '', - displayCompanyLogo: false, + companyLogoKind: 'no', + companyLogoUploadUrl: '', backgroundColor: '#5badd6', backgroundImageUrl: '', displayBadgeCount: true, @@ -293,15 +294,18 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => {

Company Banner Options

Company Logo

-

Additional Team Members

From 9e0e04298d0ea14103e6d0aecaa98bb43bd1351a Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 23:29:33 +0200 Subject: [PATCH 13/17] fix(generateImage): improve company logo handling and add logging for debugging --- src/components/BannerForm.js | 4 +--- src/utils/generateImage.js | 7 +++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/BannerForm.js b/src/components/BannerForm.js index 828cd7fc..d8bb7f61 100644 --- a/src/components/BannerForm.js +++ b/src/components/BannerForm.js @@ -317,9 +317,7 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { className='input' /> -

- The logo should be a PNG or JPEG file with a transparent or white background. -

+

)} {options.companyLogoKind === 'upload' && ( diff --git a/src/utils/generateImage.js b/src/utils/generateImage.js index 858040d1..d7e80133 100644 --- a/src/utils/generateImage.js +++ b/src/utils/generateImage.js @@ -122,9 +122,12 @@ export const generateImage = async (options) => { // Rank Logo let rankLogoBuffer = null; - if (!options.isCompanyBanner && options.companyLogoUrl) { + console.log('companyLogoKind:', options.companyLogoKind); + console.log('Company Logo URL:', options.companyLogoUrl); + if (!options.isCompanyBanner) { rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); - } else if (options.displayCompanyLogo && options.companyLogoUrl) { + } else if (options.isCompanyBanner && options.companyLogoKind != 'no' && options.companyLogoUrl) { + console.log('Loading company logo from URL:', options.companyLogoUrl); if (!(await isValidImageType(options.companyLogoUrl))) { throw new Error('Unsupported image type for company logo'); } From f6d66d64e5395246171b0b8d6637435083dc4df8 Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 23:35:00 +0200 Subject: [PATCH 14/17] fix(BannerForm): enhance company logo handling with file reader and update state management --- src/components/BannerForm.js | 38 +++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/BannerForm.js b/src/components/BannerForm.js index d8bb7f61..ba6f3083 100644 --- a/src/components/BannerForm.js +++ b/src/components/BannerForm.js @@ -142,8 +142,27 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { handleCustomUrlChange(e.target.value, setOptions, setBackgroundImageUrlError); }; - const handleImageChange = async (e) => { - await handleFileChange(e.target.files[0], setBackgroundImageUrlError, setOptions, setUploadedFile); + const handleImageChange = async (e, isCompanyLogo = false) => { + if (isCompanyLogo) { + const file = e.target.files[0]; + if (!file) return; + + try { + const reader = new FileReader(); + reader.onload = () => { + setOptions({ + ...options, + companyLogoUrl: reader.result, + companyLogoUploadUrl: reader.result, + }); + }; + reader.readAsDataURL(file); + } catch (error) { + console.error('Error reading company logo file:', error); + } + } else { + await handleFileChange(e.target.files[0], setBackgroundImageUrlError, setOptions, setUploadedFile); + } }; const handlePredefinedImage = (src) => { @@ -327,20 +346,7 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { { - await handleFileChange( - e.target.files[0], - null, - (newOptions) => { - setOptions({ - ...options, - companyLogoUrl: newOptions.backgroundImageUrl, - companyLogoUploadUrl: newOptions.backgroundImageUrl, - }); - }, - null - ); - }} + onChange={(e) => handleImageChange(e, true)} className='input-file' /> From 2a08f087e18fba4f99343f5a8b59df363b83bf40 Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sat, 31 May 2025 23:37:18 +0200 Subject: [PATCH 15/17] feat(dataUtils): add logging for company options in logOptions function --- src/utils/dataUtils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/dataUtils.js b/src/utils/dataUtils.js index 9673ef7c..ad89cb19 100644 --- a/src/utils/dataUtils.js +++ b/src/utils/dataUtils.js @@ -56,4 +56,9 @@ export const logOptions = (options) => { certificationAlignment: options.certificationAlignment, }); console.log('MVP Data:', options.mvpData); + console.log('Company Options:', { + isCompanyBanner: options.isCompanyBanner, + companyName: options.companyName, + companyLogoUrl: options.companyLogoUrl, + }); }; From e23c013580f0175d12935aafbeb0565fa7bcea6e Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sun, 1 Jun 2025 00:04:11 +0200 Subject: [PATCH 16/17] feat(BannerForm): add state for uploaded logo file and display selected file info --- src/components/BannerForm.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/BannerForm.js b/src/components/BannerForm.js index ba6f3083..2035bb60 100644 --- a/src/components/BannerForm.js +++ b/src/components/BannerForm.js @@ -99,6 +99,7 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { displayAgentblazerRank: true, }); const [uploadedFile, setUploadedFile] = useState(null); + const [uploadedLogoFile, setUploadedLogoFile] = useState(null); const [showOptions, setShowOptions] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [usernameError, setUsernameError] = useState(''); @@ -155,6 +156,7 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { companyLogoUrl: reader.result, companyLogoUploadUrl: reader.result, }); + setUploadedLogoFile(file); }; reader.readAsDataURL(file); } catch (error) { @@ -320,8 +322,8 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { onChange={(e) => setOptions({ ...options, companyLogoKind: e.target.value })} > - + {options.companyLogoKind === 'url' && ( @@ -350,9 +352,8 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => { className='input-file' /> -

- The logo should be a PNG or JPEG file with a transparent or white background. -

+ {uploadedLogoFile &&

Selected file: {uploadedLogoFile.name}

} +

)} From 85e31be082d9c79ab4029149d6b824378ab3df23 Mon Sep 17 00:00:00 2001 From: Nathan Abondance <32196400+nabondance@users.noreply.github.com> Date: Sun, 1 Jun 2025 00:14:03 +0200 Subject: [PATCH 17/17] fix(generateImage): remove console logs for company logo handling --- src/utils/generateImage.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/generateImage.js b/src/utils/generateImage.js index d7e80133..cc705d33 100644 --- a/src/utils/generateImage.js +++ b/src/utils/generateImage.js @@ -122,12 +122,9 @@ export const generateImage = async (options) => { // Rank Logo let rankLogoBuffer = null; - console.log('companyLogoKind:', options.companyLogoKind); - console.log('Company Logo URL:', options.companyLogoUrl); if (!options.isCompanyBanner) { rankLogoBuffer = await getImage(options.rankData.rank.imageUrl, 'ranks'); } else if (options.isCompanyBanner && options.companyLogoKind != 'no' && options.companyLogoUrl) { - console.log('Loading company logo from URL:', options.companyLogoUrl); if (!(await isValidImageType(options.companyLogoUrl))) { throw new Error('Unsupported image type for company logo'); }