Skip to main content

Overview

The Dev Showcase portfolio uses a modular JavaScript architecture with dedicated modules for specific functionality. All JavaScript files are located in wwwroot/js/.

Core Modules

main.js

Handles navigation menu toggling and responsive sidebar behavior. Location: wwwroot/js/main.js

Functions

toggleMenu()
Toggles the sidebar navigation menu, hamburger icon, overlay, and body scroll.
function toggleMenu() {
    sidebar.classList.toggle('active');
    menuToggle.classList.toggle('active');
    menuOverlay.classList.toggle('active');
    document.body.style.overflow = sidebar.classList.contains('active') ? 'hidden' : '';
}

Event Listeners

  • Menu Toggle Click: Opens/closes sidebar menu
  • Menu Overlay Click: Closes menu when clicking outside
  • Navigation Button Click: Closes menu on mobile (≤1024px) after navigation
  • Window Resize: Auto-closes menu when viewport exceeds 1024px

DOM Elements

  • #menuToggle: Hamburger menu button
  • #sidebar: Navigation sidebar
  • #menuOverlay: Dark overlay for mobile menu
  • .nav-button: Navigation buttons

Manages infinite carousel functionality with touch support for project galleries. Location: wwwroot/js/carousel.js

Class: InfiniteCarousel

Creates an infinite looping carousel with clones for seamless scrolling.
Constructor Parameters
{
    trackSelector: string,      // CSS selector for carousel track
    cardSelector: string,       // CSS selector for cards
    prevBtnSelector: string,    // CSS selector for prev button
    nextBtnSelector: string,    // CSS selector for next button
    paginationContainer: string // CSS selector for pagination
}
Key Methods
getVisibleSlides(): Returns number of visible slides based on viewport
  • Returns 2.1 for viewports > 1200px
  • Returns 1.1 otherwise
createClones(): Creates clone cards before and after original cards for infinite effect move(direction): Moves carousel forward (‘next’) or backward (‘prev’) updatePosition(animate): Updates carousel transform position
  • animate: boolean - whether to animate the transition
handleTransitionEnd(): Handles infinite loop by repositioning when reaching clones updatePagination(): Updates current slide number in pagination display
Touch/Mouse Support
Touch Events:
  • touchstart / mousedown: Initiates drag
  • touchmove / mousemove: Tracks drag movement
  • touchend / mouseup: Completes drag and snaps to nearest slide
Drag Threshold: 50px minimum movement to trigger slide change
Card Click Handling
Clicking a carousel card:
  1. Prevents action if user was dragging
  2. Retrieves data-project attribute
  3. Activates corresponding .project-info-card
  4. Scrolls to info section after 450ms delay
Responsive Behavior
  • ResizeObserver: Monitors track dimension changes
  • Debounced Resize: Updates dimensions after 100ms delay
  • Recalculates slide width based on card width + gap

Usage Example

new InfiniteCarousel({
    trackSelector: '.carousel-personal-project-track',
    cardSelector: '.carousel-personal-project-card',
    prevBtnSelector: '.carousel-personal-project-arrow.left',
    nextBtnSelector: '.carousel-personal-project-arrow.right',
    paginationContainer: '.carousel-personal-project-pagination'
});

charts-manager.js

Manages Chart.js integration for vocational skills radar and bar charts. Location: wwwroot/js/charts-manager.js

Chart Types

  1. Radar Chart: RIASEC vocational interests (6 dimensions)
  2. Cognitive Radar Chart: Cognitive abilities comparison
  3. Bar Charts: Skills, interests, careers, and occupational values

Global Variables

let radarChart = null;
let cognitiveChart = null;
let barCharts = {};

Color Schemes

RIASEC Colors
const RIASEC_COLORS = {
    realistic: { border: '#c2185b', fill: 'rgba(194,24,91,0.18)', point: '#c2185b' },
    investigative: { border: '#e91e63', fill: 'rgba(233,30,99,0.18)', point: '#e91e63' },
    artistic: { border: '#ffb74d', fill: 'rgba(255,183,77,0.18)', point: '#ffb74d' },
    social: { border: '#ff9800', fill: 'rgba(255,152,0,0.18)', point: '#ff9800' },
    enterprising: { border: '#ff7043', fill: 'rgba(255,112,67,0.22)', point: '#ff7043' },
    conventional: { border: '#ec407a', fill: 'rgba(236,64,122,0.15)', point: '#ec407a' }
};
Cognitive Colors
const COGNITIVE_COLORS = {
    mine: { border: '#4fc3f7', fill: 'rgba(79,195,247,0.22)', point: '#4fc3f7' },
    avg: { border: '#f06292', fill: 'rgba(240,98,146,0.15)', point: '#f06292' }
};
Bar Chart Colors
const BAR_TOP = { bg: 'rgba(79,195,247,0.75)', border: 'rgba(79,195,247,1)' };
const BAR_LOW = { bg: 'rgba(236,64,122,0.70)', border: 'rgba(236,64,122,1)' };

