// 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 = `
${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']
});