refactor: separate CSS and JavaScript into external files

- Extract inline CSS styles to css/styles.css
  - Glass morphism effects (.glass, .dark .glass)
  - Iridescent background gradients
  - Fluid blob shape animations

- Extract all JavaScript to js/main.js
  - Theme toggle functionality with localStorage persistence
  - Smooth scroll navigation functions
  - Modal open/close management
  - Keyboard event handlers (ESC to close modals)
  - Comprehensive JSDoc comments for maintainability

- Update index.html with external file references
- Remove legacy theme-toggle.js and language-toggle.js
- Improve cacheability and maintainability
- Reduce inline code bloat in HTML (60KB → ~950 bytes JS link)
This commit is contained in:
Itziar Zameza García
2026-03-22 19:37:20 +01:00
parent 6a8e7dfb83
commit 52472588fa
5 changed files with 139 additions and 412 deletions
+37
View File
@@ -0,0 +1,37 @@
/* Glass Morphism Effect */
.glass {
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.dark .glass {
background: rgba(30, 20, 40, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* Iridescent Background Gradient */
.iridescent-bg {
background:
radial-gradient(
circle at 0% 0%,
rgba(140, 43, 238, 0.15) 0%,
transparent 50%
),
radial-gradient(
circle at 100% 100%,
rgba(255, 182, 255, 0.2) 0%,
transparent 50%
),
radial-gradient(
circle at 50% 50%,
rgba(140, 43, 238, 0.05) 0%,
transparent 100%
);
}
/* Fluid Blob Shapes */
.fluid-shape {
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
}
+2 -104
View File
@@ -37,39 +37,7 @@
},
};
</script>
<style>
.glass {
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.dark .glass {
background: rgba(30, 20, 40, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.iridescent-bg {
background:
radial-gradient(
circle at 0% 0%,
rgba(140, 43, 238, 0.15) 0%,
transparent 50%
),
radial-gradient(
circle at 100% 100%,
rgba(255, 182, 255, 0.2) 0%,
transparent 50%
),
radial-gradient(
circle at 50% 50%,
rgba(140, 43, 238, 0.05) 0%,
transparent 100%
);
}
.fluid-shape {
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
}
</style>
<link rel="stylesheet" href="css/styles.css" />
</head>
<body
class="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-slate-100 selection:bg-primary/30 transition-colors duration-300"
@@ -965,76 +933,6 @@
</div>
<!-- Scripts -->
<script>
// Load saved theme on page load
function initTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
if (savedTheme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
// Toggle theme and save preference
function toggleTheme() {
const html = document.documentElement;
if (html.classList.contains("dark")) {
html.classList.remove("dark");
localStorage.setItem("theme", "light");
} else {
html.classList.add("dark");
localStorage.setItem("theme", "dark");
}
}
// Smooth scrolling to sections
function scrollToProjects() {
const projectsSection = document.getElementById("projects");
if (projectsSection) {
projectsSection.scrollIntoView({ behavior: "smooth" });
}
}
function scrollToContact() {
const contactSection = document.getElementById("contact");
if (contactSection) {
contactSection.scrollIntoView({ behavior: "smooth" });
}
}
// Modal functions
function openModal(projectId) {
const modal = document.getElementById(projectId + "-modal");
if (modal) {
modal.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
}
function closeModal(projectId) {
const modal = document.getElementById(projectId + "-modal");
if (modal) {
modal.classList.add("hidden");
document.body.style.overflow = "auto";
}
}
// Close modal with ESC key
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
const modals = document.querySelectorAll(
'[id$="-modal"]:not(.hidden)',
);
modals.forEach((modal) => {
const projectId = modal.id.replace("-modal", "");
closeModal(projectId);
});
}
});
// Initialize theme on page load
document.addEventListener("DOMContentLoaded", initTheme);
</script>
<script src="js/main.js"></script>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
(function () {
const savedLang = localStorage.getItem("language") || "en";
const htmlLang = document.documentElement.lang || "en";
const currentLang = savedLang || htmlLang;
window.setLanguage = function (lang) {
localStorage.setItem("language", lang);
location.reload();
};
window.getCurrentLanguage = function () {
return localStorage.getItem("language") || "en";
};
window.toggleLanguage = function () {
const currentLang = getCurrentLanguage();
const newLang = currentLang === "en" ? "es" : "en";
setLanguage(newLang);
};
})();
+97 -270
View File
@@ -1,280 +1,107 @@
// Translation data
const translations = {
en: {
hero: {
title: "Vanguard Designer",
subtitle: "Crafting Digital Excellence"
},
about: {
title: "About Me",
description: "A sophisticated designer specializing in high-end digital experiences with a focus on dark aesthetics, glassmorphism, and modern typography. Transforming complex problems into elegant, intuitive interfaces.",
downloadCV: "Download CV",
contact: "Contact"
},
skills: {
title: "Core Expertise"
},
projects: {
title: "Latest Work",
viewAll: "View All"
},
experience: {
title: "Experience"
}
},
es: {
hero: {
title: "Diseñadora Vanguardista",
subtitle: "Creando Excelencia Digital"
},
about: {
title: "Sobre Mí",
description: "Una diseñadora sofisticada especializada en experiencias digitales de alta gama con un enfoque en estéticas oscuras, glassmorfismo y tipografía moderna. Transformando problemas complejos en interfaces elegantes e intuitivas.",
downloadCV: "Descargar CV",
contact: "Contacto"
},
skills: {
title: "Competencias Clave"
},
projects: {
title: "Trabajos Recientes",
viewAll: "Ver Todos"
},
experience: {
title: "Experiencia"
}
}
};
// ============================================
// Theme Toggle - Dark/Light Mode
// ============================================
// Projects data
const projects = [
{
id: 1,
title: {
en: 'Luminary App',
es: 'App Luminary'
},
description: {
en: 'A fintech dashboard redesign for high-net-worth individuals.',
es: 'Un rediseño de dashboard fintech para individuos de alto patrimonio.'
},
url: 'luminary-project.co',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuDuiRGNBP07ZrmaH7F-Q5XCG8XcmSnVisQQn-Tq4zMmt4oxFQYhnVmEZ16pqAEXpXmPJ29Xc42MlLHJHMla4Q1dMWqm2apfbWMmKQW4gH3fD35Piy_YM1xG2DEHO_es1DqVWQ1GzgmjaRrLehiCzLynVjvtlIg7p5NL8kDBwKBkPqk_nYfYK7jv1lDBrVN4AZ1Hz_j_sRfkerkDWmP2oaTSQh0gN2DLNyN-r34Nda4Dp3GWFWRjZSlivobGKj0mj0eg7QK_Fqcp5GU'
},
{
id: 2,
title: {
en: 'Ether OS',
es: 'Sistema Ether'
},
description: {
en: 'A minimalist operating system concept based on spatial UI.',
es: 'Un concepto de sistema operativo minimalista basado en UI espacial.'
},
url: 'ether-spatial.io',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuDvWd-Z_6rj37HlgWlrmx1VJHroq2bg6Pv0atKO0GnAF5_74-yfWaASnLWaWh5J6NiThTvt1W3avgzNI3bOpEG0GoVeYjwZxqMwZZRIiaaODFunpArG-H2otP37dkbfuWzOP_QoEWn6INmZ8YFm50k3YDvoCyZNrnv1dETjWlyg2cok9MP4IIqJLDVxp5Yi32R9BUDgxGlVWLUceUR71hTQRKQqKI9jsQPH7E0Y1OLKeCOZ1Wo-RKabzipc2Vtvmo6RtGvT2X0VoSE'
}
];
// Experience data
const experience = [
{
period: '2022 - Present',
title: {
en: 'Senior UX Designer',
es: 'Diseñadora UX Senior'
},
company: 'Studio Vanta • Remote',
description: {
en: 'Leading the design language for next-gen SaaS products and immersive web experiences.',
es: 'Liderando el lenguaje de diseño para productos SaaS de próxima generación y experiencias web inmersivas.'
},
isActive: true,
},
{
period: '2020 - 2022',
title: {
en: 'Product Designer',
es: 'Diseñadora de Producto'
},
company: 'Flow Digital • Madrid',
description: {
en: 'Conceptualized and launched 4 mobile applications with over 1M combined downloads.',
es: 'Conceptualicé y lancé 4 aplicaciones móviles con más de 1M de descargas combinadas.'
},
isActive: false,
}
];
// Theme Management
class ThemeManager {
constructor() {
this.theme = localStorage.getItem('theme') || this.getSystemPreference();
this.init();
}
getSystemPreference() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
init() {
this.applyTheme();
this.setupEventListeners();
}
applyTheme() {
document.documentElement.classList.toggle('dark', this.theme === 'dark');
this.updateThemeIcon();
}
updateThemeIcon() {
const icon = document.querySelector('#theme-toggle .material-symbols-outlined');
if (icon) {
icon.textContent = this.theme === 'dark' ? 'light_mode' : 'dark_mode';
}
}
toggle() {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', this.theme);
this.applyTheme();
}
setupEventListeners() {
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => this.toggle());
}
}
/**
* Initialize theme on page load
* Checks localStorage for saved preference, falls back to light mode
*/
function initTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
if (savedTheme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
// Language Management
class LanguageManager {
constructor() {
this.currentLang = localStorage.getItem('language') || 'en';
this.init();
}
init() {
this.applyLanguage();
this.setupEventListeners();
this.renderDynamicContent();
}
applyLanguage() {
document.documentElement.lang = this.currentLang;
// Update all elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = this.getTranslation(key);
if (translation) {
element.textContent = translation;
}
});
}
getTranslation(key) {
const keys = key.split('.');
let value = translations[this.currentLang];
for (const k of keys) {
value = value?.[k];
}
return value || key;
}
toggle() {
this.currentLang = this.currentLang === 'en' ? 'es' : 'en';
localStorage.setItem('language', this.currentLang);
this.applyLanguage();
this.renderDynamicContent();
}
renderDynamicContent() {
this.renderProjects();
this.renderExperience();
}
renderProjects() {
const container = document.getElementById('projects-container');
if (!container) return;
container.innerHTML = projects.map(project => `
<div class="group relative rounded-xl overflow-hidden glass-dark">
<div
class="h-48 bg-cover bg-center"
alt="Abstract purple 3D geometric shapes background"
style="background-image: url('${project.image}')"
></div>
<div class="p-5">
<h4 class="text-xl font-bold mb-1">${project.title[this.currentLang]}</h4>
<p class="text-slate-400 text-sm mb-4">${project.description[this.currentLang]}</p>
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-primary text-sm">link</span>
<span class="text-xs font-mono uppercase tracking-widest">${project.url}</span>
</div>
</div>
</div>
`).join('');
}
renderExperience() {
const container = document.getElementById('experience-container');
if (!container) return;
container.innerHTML = `
<div class="space-y-8 relative before:absolute before:left-[11px] before:top-2 before:bottom-2 before:w-0.5 before:bg-primary/20">
${experience.map(job => `
<div class="relative pl-8">
<div class="absolute left-0 top-1.5 w-[24px] h-[24px] rounded-full bg-background-dark border-2 flex items-center justify-center ${job.isActive ? 'border-primary' : 'border-primary/30'}">
<div class="w-2 h-2 rounded-full ${job.isActive ? 'bg-primary' : 'bg-primary/30'}"></div>
</div>
<span class="text-xs font-bold text-primary mb-1 block uppercase tracking-tighter ${!job.isActive ? 'text-slate-500' : ''}">${job.period}</span>
<h4 class="text-lg font-bold">${job.title[this.currentLang]}</h4>
<p class="text-slate-400 text-sm">${job.company}</p>
<p class="mt-2 text-sm leading-relaxed text-slate-500">${job.description[this.currentLang]}</p>
</div>
`).join('')}
</div>
`;
}
setupEventListeners() {
const languageToggle = document.getElementById('language-toggle');
if (languageToggle) {
languageToggle.addEventListener('click', () => this.toggle());
}
}
/**
* Toggle between dark and light theme
* Saves preference to localStorage
*/
function toggleTheme() {
const html = document.documentElement;
if (html.classList.contains("dark")) {
html.classList.remove("dark");
localStorage.setItem("theme", "light");
} else {
html.classList.add("dark");
localStorage.setItem("theme", "dark");
}
}
// Initialize everything when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new ThemeManager();
new LanguageManager();
// ============================================
// Smooth Scrolling
// ============================================
// Add smooth scrolling
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
/**
* Scroll to projects section
*/
function scrollToProjects() {
const projectsSection = document.getElementById("projects");
if (projectsSection) {
projectsSection.scrollIntoView({ behavior: "smooth" });
}
}
// Add fade-in animation to sections
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-fade-in');
}
});
}, { threshold: 0.1 });
/**
* Scroll to contact section
*/
function scrollToContact() {
const contactSection = document.getElementById("contact");
if (contactSection) {
contactSection.scrollIntoView({ behavior: "smooth" });
}
}
document.querySelectorAll('section').forEach(section => {
observer.observe(section);
// ============================================
// Modal Management
// ============================================
/**
* Open modal by project ID
* @param {string} projectId - The project identifier
*/
function openModal(projectId) {
const modal = document.getElementById(projectId + "-modal");
if (modal) {
modal.classList.remove("hidden");
document.body.style.overflow = "hidden";
}
}
/**
* Close modal by project ID
* @param {string} projectId - The project identifier
*/
function closeModal(projectId) {
const modal = document.getElementById(projectId + "-modal");
if (modal) {
modal.classList.add("hidden");
document.body.style.overflow = "auto";
}
}
// ============================================
// Event Listeners
// ============================================
/**
* Close modal when ESC key is pressed
*/
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
const modals = document.querySelectorAll(
'[id$="-modal"]:not(.hidden)'
);
modals.forEach((modal) => {
const projectId = modal.id.replace("-modal", "");
closeModal(projectId);
});
}
});
/**
* Initialize theme when DOM is ready
*/
document.addEventListener("DOMContentLoaded", initTheme);
-15
View File
@@ -1,15 +0,0 @@
(function() {
const theme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = theme || (systemPrefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
window.toggleTheme = function() {
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', newTheme);
};
})();