Key Functions

buildRadarData()
Builds radar chart data from translation system.
const buildRadarData = () => {
    const voc = getVoc();
    const nums = getChartData()?.riasec ?? {};
    // Returns Chart.js data object with labels and datasets
};
buildCognitiveData()
Builds cognitive radar chart with two datasets (personal vs average).
buildBarData()
Builds all bar chart datasets:
  • topSkills, topInterests, topCareers
  • lowSkills, lowInterests, lowCareers
  • valoresOcupacionales
createRadar() / destroyRadar()
Creates/destroys the main RIASEC radar chart.
createCognitiveChart() / destroyCognitiveChart()
Creates/destroys the cognitive abilities radar chart.
createBarCharts() / destroyBarCharts()
Creates/destroys bar charts based on active section.
window.refreshVocationalCharts()
Global function to refresh all charts after language change.
window.refreshVocationalCharts = () => {
    const statsContent = document.getElementById('vocational-stats-content');
    if (!statsContent?.classList.contains('active')) return;
    updateRadarLabels();
    updateCognitiveLabels();
    destroyBarCharts();
    createBarCharts();
};

Chart Lifecycle

Charts are created/destroyed based on:
  1. Section visibility (MutationObserver on #vocational-stats-content)
  2. Active subsection (profile, aptitudes, proyecto)
  3. Language changes (via refreshVocationalCharts())

Section-Specific Charts

const SECTION_BAR_CHARTS = {
    profile: ['topSkillsChart', 'topInterestsChart', 'topCareersChart',
              'lowSkillsChart', 'lowInterestsChart', 'lowCareersChart'],
    aptitudes: ['valoresOcupacionalesChart'],
    proyecto: []
};

Handles modal dialogs for certificates and project images. Location: wwwroot/js/modal-manager.js
  1. Certificate Modal: Displays certificate images with title and date
  2. Image Modal: Displays project images in fullscreen

Functions

openModal(modal)
Opens a modal and prevents body scrolling.
function openModal(modal) {
    if (!modal) return;
    modal.classList.add('active');
    document.body.style.overflow = 'hidden';
}
closeModal(modal)
Closes a modal and restores body scrolling.
function closeModal(modal) {
    if (!modal) return;
    modal.classList.remove('active');
    document.body.style.overflow = '';
}

Event Listeners

Certificate Cards
Clicking .certificate-card elements:
  1. Extracts image, title, and date from card
  2. Populates modal with certificate details
  3. Opens certificate modal
Project Images
Clicking .left-pnl.img-pnl img elements:
  1. Extracts image source and alt text
  2. Populates image modal
  3. Opens fullscreen image modal
Close Triggers
  • Close Button: Click .certificateModalCloseBtn or .modalCloseBtn
  • Background Click: Click modal background overlay
  • Escape Key: Press Escape key to close any active modal

DOM Elements

Certificate Modal:
  • #certificateModal: Modal container
  • #certificateModalImage: Certificate image
  • #certificateModalTitle: Certificate title
  • #certificateModalDate: Issue date
  • #certificateModalCloseBtn: Close button
Image Modal:
  • #imageModal: Modal container
  • #modalImage: Project image
  • #modalCloseBtn: Close button

glow-panels.js

Creates animated glow effects that follow mouse movement on panels. Location: wwwroot/js/glow-panels.js

Functionality

Applies dynamic gradient rotation based on mouse position relative to panel center.

Event Handlers

mousemove
Calculates angle from panel center to cursor position:
panel.addEventListener('mousemove', function (e) {
    const rect = panel.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    
    const centerX = rect.width / 2;
    const centerY = rect.height / 2;
    
    const deltaX = x - centerX;
    const deltaY = y - centerY;
    
    let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
    angle = (angle + 360) % 360;
    
    panel.style.setProperty('--angle', `${angle}deg`);
});
mouseleave
Resets angle to 0deg when mouse leaves panel:
panel.addEventListener('mouseleave', function () {
    panel.style.setProperty('--angle', '0deg');
});

CSS Integration

Sets CSS custom property --angle that controls gradient rotation in CSS:
.glow-panel::before {
    background: conic-gradient(from var(--angle), ...);
}

Target Elements

Applies to all elements with .glow-panel class.

icon.js

Manages dynamic favicon updates based on page visibility. Location: wwwroot/js/icon.js

Functionality

Inverts favicon colors when page is visible (active tab) to create a white icon on dark backgrounds.

Event Handler

visibilitychange
document.addEventListener('visibilitychange', function () {
    const favicon = document.querySelector('link[rel="icon"]');
    const img = new Image();
    img.src = '/images/profile/logo-32x32.png';
    
    img.onload = function () {
        const canvas = document.createElement('canvas');
        canvas.width = 32;
        canvas.height = 32;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, 32, 32);
        
        if (document.hidden) {
            // Page not visible: Keep original (black)
            favicon.href = '/images/profile/logo-32x32.png';
        } else {
            // Page visible: Invert to white
            const imageData = ctx.getImageData(0, 0, 32, 32);
            const data = imageData.data;
            
            for (let i = 0; i < data.length; i += 4) {
                data[i] = 255 - data[i];         // Red
                data[i + 1] = 255 - data[i + 1]; // Green
                data[i + 2] = 255 - data[i + 2]; // Blue
                // Alpha channel (i+3) unchanged
            }
            
            ctx.putImageData(imageData, 0, 0);
            favicon.href = canvas.toDataURL('image/png');
        }
    };
});

