Spaces:
Sleeping
Sleeping
| console.log("Script.js started executing at:", new Date().toISOString()); | |
| // Initialize Particles.js | |
| particlesJS('particles-js', { | |
| particles: { | |
| number: { value: 80, density: { enable: true, value_area: 800 } }, | |
| color: { value: ['#1E90FF'] }, | |
| shape: { type: 'circle' }, | |
| opacity: { value: 0.5, random: true }, | |
| size: { value: 3, random: true }, | |
| line_linked: { enable: true, distance: 150, color: '#1E90FF', opacity: 0.4, width: 1 }, | |
| move: { enable: true, speed: 2, direction: 'none', random: true, straight: false, out_mode: 'out', bounce: false } | |
| }, | |
| interactivity: { | |
| detect_on: 'canvas', | |
| events: { onhover: { enable: true, mode: 'repulse' }, onclick: { enable: true, mode: 'push' }, resize: true }, | |
| modes: { repulse: { distance: 100, duration: 0.4 }, push: { particles_nb: 4 } } | |
| }, | |
| retina_detect: true | |
| }); | |
| // Glowing Cursor Trail | |
| const canvas = document.getElementById('cursor-trail'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const trails = []; | |
| window.addEventListener('mousemove', (e) => { | |
| trails.push({ x: e.clientX, y: e.clientY, life: 1 }); | |
| }); | |
| function animateTrail() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| trails.forEach((trail, index) => { | |
| ctx.beginPath(); | |
| ctx.arc(trail.x, trail.y, 5 * trail.life, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(30, 144, 255, ${trail.life})`; | |
| ctx.fill(); | |
| trail.life -= 0.02; | |
| if (trail.life <= 0) trails.splice(index, 1); | |
| }); | |
| requestAnimationFrame(animateTrail); | |
| } | |
| animateTrail(); | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| // GSAP Animations | |
| gsap.registerPlugin(ScrollTrigger); | |
| // Animate the static text and buttons in hero section | |
| gsap.fromTo('.animate-slide-in', | |
| { y: 100, opacity: 0 }, | |
| { y: 0, opacity: 1, duration: 1.2, ease: 'power3.out', onComplete: () => { | |
| document.querySelector('.animate-slide-in').style.opacity = '1'; | |
| }} | |
| ); | |
| // Animate the image in the hero section | |
| gsap.fromTo('.animate-fade-in', | |
| { opacity: 0 }, | |
| { opacity: 1, duration: 1.5, ease: 'power3.out', onComplete: () => { | |
| document.querySelector('.animate-fade-in').style.opacity = '1'; | |
| }} | |
| ); | |
| // Animate stat cards on scroll | |
| gsap.from('.stat-card', { | |
| scrollTrigger: { trigger: '#about', start: 'top 80%' }, | |
| y: 50, | |
| opacity: 0, | |
| duration: 0.8, | |
| stagger: 0.2, | |
| ease: 'power3.out' | |
| }); | |
| // Navigation Bar | |
| const menuToggle = document.getElementById('menu-toggle'); | |
| const mobileMenu = document.getElementById('mobile-menu'); | |
| menuToggle.addEventListener('click', () => { | |
| mobileMenu.classList.toggle('hidden'); | |
| menuToggle.querySelector('i').classList.toggle('fa-bars'); | |
| menuToggle.querySelector('i').classList.toggle('fa-times'); | |
| }); | |
| // Smooth Scrolling | |
| document.querySelectorAll('.nav-link').forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| mobileMenu.classList.add('hidden'); | |
| menuToggle.querySelector('i').classList.add('fa-bars'); | |
| menuToggle.querySelector('i').classList.remove('fa-times'); | |
| document.querySelector(link.getAttribute('href')).scrollIntoView({ behavior: 'smooth' }); | |
| }); | |
| }); | |
| // Theme Toggle (Disabled since theme switching isn't implemented) | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| themeToggle.addEventListener('click', () => { | |
| // Theme switching not implemented | |
| }); | |
| // Back to Top Button | |
| const backToTop = document.getElementById('back-to-top'); | |
| window.addEventListener('scroll', () => { | |
| backToTop.classList.toggle('hidden', window.scrollY < 500); | |
| }); | |
| backToTop.addEventListener('click', () => { | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| }); | |
| // Ripple Effect for Buttons | |
| document.querySelectorAll('.ripple').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const rect = btn.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const ripple = document.createElement('span'); | |
| ripple.style.left = `${x}px`; | |
| ripple.style.top = `${y}px`; | |
| ripple.classList.add('ripple-effect'); | |
| btn.appendChild(ripple); | |
| setTimeout(() => ripple.remove(), 600); | |
| }); | |
| }); | |
| // Fetch Data Helper | |
| const fetchData = async (url, elementId, renderCallback) => { | |
| try { | |
| console.log(`Fetching data from ${url}`); | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |
| const data = await response.json(); | |
| console.log(`Fetched data from ${url}:`, data); | |
| const container = document.getElementById(elementId); | |
| if (!container) throw new Error(`Container with ID ${elementId} not found`); | |
| renderCallback(data, container); | |
| } catch (error) { | |
| console.error(`Error fetching ${url}:`, error); | |
| document.getElementById(elementId).innerHTML = '<p class="text-red-500">Error loading data. Please try again later.</p>'; | |
| } | |
| }; | |
| // Render Skills with Circular Progress | |
| fetchData('/api/skills', 'skills-grid', (data, container) => { | |
| console.log('Rendering skills data:', data); | |
| container.innerHTML = ''; | |
| data.forEach(category => { | |
| const card = document.createElement('div'); | |
| card.className = 'skill-card rounded-xl hover:scale-105 transition-transform'; | |
| card.style.opacity = '1'; | |
| card.style.display = 'block'; | |
| card.innerHTML = ` | |
| <i class="fas fa-${category.icon} mb-2"></i> | |
| <h3 class="mb-4">${category.title}</h3> | |
| <div class="space-y-3"> | |
| ${category.skills.map(skill => ` | |
| <div class="skill-item"> | |
| <span>${skill.name}</span> | |
| <div class="progress-circle-container"> | |
| <div class="progress-circle-bg"></div> | |
| <svg class="progress-circle"> | |
| <circle cx="20" cy="20" r="18" stroke="#E5E5E5" stroke-width="4" fill="none" /> | |
| <circle class="progress-fill" cx="20" cy="20" r="18" stroke="#1E90FF" stroke-width="4" fill="none" stroke-dasharray="0 113" /> | |
| </svg> | |
| <span class="progress-percentage">${skill.proficiency}%</span> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| console.log(`Rendered skill card for ${category.title}`); | |
| card.querySelectorAll('.progress-fill').forEach(circle => { | |
| const proficiency = circle.parentElement.parentElement.querySelector('.progress-percentage').textContent.replace('%', ''); | |
| const circumference = 2 * Math.PI * 18; | |
| const offset = circumference * (proficiency / 100); | |
| gsap.to(circle, { | |
| scrollTrigger: { | |
| trigger: card, | |
| start: 'left 80%', | |
| end: 'right 20%', | |
| scrub: true | |
| }, | |
| strokeDasharray: `${offset} 113`, | |
| duration: 1.5, | |
| ease: 'power3.out' | |
| }); | |
| }); | |
| }); | |
| }); | |
| // Render Achievements | |
| fetchData('/api/achievements', 'achievements-grid', (data, container) => { | |
| data.forEach(achievement => { | |
| const card = document.createElement('div'); | |
| card.className = 'achievement-card glassmorphic p-6 rounded-xl hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-4"> | |
| <div class="icon-wrapper p-3 glassmorphic rounded-full"> | |
| <i class="fas fa-${achievement.icon} text-primary-blue glow"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow">${achievement.title}</h3> | |
| <p class="text-sm text-primary-blue">${achievement.organization}</p> | |
| <p class="text-gray-300">${achievement.description}</p> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| }); | |
| }); | |
| // Render Hobbies with Review Form for "Educating Others" | |
| fetchData('/api/hobbies', 'hobbies-grid', (data, container) => { | |
| data.forEach(hobby => { | |
| const card = document.createElement('div'); | |
| card.className = 'hobby-card glassmorphic p-6 rounded-xl hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="flex items-center space-x-4"> | |
| <div class="icon-wrapper p-3 glassmorphic rounded-full"> | |
| <i class="fas fa-${hobby.icon} text-primary-blue glow"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow">${hobby.title}</h3> | |
| <p class="text-gray-300">${hobby.description}</p> | |
| </div> | |
| </div> | |
| `; | |
| if (hobby.title === "Educating Others") { | |
| card.innerHTML += ` | |
| <div class="mt-6"> | |
| <h4 class="text-md font-orbitron text-primary-blue glow mb-4">Share Your Feedback</h4> | |
| <div class="glassmorphic p-6 rounded-xl space-y-4"> | |
| <div> | |
| <label class="block text-sm text-gray-300 mb-2">Your Name</label> | |
| <input type="text" id="review-name" class="w-full p-3 rounded-md bg-transparent border border-gray-600 text-gray-100 focus:border-primary-blue focus:glow" placeholder="Enter your name"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-300 mb-2">Rating</label> | |
| <div id="star-rating" class="text-2xl text-gray-400 flex space-x-1"> | |
| <span class="star cursor-pointer" data-value="1"><i class="far fa-star"></i></span> | |
| <span class="star cursor-pointer" data-value="2"><i class="far fa-star"></i></span> | |
| <span class="star cursor-pointer" data-value="3"><i class="far fa-star"></i></span> | |
| <span class="star cursor-pointer" data-value="4"><i class="far fa-star"></i></span> | |
| <span class="star cursor-pointer" data-value="5"><i class="far fa-star"></i></span> | |
| </div> | |
| <input type="hidden" id="review-rating" value="0"> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-300 mb-2">Your Experience</label> | |
| <textarea id="review-description" class="w-full p-3 rounded-md bg-transparent border border-gray-600 text-gray-100 focus:border-primary-blue focus:glow" placeholder="Describe your learning experience..." rows="4"></textarea> | |
| </div> | |
| <button id="submit-review" class="btn btn-primary ripple w-full">Submit Feedback</button> | |
| </div> | |
| <div class="mt-6"> | |
| <h4 class="text-md font-orbitron text-primary-blue glow mb-4">Testimonials</h4> | |
| <div id="reviews-container" class="space-y-4"></div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| if (hobby.title === "Educating Others") { | |
| const stars = card.querySelectorAll('.star'); | |
| const ratingInput = card.querySelector('#review-rating'); | |
| stars.forEach(star => { | |
| star.addEventListener('click', () => { | |
| const rating = star.getAttribute('data-value'); | |
| ratingInput.value = rating; | |
| stars.forEach(s => { | |
| const value = s.getAttribute('data-value'); | |
| if (value <= rating) { | |
| s.innerHTML = '<i class="fas fa-star text-primary-blue glow"></i>'; | |
| } else { | |
| s.innerHTML = '<i class="far fa-star text-gray-400"></i>'; | |
| } | |
| }); | |
| }); | |
| }); | |
| card.querySelector('#submit-review').addEventListener('click', () => { | |
| const name = card.querySelector('#review-name').value.trim(); | |
| const rating = parseInt(card.querySelector('#review-rating').value); | |
| const description = card.querySelector('#review-description').value.trim(); | |
| if (!name || rating === 0 || !description) { | |
| alert('Please fill out all fields and select a rating.'); | |
| return; | |
| } | |
| fetch('/api/reviews', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name, rating, description }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.message) { | |
| alert('Thank you for your feedback!'); | |
| card.querySelector('#review-name').value = ''; | |
| card.querySelector('#review-description').value = ''; | |
| card.querySelector('#review-rating').value = '0'; | |
| stars.forEach(star => star.innerHTML = '<i class="far fa-star text-gray-400"></i>'); | |
| fetchReviews(card.querySelector('#reviews-container')); | |
| } else { | |
| alert('Error submitting feedback: ' + (data.error || 'Unknown error')); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error submitting review:', error); | |
| alert('Error submitting feedback. Please try again later.'); | |
| }); | |
| }); | |
| function fetchReviews(reviewsContainer) { | |
| fetch('/api/reviews') | |
| .then(response => response.json()) | |
| .then(reviews => { | |
| reviewsContainer.innerHTML = ''; | |
| if (reviews.length === 0) { | |
| reviewsContainer.innerHTML = '<p class="text-gray-400">No testimonials yet. Be the first to share your experience!</p>'; | |
| return; | |
| } | |
| reviews.forEach(review => { | |
| const reviewDiv = document.createElement('div'); | |
| reviewDiv.className = 'glassmorphic p-4 rounded-xl flex justify-between items-start'; | |
| reviewDiv.innerHTML = ` | |
| <div> | |
| <div class="flex items-center mb-2"> | |
| <span class="font-orbitron text-primary-blue glow mr-2">${review.name}</span> | |
| <div class="text-primary-blue flex"> | |
| ${'<i class="fas fa-star glow"></i>'.repeat(review.rating)} | |
| ${'<i class="far fa-star text-gray-400"></i>'.repeat(5 - review.rating)} | |
| </div> | |
| </div> | |
| <p class="text-gray-300 text-sm">${review.description}</p> | |
| </div> | |
| <button class="delete-review text-red-500 hover:text-red-700 glow" data-id="${review.id}"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| `; | |
| reviewsContainer.appendChild(reviewDiv); | |
| gsap.from(reviewDiv, { opacity: 0, y: 20, duration: 0.8, ease: 'power3.out' }); | |
| reviewDiv.querySelector('.delete-review').addEventListener('click', () => { | |
| const confirmDelete = confirm(`Are you sure you want to delete the review by ${review.name}?`); | |
| if (confirmDelete) { | |
| const password = prompt('Please enter the admin password to delete this review:'); | |
| if (!password) { | |
| alert('Password is required to delete a review.'); | |
| return; | |
| } | |
| fetch(`/api/reviews/delete/${review.id}`, { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ password }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.message) { | |
| alert(data.message); | |
| fetchReviews(reviewsContainer); | |
| } else { | |
| alert('Error deleting review: ' + (data.error || 'Unknown error')); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error deleting review:', error); | |
| alert('Error deleting review. Please try again later.'); | |
| }); | |
| } | |
| }); | |
| }); | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching reviews:', error); | |
| reviewsContainer.innerHTML = '<p class="text-red-500">Error loading testimonials. Please try again later.</p>'; | |
| }); | |
| } | |
| fetchReviews(card.querySelector('#reviews-container')); | |
| } | |
| }); | |
| }); | |
| // Render Projects with Modal | |
| const modal = document.getElementById('project-modal'); | |
| const modalClose = document.getElementById('modal-close'); | |
| fetchData('/api/projects', 'projects-grid', (data, container) => { | |
| data.forEach(project => { | |
| const card = document.createElement('div'); | |
| card.className = 'project-card glassmorphic rounded-xl overflow-hidden cursor-pointer hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="relative"> | |
| <img src="${project.image}" alt="${project.title}" class="w-full h-48 object-cover glow"> | |
| <div class="project-overlay absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100"> | |
| <i class="fas fa-search text-white text-2xl glow"></i> | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow mb-2">${project.title}</h3> | |
| <p class="text-gray-300 text-sm mb-4">${project.description.substring(0, 100)}...</p> | |
| <div class="flex flex-wrap gap-2"> | |
| ${project.technologies.map(tech => `<span class="tech-tag glassmorphic text-xs px-2 py-1 rounded-full text-primary-blue glow">${tech}</span>`).join('')} | |
| </div> | |
| </div> | |
| `; | |
| card.addEventListener('click', () => { | |
| const mediaContainer = document.getElementById('modal-media'); | |
| mediaContainer.innerHTML = project.video ? ` | |
| <div class="video-container"> | |
| <iframe src="${project.video}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen class="glow"></iframe> | |
| </div> | |
| ` : `<img src="${project.image}" alt="${project.title}" class="w-full h-48 sm:h-64 object-cover rounded-md glow">`; | |
| document.getElementById('modal-title').textContent = project.title; | |
| document.getElementById('modal-description').textContent = project.description; | |
| document.getElementById('modal-role').textContent = project.role; | |
| const contributionsContainer = document.getElementById('modal-contributions'); | |
| contributionsContainer.innerHTML = project.contributions.map(contribution => `<li>${contribution}</li>`).join(''); | |
| const techContainer = document.getElementById('modal-tech'); | |
| techContainer.innerHTML = project.technologies.map(tech => `<span class="tech-tag glassmorphic text-xs px-2 py-1 rounded-full text-primary-blue glow">${tech}</span>`).join(''); | |
| document.getElementById('modal-github').href = project.githubLink; | |
| document.getElementById('modal-live').href = project.liveLink; | |
| modal.classList.remove('hidden'); | |
| gsap.from(modal.querySelector('div'), { y: 100, opacity: 0, duration: 0.8, ease: 'power3.out' }); | |
| }); | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| }); | |
| }); | |
| modalClose.addEventListener('click', () => { | |
| modal.classList.add('hidden'); | |
| }); | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) modal.classList.add('hidden'); | |
| }); | |
| // Render Education | |
| fetchData('/api/education', 'education-grid', (data, container) => { | |
| data.forEach(edu => { | |
| const card = document.createElement('div'); | |
| card.className = 'education-card glassmorphic p-6 rounded-xl hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-4"> | |
| <div class="icon-wrapper p-3 glassmorphic rounded-full"> | |
| <i class="fas fa-graduation-cap text-primary-blue glow"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow">${edu.degree}</h3> | |
| <div class="period flex items-center text-gray-400"> | |
| <i class="fas fa-calendar mr-1"></i> | |
| <span>${edu.period}</span> | |
| </div> | |
| </div> | |
| <h4 class="text-primary-blue glow mb-2">${edu.institution}</h4> | |
| <p class="text-gray-300 mb-4">${edu.description}</p> | |
| <h5 class="font-orbitron text-primary-blue glow">Key Achievements:</h5> | |
| <ul class="list-disc pl-5 text-gray-300"> | |
| ${edu.achievements.map(achievement => `<li>${achievement}</li>`).join('')} | |
| </ul> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| }); | |
| }); | |
| // Render Certifications | |
| fetchData('/api/certifications', 'certifications-grid', (data, container) => { | |
| data.forEach(cert => { | |
| const card = document.createElement('div'); | |
| card.className = 'certification-card glassmorphic p-6 rounded-xl hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-4"> | |
| <div class="icon-wrapper p-3 glassmorphic rounded-full"> | |
| <i class="fas fa-certificate text-primary-blue glow"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow">${cert.title}</h3> | |
| <div class="period flex items-center text-gray-400"> | |
| <i class="fas fa-calendar mr-1"></i> | |
| <span>${cert.year}</span> | |
| </div> | |
| </div> | |
| <h4 class="text-primary-blue glow mb-2">${cert.platform}</h4> | |
| <p class="text-gray-300 mb-4">${cert.description}</p> | |
| <img src="${cert.badge}" alt="${cert.title} badge" class="w-24 h-24 object-contain mb-4 glow"> | |
| <a href="${cert.certificateLink}" target="_blank" rel="noopener noreferrer" class="btn btn-secondary ripple text-sm">Verify Certificate</a> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| }); | |
| }); | |
| // Render Volunteer Experience | |
| fetchData('/api/volunteer', 'volunteer-grid', (data, container) => { | |
| data.forEach(vol => { | |
| const card = document.createElement('div'); | |
| card.className = 'volunteer-card glassmorphic p-6 rounded-xl hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-4"> | |
| <div class="icon-wrapper p-3 glassmorphic rounded-full"> | |
| <i class="fas fa-hands-helping text-primary-blue glow"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow">${vol.role}</h3> | |
| <div class="period flex items-center text-gray-400"> | |
| <i class="fas fa-calendar mr-1"></i> | |
| <span>${vol.period}</span> | |
| </div> | |
| </div> | |
| <h4 class="text-primary-blue glow mb-2">${vol.organization}</h4> | |
| <p class="text-gray-300 mb-4">${vol.description}</p> | |
| <h5 class="font-orbitron text-primary-blue glow">Contributions:</h5> | |
| <ul class="list-disc pl-5 text-gray-300"> | |
| ${vol.contributions.map(contribution => `<li>${contribution}</li>`).join('')} | |
| </ul> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| }); | |
| }); | |
| // Render Talks | |
| fetchData('/api/talks', 'talks-grid', (data, container) => { | |
| data.forEach(talk => { | |
| const card = document.createElement('div'); | |
| card.className = 'talk-card glassmorphic p-6 rounded-xl hover:scale-105 transition-transform'; | |
| card.setAttribute('data-tilt', ''); | |
| card.setAttribute('data-tilt-max', '10'); | |
| card.innerHTML = ` | |
| <div class="flex items-start space-x-4"> | |
| <div class="icon-wrapper p-3 glassmorphic rounded-full"> | |
| <i class="fas fa-microphone text-primary-blue glow"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-lg font-orbitron text-primary-blue glow">${talk.title}</h3> | |
| <div class="date flex items-center text-gray-400"> | |
| <i class="fas fa-calendar mr-1"></i> | |
| <span>${talk.date}</span> | |
| </div> | |
| </div> | |
| <h4 class="text-primary-blue glow mb-2">${talk.event}</h4> | |
| <p class="text-gray-300 mb-4">${talk.description}</p> | |
| ${talk.image ? ` | |
| <div class="mb-4"> | |
| <img src="${talk.image}" alt="${talk.title} thumbnail" class="w-full h-48 object-cover rounded-md glow"> | |
| </div> | |
| ` : ''} | |
| <div class="video-container mb-4"> | |
| <iframe src="${talk.videoLink}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen class="glow"></iframe> | |
| </div> | |
| <a href="${talk.videoLink}" target="_blank" rel="noopener noreferrer" class="btn btn-primary ripple text-sm">Watch Full Talk</a> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| VanillaTilt.init(card); | |
| gsap.from(card, { scrollTrigger: { trigger: card, start: 'top 80%' }, y: 50, opacity: 0, duration: 1, ease: 'power3.out' }); | |
| }); | |
| }); | |
| // Handle Contact Form Submission | |
| const contactSubmitButton = document.getElementById('contact-submit'); | |
| const emailInput = document.getElementById('email'); | |
| const messageInput = document.getElementById('message'); | |
| contactSubmitButton.addEventListener('click', async () => { | |
| const email = emailInput.value.trim(); | |
| const message = messageInput.value.trim(); | |
| // Validate inputs | |
| if (!email || !message) { | |
| alert('Please fill in both email and message fields.'); | |
| return; | |
| } | |
| if (!/\S+@\S+\.\S+/.test(email)) { | |
| alert('Please enter a valid email address.'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/api/contact', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, message }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| alert(data.message); | |
| emailInput.value = ''; | |
| messageInput.value = ''; | |
| } catch (error) { | |
| console.error('Error submitting contact form:', error); | |
| alert('Error submitting form. If you\'re on Hugging Face Spaces, please use LinkedIn or GitHub to contact me.'); | |
| } | |
| }); |