diff --git a/src/assets/js/components/experience.js b/src/assets/js/components/experience.js
index 78d241e..088d952 100644
--- a/src/assets/js/components/experience.js
+++ b/src/assets/js/components/experience.js
@@ -2,6 +2,8 @@
* Experience section component
*/
+import { sanitizeUrl } from '../utils/validator.js';
+
export const populateExperience = (experience) => {
const experienceContainer = document.getElementById('experience-container');
if (!experienceContainer) return;
@@ -11,21 +13,16 @@ export const populateExperience = (experience) => {
// The fallback icon now acts as a base layer.
const fallbackIconHTML = `
`;
- // The image is an overlay that will be removed on error.
- const imageHTML = `
-
`;
+ // The image will be added via JavaScript to handle errors properly
+ const imageContainerHTML = exp.logoUrl
+ ? ``
+ : '';
return `
${fallbackIconHTML}
- ${exp.logoUrl ? imageHTML : ''}
+ ${imageContainerHTML}
${exp.title}
@@ -41,4 +38,29 @@ export const populateExperience = (experience) => {
.join('');
experienceContainer.innerHTML = experienceHTML;
+
+ // Add company logos with proper error handling (no inline handlers)
+ const logoContainers = experienceContainer.querySelectorAll('.company-logo-container');
+ logoContainers.forEach((container) => {
+ const logoUrl = container.dataset.logoUrl;
+ const company = container.dataset.company;
+
+ if (logoUrl && logoUrl !== '#') {
+ const img = document.createElement('img');
+ img.src = logoUrl;
+ img.alt = `${company} Logo`;
+ img.className =
+ 'company-logo w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-700';
+ img.style.position = 'absolute';
+ img.style.top = '0';
+ img.style.left = '0';
+
+ // Use addEventListener instead of inline onerror
+ img.addEventListener('error', () => {
+ img.remove();
+ });
+
+ container.appendChild(img);
+ }
+ });
};
diff --git a/src/assets/js/components/projects.js b/src/assets/js/components/projects.js
index 95be364..758e87f 100644
--- a/src/assets/js/components/projects.js
+++ b/src/assets/js/components/projects.js
@@ -2,6 +2,8 @@
* Projects section with modern design
*/
+import { sanitizeUrl } from '../utils/validator.js';
+
export const populateProjects = (projects) => {
const projectsContainer = document.getElementById('projects-container');
if (!projectsContainer) return;
@@ -65,10 +67,10 @@ export const populateProjects = (projects) => {
${
project.link
? `
-
@@ -81,8 +83,8 @@ export const populateProjects = (projects) => {
${
project.github
? `
- {
+ if (!url || typeof url !== 'string') {
+ return false;
+ }
+
+ try {
+ const parsed = new URL(url, window.location.origin);
+ // Only allow http, https, and mailto protocols
+ return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
+ } catch {
+ // If URL parsing fails, treat as relative URL
+ // Check if it starts with / (relative path) or # (anchor)
+ return url.startsWith('/') || url.startsWith('#');
+ }
+};
+
+/**
+ * Sanitizes a URL to prevent XSS attacks
+ * Returns a safe URL or fallback to '#'
+ *
+ * @param {string} url - The URL to sanitize
+ * @returns {string} - Safe URL or '#' if invalid
+ */
+export const sanitizeUrl = (url) => {
+ return isValidUrl(url) ? url : '#';
+};
+
+/**
+ * Validates email address format
+ *
+ * @param {string} email - The email to validate
+ * @returns {boolean} - True if email format is valid
+ */
+export const isValidEmail = (email) => {
+ if (!email || typeof email !== 'string') {
+ return false;
+ }
+
+ // Basic email validation regex
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+};