171 lines
6.1 KiB
JavaScript
171 lines
6.1 KiB
JavaScript
|
|
// Home page specific JS: team carousel + modal
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
// Fetch team data and render
|
||
|
|
fetch('assets/data/team.json')
|
||
|
|
.then(res => res.json())
|
||
|
|
.then(members => {
|
||
|
|
initTeamSection(members);
|
||
|
|
})
|
||
|
|
.catch(err => {
|
||
|
|
console.error('Failed to load team data', err);
|
||
|
|
});
|
||
|
|
|
||
|
|
function initTeamSection(members) {
|
||
|
|
const carousel = document.getElementById('team-carousel');
|
||
|
|
const dotsContainer = document.getElementById('carousel-dots');
|
||
|
|
const prevBtn = document.getElementById('carousel-prev');
|
||
|
|
const nextBtn = document.getElementById('carousel-next');
|
||
|
|
|
||
|
|
// Render cards
|
||
|
|
carousel.innerHTML = members.map((m, i) => `
|
||
|
|
<div class="team-card" data-member="${i}">
|
||
|
|
<div class="team-avatar">
|
||
|
|
<div class="team-avatar-inner">${m.avatar}</div>
|
||
|
|
</div>
|
||
|
|
<h4>${m.name}</h4>
|
||
|
|
<p class="team-role">${m.role}</p>
|
||
|
|
<p class="team-desc">${(m.bio || '').split('.').shift()}.</p>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
|
||
|
|
// Carousel logic
|
||
|
|
const cardsContainer = carousel; // the track
|
||
|
|
let cards = carousel.querySelectorAll('.team-card');
|
||
|
|
let currentIndex = 0;
|
||
|
|
let cardsPerView = 3;
|
||
|
|
let autoPlayInterval;
|
||
|
|
|
||
|
|
function updateCardsPerView() {
|
||
|
|
if (window.innerWidth <= 768) cardsPerView = 1;
|
||
|
|
else if (window.innerWidth <= 1024) cardsPerView = 2;
|
||
|
|
else cardsPerView = 3;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createDots() {
|
||
|
|
dotsContainer.innerHTML = '';
|
||
|
|
const totalDots = Math.max(1, Math.ceil(cards.length / cardsPerView));
|
||
|
|
for (let i = 0; i < totalDots; i++) {
|
||
|
|
const dot = document.createElement('div');
|
||
|
|
dot.classList.add('carousel-dot');
|
||
|
|
if (i === 0) dot.classList.add('active');
|
||
|
|
dot.addEventListener('click', () => goToSlide(i));
|
||
|
|
dotsContainer.appendChild(dot);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateCarousel() {
|
||
|
|
cards = carousel.querySelectorAll('.team-card');
|
||
|
|
if (cards.length === 0) return;
|
||
|
|
const cardStyle = window.getComputedStyle(cards[0]);
|
||
|
|
const gap = 32; // matches previous implementation
|
||
|
|
const cardWidth = cards[0].offsetWidth + gap;
|
||
|
|
carousel.style.transform = `translateX(-${currentIndex * cardWidth}px)`;
|
||
|
|
|
||
|
|
const dots = dotsContainer.querySelectorAll('.carousel-dot');
|
||
|
|
const activeDotIndex = Math.floor(currentIndex / cardsPerView);
|
||
|
|
dots.forEach((dot, i) => dot.classList.toggle('active', i === activeDotIndex));
|
||
|
|
}
|
||
|
|
|
||
|
|
function goToSlide(dotIndex) {
|
||
|
|
currentIndex = dotIndex * cardsPerView;
|
||
|
|
if (currentIndex > cards.length - cardsPerView) currentIndex = Math.max(0, cards.length - cardsPerView);
|
||
|
|
updateCarousel();
|
||
|
|
resetAutoPlay();
|
||
|
|
}
|
||
|
|
|
||
|
|
function nextSlide() {
|
||
|
|
currentIndex++;
|
||
|
|
if (currentIndex > cards.length - cardsPerView) currentIndex = 0;
|
||
|
|
updateCarousel();
|
||
|
|
}
|
||
|
|
|
||
|
|
function prevSlide() {
|
||
|
|
currentIndex--;
|
||
|
|
if (currentIndex < 0) currentIndex = Math.max(0, cards.length - cardsPerView);
|
||
|
|
updateCarousel();
|
||
|
|
}
|
||
|
|
|
||
|
|
function startAutoPlay() {
|
||
|
|
clearInterval(autoPlayInterval);
|
||
|
|
autoPlayInterval = setInterval(nextSlide, 4000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resetAutoPlay() {
|
||
|
|
clearInterval(autoPlayInterval);
|
||
|
|
startAutoPlay();
|
||
|
|
}
|
||
|
|
|
||
|
|
prevBtn && prevBtn.addEventListener('click', () => { prevSlide(); resetAutoPlay(); });
|
||
|
|
nextBtn && nextBtn.addEventListener('click', () => { nextSlide(); resetAutoPlay(); });
|
||
|
|
|
||
|
|
carousel.addEventListener('mouseenter', () => clearInterval(autoPlayInterval));
|
||
|
|
carousel.addEventListener('mouseleave', startAutoPlay);
|
||
|
|
|
||
|
|
updateCardsPerView();
|
||
|
|
createDots();
|
||
|
|
startAutoPlay();
|
||
|
|
|
||
|
|
window.addEventListener('resize', () => {
|
||
|
|
updateCardsPerView();
|
||
|
|
createDots();
|
||
|
|
currentIndex = 0;
|
||
|
|
updateCarousel();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Modal logic
|
||
|
|
const modalOverlay = document.getElementById('team-modal-overlay');
|
||
|
|
const modalClose = document.getElementById('team-modal-close');
|
||
|
|
const modalAvatar = document.getElementById('modal-avatar');
|
||
|
|
const modalName = document.getElementById('modal-name');
|
||
|
|
const modalRole = document.getElementById('modal-role');
|
||
|
|
const modalBio = document.getElementById('modal-bio');
|
||
|
|
const modalStats = document.getElementById('modal-stats');
|
||
|
|
const modalSkills = document.getElementById('modal-skills');
|
||
|
|
const modalSocial = document.getElementById('modal-social');
|
||
|
|
|
||
|
|
function openMemberModal(index) {
|
||
|
|
const member = members[index];
|
||
|
|
if (!member) return;
|
||
|
|
modalAvatar.textContent = member.avatar || member.name.charAt(0);
|
||
|
|
modalName.textContent = member.name;
|
||
|
|
modalRole.textContent = member.role;
|
||
|
|
modalBio.textContent = member.bio;
|
||
|
|
|
||
|
|
modalStats.innerHTML = (member.stats || []).map(stat => `
|
||
|
|
<div class="team-modal-stat">
|
||
|
|
<div class="team-modal-stat-value">${stat.value}</div>
|
||
|
|
<div class="team-modal-stat-label">${stat.label}</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
|
||
|
|
modalSkills.innerHTML = (member.skills || []).map(s => `<span class="team-modal-skill">${s}</span>`).join('');
|
||
|
|
|
||
|
|
modalSocial.innerHTML = `
|
||
|
|
<a href="#" aria-label="Discord"><i data-feather="message-circle"></i></a>
|
||
|
|
<a href="#" aria-label="Twitter"><i data-feather="twitter"></i></a>
|
||
|
|
`;
|
||
|
|
|
||
|
|
if (window.feather && feather.replace) feather.replace();
|
||
|
|
|
||
|
|
modalOverlay.classList.add('active');
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeMemberModal() {
|
||
|
|
modalOverlay.classList.remove('active');
|
||
|
|
document.body.style.overflow = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Attach click listeners to generated cards
|
||
|
|
carousel.querySelectorAll('.team-card').forEach(card => {
|
||
|
|
card.addEventListener('click', () => {
|
||
|
|
const idx = parseInt(card.dataset.member, 10);
|
||
|
|
openMemberModal(idx);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
modalClose && modalClose.addEventListener('click', closeMemberModal);
|
||
|
|
modalOverlay && modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) closeMemberModal(); });
|
||
|
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalOverlay.classList.contains('active')) closeMemberModal(); });
|
||
|
|
}
|
||
|
|
});
|