Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cc6eecb
feat(banner): add company banner functionality with additional user v…
nabondance May 31, 2025
573dd21
feat(generate-image): implement user data fetching and merging for co…
nabondance May 31, 2025
aed6f76
fix data merging
nabondance May 31, 2025
d1499a1
dix rankdata counters
nabondance May 31, 2025
88e0180
fix(mergeTrailblazerData): correctly update badge and superbadge coun…
nabondance May 31, 2025
a012660
fix(mergeTrailblazerData): update certification merging to use title …
nabondance May 31, 2025
56164b5
fix(mergeTrailblazerData): enhance learnerStatusLevels merging to ret…
nabondance May 31, 2025
e30559e
feat(BannerForm): add company logo options and update rank logo handling
nabondance May 31, 2025
b6ad4b0
fix(generateImage): adjust rank logo handling logic
nabondance May 31, 2025
cea97fc
fix(generateImage): initialize rankLogoBuffer to null and set default…
nabondance May 31, 2025
8490468
fix(generateImage): set default dimensions for rank logo and remove r…
nabondance May 31, 2025
a73ace4
feat(BannerForm): add company logo options with select input for logo…
nabondance May 31, 2025
9e0e042
fix(generateImage): improve company logo handling and add logging for…
nabondance May 31, 2025
f6d66d6
fix(BannerForm): enhance company logo handling with file reader and u…
nabondance May 31, 2025
2a08f08
feat(dataUtils): add logging for company options in logOptions function
nabondance May 31, 2025
e23c013
feat(BannerForm): add state for uploaded logo file and display select…
nabondance May 31, 2025
85e31be
fix(generateImage): remove console logs for company logo handling
nabondance May 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 194 additions & 17 deletions src/components/BannerForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ const BackgroundPreview = ({ src, backgroundColor }) => {
const BannerForm = ({ onSubmit, setMainError, onValidationError }) => {
const [options, setOptions] = useState({
username: '',
additionalUsers: [],
isCompanyBanner: false,
companyLogoUrl: '',
companyLogoKind: 'no',
companyLogoUploadUrl: '',
backgroundColor: '#5badd6',
backgroundImageUrl: '',
displayBadgeCount: true,
Expand Down Expand Up @@ -94,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('');
Expand Down Expand Up @@ -137,8 +143,28 @@ 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,
});
setUploadedLogoFile(file);
};
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) => {
Expand All @@ -151,6 +177,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));
Expand All @@ -159,20 +186,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;
}
Expand All @@ -184,6 +257,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);
Expand All @@ -197,23 +272,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 && (
<div className='validation-icon' data-tooltip={validationResult.message}>
{validationResult.state === 'ok' ? (
<FontAwesomeIcon icon={faCheck} className='fa-fw icon-valid' /> // Checkmark
<FontAwesomeIcon icon={faCheck} className='fa-fw icon-valid' />
) : validationResult.state === 'private' ? (
<FontAwesomeIcon icon={faTriangleExclamation} className='fa-fw icon-warning' /> // Yellow warning
<FontAwesomeIcon icon={faTriangleExclamation} className='fa-fw icon-warning' />
) : (
<FontAwesomeIcon icon={faCircleXmark} className='fa-fw icon-error' /> // Red cross
<FontAwesomeIcon icon={faCircleXmark} className='fa-fw icon-error' />
)}
</div>
)}
Expand All @@ -223,6 +298,108 @@ const BannerForm = ({ onSubmit, setMainError, onValidationError }) => {
</div>
)}
</div>

<div className='company-banner-toggle'>
<label>
Enable Company Banner
<input
type='checkbox'
checked={options.isCompanyBanner}
onChange={(e) => setOptions({ ...options, isCompanyBanner: e.target.checked })}
/>
</label>
</div>

{options.isCompanyBanner && (
<div className='company-banner-options'>
<h3>Company Banner Options</h3>
<div className='company-logo-section'>
<h4>Company Logo</h4>
<label className='picklist'>
Company Logo:
<select
value={options.companyLogoKind}
onChange={(e) => setOptions({ ...options, companyLogoKind: e.target.value })}
>
<option value='no'>No Logo</option>
<option value='upload'>Upload Logo</option>
<option value='url'>Custom URL</option>
</select>
</label>
{options.companyLogoKind === 'url' && (
<div className='company-logo-input'>
<label>
Company Logo URL:
<input
type='text'
value={options.companyLogoUrl}
onChange={(e) => setOptions({ ...options, companyLogoUrl: e.target.value })}
placeholder='Enter company logo URL'
className='input'
/>
</label>
<p className='help-text'></p>
</div>
)}
{options.companyLogoKind === 'upload' && (
<div className='company-logo-input'>
<label>
Upload Company Logo:
<input
type='file'
accept='image/*'
onChange={(e) => handleImageChange(e, true)}
className='input-file'
/>
</label>
{uploadedLogoFile && <p className='file-info'>Selected file: {uploadedLogoFile.name}</p>}
<p className='help-text'></p>
</div>
)}
</div>
<div className='additional-users'>
<h4>Additional Team Members</h4>
{options.additionalUsers.map((user, index) => (
<div key={index} className='additional-user-input'>
<input
type='text'
value={user}
onChange={(e) => {
const newUsers = [...options.additionalUsers];
newUsers[index] = e.target.value;
setOptions({ ...options, additionalUsers: newUsers });
}}
placeholder={`Enter team member ${index + 1} username`}
className='input'
/>
<button
type='button'
className='button remove-user'
onClick={() => {
const newUsers = options.additionalUsers.filter((_, i) => i !== index);
setOptions({ ...options, additionalUsers: newUsers });
}}
>
Remove
</button>
</div>
))}
<button
type='button'
className='button add-user'
onClick={() => {
setOptions({
...options,
additionalUsers: [...options.additionalUsers, ''],
});
}}
>
Add Team Member
</button>
</div>
</div>
)}

{!isGenerating && (
<button type='button' className='button more-options-button' onClick={() => setShowOptions(!showOptions)}>
{showOptions ? 'Hide Options' : 'More Options'}
Expand Down
Loading