// Theme handling (function initTheme() { const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const saved = localStorage.getItem('theme'); const html = document.documentElement; const toggle = document.getElementById('theme-toggle'); const knob = document.getElementById('theme-toggle-knob'); const sunIcon = toggle?.querySelector('[data-feather="sun"]'); const moonIcon = toggle?.querySelector('[data-feather="moon"]'); const applyTheme = (mode) => { if (mode === 'light') { html.classList.add('light'); // Move knob to the right for light mode if (knob) { knob.style.left = 'calc(100% - 1.25rem - 0.125rem)'; // Right position (w-5 = 1.25rem, top-0.5 = 0.125rem) } // Update icons if (moonIcon && sunIcon) { moonIcon.style.opacity = '0'; moonIcon.style.transform = 'rotate(90deg)'; sunIcon.style.opacity = '1'; sunIcon.style.transform = 'rotate(0deg)'; } } else { html.classList.remove('light'); // Move knob to the left for dark mode if (knob) { knob.style.left = '0.125rem'; // Left position (top-0.5 = 0.125rem) } // Update icons if (moonIcon && sunIcon) { moonIcon.style.opacity = '1'; moonIcon.style.transform = 'rotate(0deg)'; sunIcon.style.opacity = '0'; sunIcon.style.transform = 'rotate(-90deg)'; } } }; const initial = saved || (prefersDark ? 'dark' : 'dark'); applyTheme(initial); toggle?.addEventListener('click', () => { const isLight = html.classList.toggle('light'); localStorage.setItem('theme', isLight ? 'light' : 'dark'); applyTheme(isLight ? 'light' : 'dark'); }); })(); // Mobile menu const menuToggle = document.getElementById('menu-toggle'); const mobileMenu = document.getElementById('mobile-menu'); menuToggle?.addEventListener('click', () => { mobileMenu?.classList.toggle('hidden'); const icon = menuToggle.querySelector('[data-feather]'); if (icon) icon.setAttribute('data-feather', mobileMenu?.classList.contains('hidden') ? 'menu' : 'x'); }); mobileMenu?.querySelectorAll('a').forEach(a => a.addEventListener('click', () => mobileMenu.classList.add('hidden'))); // Smooth anchor offsets for fixed header (optional - using scroll-margin in CSS not needed with pt-20) // Scroll progress + back to top const backToTop = document.getElementById('back-to-top'); window.addEventListener('scroll', () => { const scrolled = window.scrollY; backToTop?.classList.toggle('opacity-100', scrolled > 400); backToTop?.classList.toggle('pointer-events-auto', scrolled > 400); }); backToTop?.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); // Contact form with validation const form = document.getElementById('contact-form'); const status = document.getElementById('form-status'); const statusIcon = document.getElementById('status-icon'); const statusText = document.getElementById('status-text'); const submitBtn = document.getElementById('submit-btn'); const submitText = document.getElementById('submit-text'); const resetBtn = document.getElementById('reset-btn'); // Reset button functionality resetBtn?.addEventListener('click', () => { form.reset(); // Clear custom component errors document.querySelectorAll('custom-input, custom-textarea').forEach(el => { if (el.clearError) el.clearError(); }); // Clear radio button selections document.querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach(input => { input.checked = false; }); statusText.textContent = ''; statusIcon.innerHTML = ''; feather.replace(); }); function validateForm() { let isValid = true; // Clear all errors first document.querySelectorAll('custom-input, custom-textarea').forEach(el => { if (el.clearError) el.clearError(); }); // Get custom components const nameInput = document.querySelector('custom-input#name'); const emailInput = document.querySelector('custom-input#email'); const subjectInput = document.querySelector('custom-input#subject'); const messageInput = document.querySelector('custom-textarea#message'); const privacyCheckbox = document.getElementById('privacy'); // Validate name if (!nameInput || !nameInput.value.trim()) { if (nameInput) nameInput.setError('Please enter your name'); isValid = false; } // Validate email const email = emailInput?.value.trim(); if (!emailInput || !email) { if (emailInput) emailInput.setError('Please enter your email'); isValid = false; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (emailInput) emailInput.setError('Please enter a valid email address'); isValid = false; } // Validate subject if (!subjectInput || !subjectInput.value.trim()) { if (subjectInput) subjectInput.setError('Please enter a subject'); isValid = false; } // Validate message const message = messageInput?.value.trim(); if (!messageInput || !message) { if (messageInput) messageInput.setError('Please enter your message'); isValid = false; } else if (message.length < 10) { if (messageInput) messageInput.setError('Message must be at least 10 characters long'); isValid = false; } // Validate privacy checkbox if (!privacyCheckbox || !privacyCheckbox.checked) { showStatus('Please agree to the privacy policy', 'error'); isValid = false; } return isValid; } function collectFormData() { const projectType = document.querySelector('input[name="project-type"]:checked'); const budget = document.querySelector('input[name="budget"]:checked'); return { name: document.querySelector('custom-input#name')?.value || '', email: document.querySelector('custom-input#email')?.value || '', subject: document.querySelector('custom-input#subject')?.value || '', message: document.querySelector('custom-textarea#message')?.value || '', projectType: projectType?.value || '', budget: budget?.value || '', privacy: document.getElementById('privacy')?.checked || false }; } function showStatus(message, type = 'info') { statusText.textContent = message; status.classList.remove('text-red-400', 'text-emerald-400', 'text-zinc-400'); if (type === 'error') { status.classList.add('text-red-400'); statusIcon.innerHTML = ''; } else if (type === 'success') { status.classList.add('text-emerald-400'); statusIcon.innerHTML = ''; } else { status.classList.add('text-zinc-400'); statusIcon.innerHTML = ''; } feather.replace(); } // Handle form submission form?.addEventListener('submit', async (e) => { e.preventDefault(); if (!validateForm()) { return; } // Reset status showStatus('Sending your message...', 'info'); // Disable form and show loading state submitBtn.disabled = true; submitText.textContent = 'Sending...'; submitBtn.classList.add('opacity-75'); try { // Simulate form submission (replace with actual endpoint) const data = collectFormData(); // Simulate API call delay await new Promise(resolve => setTimeout(resolve, 1500)); // For demo purposes, just log the data console.log('Form submitted:', data); // Success showStatus('Message sent successfully! I\'ll get back to you soon.', 'success'); // Reset form after a delay setTimeout(() => { form.reset(); document.querySelectorAll('custom-input, custom-textarea').forEach(el => { if (el.clearError) el.clearError(); }); statusText.textContent = ''; statusIcon.innerHTML = ''; feather.replace(); }, 3000); } catch (error) { console.error('Form submission error:', error); showStatus('Something went wrong. Please try again or email me directly.', 'error'); } finally { // Reset button state submitBtn.disabled = false; submitText.textContent = 'Send Message'; submitBtn.classList.remove('opacity-75'); } }); // Projects data const projects = [ { title: 'SaaS Billing Dashboard', desc: 'Multi-tenant analytics dashboard with role-based access, charts and export.', tags: ['React', 'TypeScript', 'Tailwind', 'Recharts'], demo: '#', code: '#', cover: 'linear-gradient(135deg, rgb(var(--color-primary-500)), rgb(var(--color-secondary-500)))' }, { title: 'Design System', desc: 'Token-driven component library with docs site and a11y testing.', tags: ['React', 'Storybook', 'A11y', 'Radix'], demo: '#', code: '#', cover: 'linear-gradient(135deg, rgb(var(--color-tertiary-500)), rgb(var(--color-primary-500)))' }, { title: 'E‑commerce Frontend', desc: 'Headless storefront with SSR, cart, and payments integration.', tags: ['Next.js', 'TypeScript', 'Tailwind', 'Stripe'], demo: '#', code: '#', cover: 'linear-gradient(135deg, rgb(var(--color-secondary-500)), rgb(var(--color-tertiary-500)))' }, { title: 'Real‑time Chat', desc: 'WebSocket chat with presence, typing indicators, and message threads.', tags: ['Vue', 'Pinia', 'WebSocket', 'Node.js'], demo: '#', code: '#', cover: 'linear-gradient(135deg, rgb(var(--color-primary-500)), rgb(var(--color-tertiary-500)))' }, { title: 'Perf Audit & Optimization', desc: 'Improved LCP, CLS and TTI; bundle splitting and image optimization.', tags: ['Lighthouse', 'Vite', 'SSR', 'Bundle'], demo: '#', code: '#', cover: 'linear-gradient(135deg, rgb(var(--color-tertiary-500)), rgb(var(--color-secondary-500)))' }, { title: 'Headless CMS Frontend', desc: 'Content-driven site with ISR, preview mode and i18n.', tags: ['Next.js', 'CMS', 'ISR', 'i18n'], demo: '#', code: '#', cover: 'linear-gradient(135deg, rgb(var(--color-secondary-500)), rgb(var(--color-primary-500)))' } ]; // Render project cards const grid = document.getElementById('projects-grid'); if (grid) { projects.forEach(p => { const el = document.createElement('article'); el.className = 'group relative overflow-hidden rounded-2xl border border-white/10 bg-[color:rgb(var(--bg)_/_0.7)] hover:border-[color:rgb(var(--color-primary-500)_/_0.35)] transition-colors'; el.innerHTML = `
Live demo Source