Canvas Manipulation

  1. Creates 32x32 canvas
  2. Draws original favicon image
  3. If page visible, inverts RGB channels (255 - value)
  4. Maintains alpha transparency
  5. Converts canvas to data URL
  6. Updates favicon href

DOM Manipulation Patterns

Class Toggle Pattern

Used extensively for showing/hiding elements:
element.classList.toggle('active');
element.classList.add('active');
element.classList.remove('active');
element.classList.contains('active');

Query Selectors

// Single element
const element = document.querySelector('.class-name');
const elementById = document.getElementById('id');

// Multiple elements
const elements = document.querySelectorAll('.class-name');
elements.forEach(el => { /* ... */ });

Event Delegation

Used in carousel for card clicks:
this.track.addEventListener('click', (e) => {
    const card = e.target.closest('.carousel-card');
    if (!card) return;
    // Handle card click
}, { capture: true });

Data Attributes

Used for storing configuration:
const projectKey = card.dataset.project;
const sectionKey = element.getAttribute('data-section');

Initialization Pattern

All modules use DOMContentLoaded event:
document.addEventListener('DOMContentLoaded', () => {
    // Initialization code
});
This ensures scripts execute after DOM is fully loaded.

Additional Modules

translation.js

Complete multilingual translation system with language detection and content updates. Location: wwwroot/js/translation.js

Key Functions

resolveInitialLanguage()
Language detection priority:
  1. URL path prefix (/es/ or /en/)
  2. localStorage (preferredLanguage)
  3. Browser language (navigator.language)
  4. Default fallback (es)
translation.js:4-15
const resolveInitialLanguage = () => {
    const pathMatch = window.location.pathname.match(/^\/(es|en)(?:\/|$)/);
    if (pathMatch) return pathMatch[1];

    const stored = localStorage.getItem('preferredLanguage');
    if (stored && SUPPORTED_LANGUAGES.includes(stored)) return stored;

    const browserLang = (navigator.language || '').slice(0, 2).toLowerCase();
    if (SUPPORTED_LANGUAGES.includes(browserLang)) return browserLang;

    return DEFAULT_LANGUAGE;
};
loadTranslations(language)
Async translation loading with caching and fallback:
translation.js:19-34
const loadTranslations = async (language) => {
    if (translationCache[language]) return translationCache[language];

    try {
        const response = await fetch(`/languages/${language}.json`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();
        translationCache[language] = data;
        return data;
    } catch (error) {
        if (language !== DEFAULT_LANGUAGE) {
            return loadTranslations(DEFAULT_LANGUAGE);
        }
        return null;
    }
};
updateContent(translations)
Updates all elements with data-translate attributes:
translation.js:44-72
document.querySelectorAll('[data-translate]').forEach(element => {
    const value = resolvePath(translations, element.dataset.translate);
    if (value === undefined || value === null) return;

    if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
        element.hasAttribute('placeholder')
            ? (element.placeholder = value)
            : (element.value = value);
    } else {
        element.textContent = value;
    }
});
changeLanguage(language)
Master function that orchestrates language switching:
  • Loads translations
  • Updates DOM content
  • Applies progress bars
  • Updates URL
  • Syncs with server
  • Triggers typewriter and charts refresh

Global Exports

  • window.changeLanguage(language) - Switch language
  • window.updateCVLink() - Update CV download link based on language and profile

typewriter.js

Animated typewriter effect for the header text. Location: wwwroot/js/typewriter.js

TypewriterEffect Class

typewriter.js:1-54
class TypewriterEffect {
    constructor(element, phrases, prefix, options = {}) {
        this.element = element;
        this.phrases = phrases;
        this.prefix = prefix;
        this.currentPhraseIndex = 0;
        this.currentText = '';
        this.isDeleting = false;

        this.config = {
            typingSpeed: options.typingSpeed ?? 100,
            deletingSpeed: options.deletingSpeed ?? 50,
            pauseAfterTyping: options.pauseAfterTyping ?? 2000,
            pauseAfterDeleting: options.pauseAfterDeleting ?? 500,
            loop: options.loop ?? true
        };
    }

    start() {
        this.type();
    }

    stop() {
        if (this.timeoutId) {
            clearTimeout(this.timeoutId);
            this.timeoutId = null;
        }
    }

    type() {
        const currentPhrase = this.phrases[this.currentPhraseIndex];

        this.currentText = this.isDeleting
            ? currentPhrase.substring(0, this.currentText.length - 1)
            : currentPhrase.substring(0, this.currentText.length + 1);

        this.element.textContent = this.prefix + this.currentText;
        // ... timing logic
    }
}

Configuration Options

  • typingSpeed: Milliseconds per character when typing (default: 100)
  • deletingSpeed: Milliseconds per character when deleting (default: 50)
  • pauseAfterTyping: Pause before starting to delete (default: 2000)
  • pauseAfterDeleting: Pause before next phrase (default: 500)
  • loop: Whether to loop through phrases (default: true)

Global Exports

  • window.TypewriterEffect - Class constructor
  • window.initTypewriter(language) - Initialize typewriter with language-specific phrases

Section switching and tab management for portfolio sections. Location: wwwroot/js/navigation.js

Main Functions

switchSection(targetId)
Switches between main content sections (introduction, skills, projects, education):
navigation.js:12-51
function switchSection(targetId) {
    contentSections.forEach(section => section.classList.remove('active'));

    const target = document.querySelector(`.content-section[data-content="${targetId}"]`);
    if (target) {
        target.classList.add('active');
        target.scrollTop = 0;
    }

    navButtons.forEach(btn => {
        btn.classList.toggle('active', btn.getAttribute('data-section') === targetId);
    });

    // Section-specific initialization...
}

Event Listeners

  • Navigation buttons: Switch between main sections
  • Skill filter buttons: Toggle between technical/soft skills
  • Project filter buttons: Toggle between work/personal projects
  • Education buttons: Toggle between preparation/certificates

Section Management

Automatically activates default subsections when entering a section:
  • Skills: Activates “hard skills” tab by default
  • Projects: Activates “professional work” tab by default
  • Education: Activates “preparation” tab by default

stats-animation.js

Animated progress bar filling with intersection observer. Location: wwwroot/js/stats-animation.js

animateStatBars(container)

Animates all progress bars within a container:
stats-animation.js:2-14
function animateStatBars(container) {
    const bars = container.querySelectorAll('.stat-bar[data-percent]');
    bars.forEach(bar => {
        const percent = parseInt(bar.getAttribute('data-percent'), 10);
        if (isNaN(percent)) return;
        bar.style.setProperty('--target-width', percent + '%');
        requestAnimationFrame(() => {
            requestAnimationFrame(() => {
                bar.classList.add('animated');
            });
        });
    });
}

Intersection Observer

Uses IntersectionObserver to trigger animations when sections become visible:
stats-animation.js:20-31
skillSections.forEach(section => {
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting && section.classList.contains('active')) {
                animateStatBars(section);
                observer.disconnect();
            }
        });
    }, { threshold: 0.1 });

    observer.observe(section);
});

Global Exports

  • window.animateStatBars(container) - Manually trigger bar animations

Performance

Uses double requestAnimationFrame for smooth animation timing and prevents layout thrashing.

Build docs developers (and LLMs) love