Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
153 changes: 153 additions & 0 deletions frontend/_includes/alert.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<div
id="alert-container" class="fixed top-4 right-4 z-50 space-y-2">
</div>
<script>
const AlertSystem = {
container: null,
init() {
this.container = document.getElementById('alert-container');
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'alert-container';
this.container.className = 'fixed top-4 right-4 z-50 space-y-2';
document.body.appendChild(this.container);
}
},
show(options = {}) {
if (!this.container) {
this.init();
}
const {
message = '',
type = 'info',
duration = 5000,
dismissible = true
} = options;
const typeConfig = {
success: {
bg: 'bg-green-700',
border: 'border-green-800',
text: 'text-white',
},
error: {
bg: 'bg-red-500',
border: 'border-red-700',
text: 'text-white',
},
warning: {
bg: 'bg-orange-100',
border: 'border-orange-500',
text: 'text-orange-700'
},
info: {
bg: 'bg-indigo-900',
border: 'border-indigo-800',
text: 'text-white',
}
};
const config = typeConfig[type] || typeConfig.info;
const alertId = 'alert-' + crypto.randomUUID();
const alertHTML = `
<div id="${alertId}" class="w-lg w-full ${config.bg} border ${config.border} ${config.text} px-4 py-3 rounded relative shadow-lg transform transition-all duration-300 ease-in-out translate-x-full opacity-0" role="alert">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The CSS class w-lg is not a standard Tailwind CSS class and will have no effect. It seems you intended to set a maximum width for the alert. I suggest changing it to max-w-lg to constrain the alert's width on larger screens, which improves readability.

				<div id="${alertId}" class="max-w-lg w-full ${config.bg} border ${config.border} ${config.text} px-4 py-3 rounded relative shadow-lg transform transition-all duration-300 ease-in-out translate-x-full opacity-0" role="alert">

<span class="block pr-2">${message}</span>
${dismissible ? `
<button class="absolute top-2 right-2 cursor-pointer alert-dismiss-button">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better accessibility and to follow best practices, the dismiss button should have an aria-label attribute to describe its function to screen readers. The <title> element inside an SVG is not consistently supported by all screen reader/browser combinations. Also, adding type="button" prevents it from unintentionally submitting a form if it were ever placed inside one.

						<button type="button" aria-label="Close" class="absolute top-2 right-2 cursor-pointer alert-dismiss-button">

<svg class="fill-current h-6 w-6 ${config.text}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Close</title><path d="M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12l-4.89 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.89a1 1 0 0 0 1.41-1.41L13.41 12l4.89-4.89a1 1 0 0 0 0-1.4z"/></svg>
</button>
` : ''}
</div>
`;
this.container.insertAdjacentHTML('beforeend', alertHTML);
const alertElement = document.getElementById(alertId);
const dismissButton = alertElement.querySelector('.alert-dismiss-button');
if (dismissButton) {
dismissButton.addEventListener('click', () => this.dismiss(alertId));
}
setTimeout(() => {
alertElement.classList.remove('translate-x-full', 'opacity-0');
alertElement.classList.add('translate-x-0', 'opacity-100');
}, 10);
if (duration > 0) {
setTimeout(() => {
this.dismiss(alertId);
}, duration);
}
return alertId;
},
showSuccess(message, options = {}) {
return this.show({
message: formatMessage(message),
type: 'success',
...options
});
},
showError(message, options = {}) {
return this.show({
message: formatMessage(message),
type: 'error',
...options
});
},
showWarning(message, options = {}) {
return this.show({
message: formatMessage(message),
type: 'warning',
...options
});
},
showInfo(message, options = {}) {
return this.show({
message: formatMessage(message),
type: 'info',
...options
});
},
dismiss(alertId) {
const alertElement = document.getElementById(alertId);
if (alertElement) {
alertElement.classList.remove('translate-x-0', 'opacity-100');
alertElement.classList.add('translate-x-full', 'opacity-0');
setTimeout(() => {
if (alertElement.parentNode) {
alertElement.parentNode.removeChild(alertElement);
}
}, 300);
}
},
dismissAll() {
const alerts = this.container.querySelectorAll('[id^="alert-"]');
alerts.forEach(alert => {
const alertId = alert.id;
this.dismiss(alertId);
});
}
};
window.AlertSystem = AlertSystem;
function formatMessage(message) {
if (Array.isArray(message)) {
if (message.length === 1) {
return `Please fix the error: ${escapeHtml(message[0])}`;
} else {
return `Please fix the following:<br>- ${message.map(escapeHtml).join('<br>- ')}`;
}
}
// Escape HTML to prevent XSS
return escapeHtml(message);
}
function escapeHtml(text) {
const temp = document.createElement('div');
temp.textContent = text;
return temp.innerHTML;
}
</script>
1 change: 1 addition & 0 deletions frontend/_includes/layouts/base.njk
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,6 @@
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
{% endif %}
<script type="text/javascript" src="{{ '/assets/scripts/bundle.js' | url }}"></script>
{% include "alert.njk" %}
</body>
</html>
35 changes: 25 additions & 10 deletions frontend/assets/scripts/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,27 @@ function applyStoredRandomTheme(forceNew = false) {
}

/**
* Displays an alert message with a list of validation errors.
* @param {string[]} errors - The list of error messages.
* Displays a warning alert message.
* @param {string|string[]} message - The list of error messages.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JSDoc for the message parameter is a bit confusing. Since the function is named showWarning, the documentation should refer to "warning messages" instead of "error messages" to maintain consistency and clarity.

Suggested change
* @param {string|string[]} message - The list of error messages.
* @param {string|string[]} message - The warning message or an array of messages.

*/
function showAlert(errors) {
if (errors.length === 1) alert(`Please fix the error: ${errors.join(' ')}`);
else alert(`Please fix the following:\n- ${errors.join('\n- ')}`);
function showWarning(message) {
window.AlertSystem.showWarning(message);
}

/**
* Displays a success alert message.
* @param {string|string[]} message - The success message(s).
*/
function showSuccess(message) {
window.AlertSystem.showSuccess(message);
}

/**
* Displays an error alert message.
* @param {string|string[]} message - The error message(s).
*/
function showError(message) {
window.AlertSystem.showError(message);
}

/**
Expand Down Expand Up @@ -253,7 +268,7 @@ async function handleFormSubmit(form, endpoint, validateFn) {

const errors = validateFn(data);
if (errors.length > 0) {
showAlert(errors);
showWarning(errors);
return;
}

Expand All @@ -265,21 +280,21 @@ async function handleFormSubmit(form, endpoint, validateFn) {
});

if (response?.ok && response?.status === 200) {
alert('✅ Submission successful!');
showSuccess('Submission successful!');
form.reset();
} else {
const defaultErrorMessage = 'Something went wrong.';
try {
const result = await response.json();
const errorMessage = result.message || defaultErrorMessage;
alert(`❌ Error: ${errorMessage}`);
showError(errorMessage);
} catch (e) {
console.error('Error parsing JSON response:', e);
alert(`❌ Error: ${defaultErrorMessage}`);
showError(defaultErrorMessage);
}
}
} catch (err) {
alert('❌ Network error. Please try again.');
showError('Network error. Please try again.');
console.error(err);
}
}
Expand Down
Loading