${p.title}

${p.desc}

${p.tags.map(t => `${t}`).join('')}
`; grid.appendChild(el); }); } // Year document.getElementById('year').textContent = new Date().getFullYear(); // Optional: Download CV button (placeholder) document.getElementById('download-cv')?.addEventListener('click', (e) => { e.preventDefault(); const blob = new Blob([`CoolDev — Frontend Engineer\nResume placeholder`], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'CoolDev-Resume.txt'; a.click(); URL.revokeObjectURL(url); }); // Typing animation for code function initTypingAnimation() { const codeContainer = document.getElementById('typing-code'); if (!codeContainer) return; const codeLines = [ { text: 'const theme = "dark"', chars: [ { type: 'keyword', text: 'const' }, { type: 'text', text: ' ' }, { type: 'text', text: 'theme ' }, { type: 'operator', text: '=' }, { type: 'text', text: ' ' }, { type: 'string', text: '"dark"' } ] }, { text: 'function Button({ children, ...props }) {', chars: [ { type: 'keyword', text: 'function' }, { type: 'text', text: ' ' }, { type: 'function', text: 'Button' }, { type: 'punctuation', text: '(' }, { type: 'punctuation', text: '{' }, { type: 'text', text: ' children, ' }, { type: 'operator', text: '...' }, { type: 'text', text: 'props ' }, { type: 'punctuation', text: '}' }, { type: 'punctuation', text: ')' }, { type: 'text', text: ' ' }, { type: 'punctuation', text: '{' } ] }, { text: ' return (', chars: [ { type: 'keyword', text: 'return' }, { type: 'text', text: ' (' } ] }, { text: ' ', chars: [ { type: 'tag', text: '<' }, { type: 'operator', text: '/' }, { type: 'tag', text: 'button' }, { type: 'tag', text: '>' } ] }, { text: ' )', chars: [ { type: 'punctuation', text: ')' } ] }, { text: '}', chars: [ { type: 'punctuation', text: '}' } ] } ]; let currentLine = 0; let currentChar = 0; let isTyping = true; let timeoutId; const cursor = document.querySelector('.typing-cursor'); // Create a line container for better formatting let currentLineContainer = null; function typeCharacter() { if (currentLine >= codeLines.length) { // Finished typing all lines isTyping = false; // Wait 5 seconds, then restart setTimeout(() => { restartTyping(); }, 5000); return; } const line = codeLines[currentLine]; const char = line.chars[currentChar]; // Create line container on first character of each line if (currentChar === 0) { currentLineContainer = document.createElement('div'); currentLineContainer.style.whiteSpace = 'pre'; currentLineContainer.style.fontFamily = 'inherit'; } // Create and append the character element const charElement = document.createElement('span'); charElement.className = char.type !== 'text' ? char.type : ''; charElement.textContent = char.text; // Handle indentation by adjusting margin on the line container if (currentChar === 0) { const indentLevel = Math.floor((line.text.match(/^(\s*)/)[1].length) / 2); currentLineContainer.style.marginLeft = (indentLevel * 12) + 'px'; } currentLineContainer.appendChild(charElement); codeContainer.appendChild(currentLineContainer); currentChar++; // Calculate typing speed (varies by character type) let delay = 50; // default delay if (char.type === 'punctuation') delay = 100; else if (char.type === 'operator') delay = 80; else if (char.type === 'keyword' || char.type === 'function') delay = 120; else if (char.type === 'string') delay = 150; else if (char.text === ' ') delay = 30; // Add some random variation delay += Math.random() * 50; timeoutId = setTimeout(typeCharacter, delay); // If we finished the current line, move to next line if (currentChar >= line.chars.length) { currentLine++; currentChar = 0; currentLineContainer = null; // Add small gap between lines if (currentLine < codeLines.length) { const lineBreak = document.createElement('div'); lineBreak.style.height = '4px'; lineBreak.style.width = '100%'; codeContainer.appendChild(lineBreak); } } } function restartTyping() { // Clear existing content codeContainer.innerHTML = ''; // Reset counters currentLine = 0; currentChar = 0; isTyping = true; currentLineContainer = null; // Clear any existing timeout if (timeoutId) { clearTimeout(timeoutId); } // Start typing again setTimeout(typeCharacter, 1000); // 1 second delay before restart } // Start the typing animation setTimeout(typeCharacter, 2000); // 2 second initial delay // Hide cursor when not typing (during wait periods) setInterval(() => { if (cursor && isTyping) { cursor.classList.toggle('hide'); } }, 500); } // Initialize typing animation when page loads document.addEventListener('DOMContentLoaded', initTypingAnimation); // Custom Input Component class CustomInput extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.value = ''; } static get observedAttributes() { return ['placeholder', 'type', 'name', 'required', 'value']; } connectedCallback() { this.render(); } attributeChangedCallback() { this.render(); } render() { const placeholder = this.getAttribute('placeholder') || ''; const type = this.getAttribute('type') || 'text'; const name = this.getAttribute('name') || ''; const required = this.hasAttribute('required'); this.shadowRoot.innerHTML = `
`; this.value = this.getAttribute('value') || ''; this.setupEventListeners(); this.updateTheme(); } setupEventListeners() { const input = this.shadowRoot.querySelector('.input-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (input) { input.addEventListener('input', (e) => { this.value = e.target.value; this.dispatchEvent(new CustomEvent('input', { detail: { value: this.value } })); this.clearError(); }); input.addEventListener('blur', () => { this.dispatchEvent(new CustomEvent('blur', { detail: { value: this.value } })); }); input.addEventListener('focus', () => { this.dispatchEvent(new CustomEvent('focus', { detail: { value: this.value } })); }); } } updateTheme() { const html = document.documentElement; if (html.classList.contains('light')) { this.shadowRoot.host.classList.add('light'); } else { this.shadowRoot.host.classList.remove('light'); } } setError(message) { const input = this.shadowRoot.querySelector('.input-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (input) input.classList.add('error'); if (errorText) { errorText.textContent = message; errorText.classList.add('show'); } } clearError() { const input = this.shadowRoot.querySelector('.input-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (input) input.classList.remove('error'); if (errorText) { errorText.classList.remove('show'); } } get value() { return this._value || ''; } set value(val) { this._value = val; const input = this.shadowRoot?.querySelector('.input-field'); if (input) { input.value = val; } } } // Custom Textarea Component class CustomTextarea extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.value = ''; } static get observedAttributes() { return ['placeholder', 'name', 'required', 'rows', 'value']; } connectedCallback() { this.render(); } attributeChangedCallback() { this.render(); } render() { const placeholder = this.getAttribute('placeholder') || ''; const name = this.getAttribute('name') || ''; const required = this.hasAttribute('required'); const rows = this.getAttribute('rows') || '4'; this.shadowRoot.innerHTML = `
`; this.value = this.getAttribute('value') || ''; this.setupEventListeners(); this.updateTheme(); } setupEventListeners() { const textarea = this.shadowRoot.querySelector('.textarea-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (textarea) { textarea.addEventListener('input', (e) => { this.value = e.target.value; this.dispatchEvent(new CustomEvent('input', { detail: { value: this.value } })); this.clearError(); }); textarea.addEventListener('blur', () => { this.dispatchEvent(new CustomEvent('blur', { detail: { value: this.value } })); }); textarea.addEventListener('focus', () => { this.dispatchEvent(new CustomEvent('focus', { detail: { value: this.value } })); }); } } updateTheme() { const html = document.documentElement; if (html.classList.contains('light')) { this.shadowRoot.host.classList.add('light'); } else { this.shadowRoot.host.classList.remove('light'); } } setError(message) { const textarea = this.shadowRoot.querySelector('.textarea-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (textarea) textarea.classList.add('error'); if (errorText) { errorText.textContent = message; errorText.classList.add('show'); } } clearError() { const textarea = this.shadowRoot.querySelector('.textarea-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (textarea) textarea.classList.remove('error'); if (errorText) { errorText.classList.remove('show'); } } get value() { return this._value || ''; } set value(val) { this._value = val; const textarea = this.shadowRoot?.querySelector('.textarea-field'); if (textarea) { textarea.value = val; } } } // Register the custom elements customElements.define('custom-input', CustomInput); customElements.define('custom-textarea', CustomTextarea); // Theme change observer const themeObserver = new MutationObserver(() => { document.querySelectorAll('custom-input, custom-textarea').forEach(el => { if (el.updateTheme) el.updateTheme(); }); }); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); // Custom Input Component class CustomInput extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.value = ''; } static get observedAttributes() { return ['placeholder', 'type', 'name', 'required', 'value']; } connectedCallback() { this.render(); } attributeChangedCallback() { this.render(); } render() { const placeholder = this.getAttribute('placeholder') || ''; const type = this.getAttribute('type') || 'text'; const name = this.getAttribute('name') || ''; const required = this.hasAttribute('required'); this.shadowRoot.innerHTML = `
`; this.value = this.getAttribute('value') || ''; this.setupEventListeners(); this.updateTheme(); } setupEventListeners() { const input = this.shadowRoot.querySelector('.input-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (input) { input.addEventListener('input', (e) => { this.value = e.target.value; this.dispatchEvent(new CustomEvent('input', { detail: { value: this.value } })); this.clearError(); }); input.addEventListener('blur', () => { this.dispatchEvent(new CustomEvent('blur', { detail: { value: this.value } })); }); input.addEventListener('focus', () => { this.dispatchEvent(new CustomEvent('focus', { detail: { value: this.value } })); }); } } updateTheme() { const html = document.documentElement; if (html.classList.contains('light')) { this.shadowRoot.host.classList.add('light'); } else { this.shadowRoot.host.classList.remove('light'); } } setError(message) { const input = this.shadowRoot.querySelector('.input-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (input) input.classList.add('error'); if (errorText) { errorText.textContent = message; errorText.classList.add('show'); } } clearError() { const input = this.shadowRoot.querySelector('.input-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (input) input.classList.remove('error'); if (errorText) { errorText.classList.remove('show'); } } get value() { return this._value || ''; } set value(val) { this._value = val; const input = this.shadowRoot?.querySelector('.input-field'); if (input) { input.value = val; } } } // Custom Textarea Component class CustomTextarea extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.value = ''; } static get observedAttributes() { return ['placeholder', 'name', 'required', 'rows', 'value']; } connectedCallback() { this.render(); } attributeChangedCallback() { this.render(); } render() { const placeholder = this.getAttribute('placeholder') || ''; const name = this.getAttribute('name') || ''; const required = this.hasAttribute('required'); const rows = this.getAttribute('rows') || '4'; this.shadowRoot.innerHTML = `
`; this.value = this.getAttribute('value') || ''; this.setupEventListeners(); this.updateTheme(); } setupEventListeners() { const textarea = this.shadowRoot.querySelector('.textarea-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (textarea) { textarea.addEventListener('input', (e) => { this.value = e.target.value; this.dispatchEvent(new CustomEvent('input', { detail: { value: this.value } })); this.clearError(); }); textarea.addEventListener('blur', () => { this.dispatchEvent(new CustomEvent('blur', { detail: { value: this.value } })); }); textarea.addEventListener('focus', () => { this.dispatchEvent(new CustomEvent('focus', { detail: { value: this.value } })); }); } } updateTheme() { const html = document.documentElement; if (html.classList.contains('light')) { this.shadowRoot.host.classList.add('light'); } else { this.shadowRoot.host.classList.remove('light'); } } setError(message) { const textarea = this.shadowRoot.querySelector('.textarea-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (textarea) textarea.classList.add('error'); if (errorText) { errorText.textContent = message; errorText.classList.add('show'); } } clearError() { const textarea = this.shadowRoot.querySelector('.textarea-field'); const errorText = this.shadowRoot.querySelector('.error-text'); if (textarea) textarea.classList.remove('error'); if (errorText) { errorText.classList.remove('show'); } } get value() { return this._value || ''; } set value(val) { this._value = val; const textarea = this.shadowRoot?.querySelector('.textarea-field'); if (textarea) { textarea.value = val; } } } // Register the custom elements customElements.define('custom-input', CustomInput); customElements.define('custom-textarea', CustomTextarea); // Theme change observer const themeObserver = new MutationObserver(() => { document.querySelectorAll('custom-input, custom-textarea').forEach(el => { if (el.updateTheme) el.updateTheme(); }); }); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });