Skip to content

Commit ee2ab97

Browse files
refac: meet the team page (#24)
* chore: update DS_Store * tejas nageshwaran * init: add team descriptions * feat (add): extra image support * chore: update gitignore * feat (add): major changes - added a new image card which can be changed by clicking on the smaller ones - added spirit dog hyperlink - increased card size to subpage standard for better viewing - * fix: spirit dog hyperlink * feat (add): enlarged image view on click * feat (ui): navigation - add card time sync for popup - add navigation buttons to move around page * remove: core team subtitle redundancy * remove: leadership from core section * feat (ui): mobile optimisation * chore: remove ds store --------- Co-authored-by: Nitin S <28nitin07@gmail.com>
1 parent 5246a09 commit ee2ab97

75 files changed

Lines changed: 387 additions & 117 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.DS_Store

-14 KB
Binary file not shown.

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ Pawsitive 2025-26 _ Dog Population Census - Sheet1.csv
22
/Pawsitive 2025-26 _ Dog Population Census
33
Pawsitive 2025-26 _ Dog Population Census - Current Census.csv
44
/Department Write-Ups
5-
*.env
5+
*.env
6+
_copy_census_images.py
7+
_copy_extra_images.py
8+
# Ignore macOS system files
9+
.DS_Store

js/team.js

Lines changed: 233 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,17 @@ function renderLeadershipCinema(sections, container) {
7272
const bio = member.body || 'Bio coming soon...';
7373
const hasImg = imageExists(m);
7474
const activeClass = idx === 0 ? ' active' : '';
75+
const images = [m.image, m.image2, m.image3, m.image4].filter(x => x && x.trim());
7576
cardsHtml += `<div class="cinema-card${activeClass}" data-idx="${idx}"
7677
data-name="${esc(m.name)}" data-role="${esc(m.role)}"
7778
data-batch="${esc(m.batch)}" data-bio="${esc(bio)}"
78-
data-image="${esc(m.image)}" data-section-label="${esc(member.sectionLabel)}">
79+
data-spirit-dog="${esc(m.spirit_dog || '')}"
80+
data-images="${esc(images.join('|'))}" data-section-label="${esc(member.sectionLabel)}">
7981
${cinemaAvatarHtml(m, idx, hasImg)}
8082
<div class="cinema-info">
8183
<h3 class="cinema-name">${esc(m.name)}</h3>
8284
<span class="cinema-role">${esc(m.role)}</span>
85+
${m.spirit_dog ? `<span class="cinema-spirit-dog">🐾 ${esc(m.spirit_dog)}</span>` : ''}
8386
</div>
8487
</div>`;
8588
dotsHtml += `<button class="cinema-dot${idx === 0 ? ' active' : ''}" data-idx="${idx}" aria-label="Go to ${esc(m.name)}"></button>`;
@@ -105,9 +108,17 @@ function renderCoreGrid(members, container) {
105108
members.forEach((member, idx) => {
106109
const m = member.meta;
107110
const hasImg = imageExists(m);
108-
html += `<div class="core-grid-item" style="--i:${idx}">
111+
const coreBio = member.body ? member.body.trim() : '';
112+
const coreSDog = m.spirit_dog || '';
113+
const hasPopup = coreBio || coreSDog;
114+
const coreImages = [m.image, m.image2, m.image3, m.image4].filter(x => x && x.trim());
115+
html += `<div class="core-grid-item${hasPopup ? ' core-grid-item--has-popup' : ''}" style="--i:${idx}"
116+
data-name="${esc(m.name)}" data-role=""
117+
data-batch="${esc(m.batch || '')}" data-bio="${esc(coreBio)}"
118+
data-spirit-dog="${esc(coreSDog)}" data-images="${esc(coreImages.join('|'))}">
109119
${coreAvatarHtml(m, idx, hasImg)}
110120
<span class="core-name">${esc(m.name)}</span>
121+
${coreSDog ? `<span class="core-spirit-dog">🐾 ${esc(coreSDog)}</span>` : ''}
111122
</div>`;
112123
});
113124
container.innerHTML = `<div class="core-grid">${html}</div>`;
@@ -141,6 +152,9 @@ function initLeaderCarousel() {
141152
dots.forEach((d, i) => d.classList.toggle('active', i === currentIdx));
142153
const sectionLabel = cards[currentIdx].dataset.sectionLabel;
143154
if (label && sectionLabel) label.textContent = sectionLabel;
155+
if (typeof window.__syncTeamPopupFromLeaderCard === 'function') {
156+
window.__syncTeamPopupFromLeaderCard(cards[currentIdx]);
157+
}
144158
resetProgress();
145159
}
146160

@@ -196,43 +210,221 @@ function initLeaderCarousel() {
196210
function initTeamPopup() {
197211
const overlay = document.getElementById('team-popup-overlay');
198212
if (!overlay) return;
199-
200-
const closePopup = () => overlay.classList.remove('active');
213+
const prevBtn = overlay.querySelector('.team-popup-nav-prev');
214+
const nextBtn = overlay.querySelector('.team-popup-nav-next');
215+
216+
const closePopup = () => {
217+
overlay.classList.remove('active');
218+
overlay.dataset.popupSource = '';
219+
overlay.dataset.currentName = '';
220+
};
201221
overlay.querySelector('.team-popup-close').addEventListener('click', closePopup);
202222
overlay.addEventListener('click', e => { if (e.target === overlay) closePopup(); });
203223
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePopup(); });
204224

205-
const leadershipSection = document.querySelector('[data-section="leadership"]');
206-
if (!leadershipSection) return;
207-
208-
leadershipSection.addEventListener('click', e => {
209-
const card = e.target.closest('.cinema-card');
210-
if (!card) return;
211-
212-
const name = card.dataset.name;
213-
const role = card.dataset.role;
214-
const batch = card.dataset.batch;
215-
const bio = card.dataset.bio;
216-
const image = card.dataset.image;
217-
225+
function openPopup(name, role, batch, bio, images, spiritDog, source = '') {
218226
const popupCard = overlay.querySelector('.team-popup-card');
219227
const avatarEl = popupCard.querySelector('.team-popup-avatar');
220-
const imgPath = image && image.trim();
228+
const stripEl = popupCard.querySelector('.team-popup-img-strip');
229+
let activeImageIndex = 0;
221230

222-
if (imgPath) {
223-
avatarEl.innerHTML = `<img class="team-popup-avatar-img" src="${esc(imgPath)}" alt="${esc(name)}"
231+
function setMain(src) {
232+
avatarEl.innerHTML = `<img class="team-popup-avatar-img" src="${esc(src)}" alt="${esc(name)}"
224233
onerror="this.remove();this.parentNode.innerHTML='<span class=\\'team-popup-emoji\\'>🐾</span>'">`;
234+
}
235+
236+
if (images.length) {
237+
activeImageIndex = 0;
238+
setMain(images[0]);
239+
avatarEl.classList.add('team-popup-avatar--zoomable');
240+
avatarEl.title = 'Click to enlarge';
241+
avatarEl.onclick = () => {
242+
if (typeof openDeptLightbox === 'function') {
243+
openDeptLightbox(images, activeImageIndex);
244+
}
245+
};
225246
} else {
226247
avatarEl.innerHTML = `<span class="team-popup-emoji">🐾</span>`;
248+
avatarEl.classList.remove('team-popup-avatar--zoomable');
249+
avatarEl.title = '';
250+
avatarEl.onclick = null;
251+
}
252+
253+
if (images.length > 1) {
254+
stripEl.classList.add('has-thumbs');
255+
stripEl.innerHTML = images.map((src, i) =>
256+
`<button class="popup-thumb${i === 0 ? ' active' : ''}" data-src="${esc(src)}" data-idx="${i}">
257+
<img src="${esc(src)}" alt="" loading="lazy">
258+
</button>`
259+
).join('');
260+
stripEl.querySelectorAll('.popup-thumb').forEach(btn => {
261+
btn.addEventListener('click', () => {
262+
stripEl.querySelectorAll('.popup-thumb').forEach(b => b.classList.remove('active'));
263+
btn.classList.add('active');
264+
activeImageIndex = Number(btn.dataset.idx || 0);
265+
setMain(btn.dataset.src);
266+
});
267+
});
268+
} else {
269+
stripEl.classList.remove('has-thumbs');
270+
stripEl.innerHTML = '';
227271
}
228272

229273
popupCard.querySelector('.team-popup-name').textContent = name;
230274
popupCard.querySelector('.team-popup-role').textContent = role;
231275
popupCard.querySelector('.team-popup-batch').textContent = batch;
232-
popupCard.querySelector('.team-popup-bio').textContent = bio;
233-
276+
const sdEl = popupCard.querySelector('.team-popup-spirit-dog');
277+
sdEl.innerHTML = '';
278+
if (spiritDog) {
279+
const lbl = document.createElement('span');
280+
lbl.className = 'team-popup-spirit-dog-label';
281+
lbl.textContent = 'spirit dog: ';
282+
sdEl.appendChild(lbl);
283+
284+
const dogNames = spiritDog
285+
.split('/')
286+
.map(s => s.trim())
287+
.filter(Boolean);
288+
289+
dogNames.forEach((dogName, idx) => {
290+
const a = document.createElement('a');
291+
a.className = 'team-popup-spirit-dog-link';
292+
a.textContent = dogName;
293+
a.href = '#';
294+
a.addEventListener('click', async e => {
295+
e.preventDefault();
296+
closePopup();
297+
await showPage('dogs');
298+
299+
// Wait until dog cards are rendered, then open the matched dog card directly.
300+
function tryOpenDog() {
301+
const input = document.getElementById('dogs-search');
302+
const grid = document.getElementById('dogs-grid');
303+
if (input && grid && grid.querySelector('.dog-card')) {
304+
input.value = '';
305+
input.dispatchEvent(new Event('input'));
306+
307+
const cards = [...grid.querySelectorAll('.dog-card')];
308+
const target = dogName.trim().toLowerCase();
309+
const exact = cards.find(c => (c.dataset.name || '').trim().toLowerCase() === target);
310+
const starts = cards.find(c => (c.dataset.name || '').trim().toLowerCase().startsWith(target));
311+
const partial = cards.find(c => (c.dataset.name || '').trim().toLowerCase().includes(target));
312+
const match = exact || starts || partial;
313+
if (match) {
314+
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
315+
setTimeout(() => match.click(), 120);
316+
}
317+
} else {
318+
setTimeout(tryOpenDog, 80);
319+
}
320+
}
321+
322+
setTimeout(tryOpenDog, 0);
323+
});
324+
325+
sdEl.appendChild(a);
326+
if (idx < dogNames.length - 1) {
327+
sdEl.appendChild(document.createTextNode(' / '));
328+
}
329+
});
330+
}
331+
popupCard.querySelector('.team-popup-bio').textContent = bio;
332+
overlay.dataset.popupSource = source;
333+
overlay.dataset.currentName = name;
234334
overlay.classList.add('active');
235-
});
335+
336+
if (prevBtn) prevBtn.disabled = false;
337+
if (nextBtn) nextBtn.disabled = false;
338+
}
339+
340+
function openPopupFromCard(card, source = 'leadership') {
341+
if (!card) return;
342+
openPopup(
343+
card.dataset.name,
344+
card.dataset.role,
345+
card.dataset.batch,
346+
card.dataset.bio,
347+
(card.dataset.images || '').split('|').filter(Boolean),
348+
card.dataset.spiritDog || '',
349+
source
350+
);
351+
}
352+
353+
// Used by the leadership carousel so an open leadership popup tracks auto/manual slide changes.
354+
window.__syncTeamPopupFromLeaderCard = card => {
355+
if (!overlay.classList.contains('active')) return;
356+
if (overlay.dataset.popupSource !== 'leadership') return;
357+
openPopupFromCard(card, 'leadership');
358+
};
359+
360+
function stepCorePopup(dir) {
361+
const coreItems = [...document.querySelectorAll('[data-section="core"] .core-grid-item--has-popup')];
362+
if (!coreItems.length) return;
363+
const currentName = (overlay.dataset.currentName || '').trim();
364+
let idx = coreItems.findIndex(item => (item.dataset.name || '').trim() === currentName);
365+
if (idx < 0) idx = 0;
366+
const nextIdx = (idx + dir + coreItems.length) % coreItems.length;
367+
const item = coreItems[nextIdx];
368+
openPopup(
369+
item.dataset.name,
370+
item.dataset.role,
371+
item.dataset.batch,
372+
item.dataset.bio,
373+
(item.dataset.images || '').split('|').filter(Boolean),
374+
item.dataset.spiritDog || '',
375+
'core'
376+
);
377+
}
378+
379+
if (prevBtn) {
380+
prevBtn.addEventListener('click', () => {
381+
if (!overlay.classList.contains('active')) return;
382+
if (overlay.dataset.popupSource === 'leadership') {
383+
const arrow = document.querySelector('.cinema-carousel .cinema-nav-arrow[data-dir="prev"]');
384+
if (arrow) arrow.click();
385+
} else if (overlay.dataset.popupSource === 'core') {
386+
stepCorePopup(-1);
387+
}
388+
});
389+
}
390+
391+
if (nextBtn) {
392+
nextBtn.addEventListener('click', () => {
393+
if (!overlay.classList.contains('active')) return;
394+
if (overlay.dataset.popupSource === 'leadership') {
395+
const arrow = document.querySelector('.cinema-carousel .cinema-nav-arrow[data-dir="next"]');
396+
if (arrow) arrow.click();
397+
} else if (overlay.dataset.popupSource === 'core') {
398+
stepCorePopup(1);
399+
}
400+
});
401+
}
402+
403+
const leadershipSection = document.querySelector('[data-section="leadership"]');
404+
if (leadershipSection) {
405+
leadershipSection.addEventListener('click', e => {
406+
const card = e.target.closest('.cinema-card');
407+
if (!card) return;
408+
openPopupFromCard(card, 'leadership');
409+
});
410+
}
411+
412+
const coreSection = document.querySelector('[data-section="core"]');
413+
if (coreSection) {
414+
coreSection.addEventListener('click', e => {
415+
const item = e.target.closest('.core-grid-item--has-popup');
416+
if (!item) return;
417+
openPopup(
418+
item.dataset.name,
419+
item.dataset.role,
420+
item.dataset.batch,
421+
item.dataset.bio,
422+
(item.dataset.images || '').split('|').filter(Boolean),
423+
item.dataset.spiritDog || '',
424+
'core'
425+
);
426+
});
427+
}
236428
}
237429

238430
/* ── Main loader ── */
@@ -295,3 +487,21 @@ async function loadTeam() {
295487
⚠️ couldn't load team data. try refreshing.</div>`;
296488
}
297489
}
490+
491+
function enableTouchGestures(carousel) {
492+
let startX;
493+
carousel.addEventListener('touchstart', (e) => {
494+
startX = e.touches[0].clientX;
495+
});
496+
carousel.addEventListener('touchmove', (e) => {
497+
if (!startX) return;
498+
const diffX = startX - e.touches[0].clientX;
499+
if (Math.abs(diffX) > 50) {
500+
const dir = diffX > 0 ? 'next' : 'prev';
501+
carousel.querySelector(`.cinema-nav-arrow[data-dir="${dir}"]`).click();
502+
startX = null;
503+
}
504+
});
505+
}
506+
507+
document.querySelectorAll('.cinema-carousel').forEach(enableTouchGestures);

pages/team.html

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
<head>
2+
<meta charset="UTF-8">
3+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
4+
<style>
5+
.team-tabs {
6+
display: flex;
7+
flex-wrap: wrap;
8+
justify-content: center;
9+
}
10+
.team-tab {
11+
flex: 1 1 auto;
12+
margin: 0.5rem;
13+
padding: 0.5rem;
14+
font-size: 1rem;
15+
}
16+
.team-photo-wrap img {
17+
max-width: 100%;
18+
height: auto;
19+
}
20+
.team-desc-block {
21+
padding: 1rem;
22+
font-size: 1rem;
23+
}
24+
</style>
25+
</head>
26+
127
<div class="page-header">
228
<h1>🌟 meet the dawgs</h1>
329
<p>the humans behind every feeding round, every event, every late-night vet run</p>
@@ -51,13 +77,23 @@ <h3>more than a club — a pack</h3>
5177

5278
<!-- ===== POPUP MODAL (populated by js/team.js) ===== -->
5379
<div class="team-popup-overlay" id="team-popup-overlay">
54-
<div class="team-popup-card">
55-
<button class="team-popup-close" aria-label="Close">&times;</button>
56-
<div class="team-popup-avatar"></div>
57-
<h3 class="team-popup-name"></h3>
58-
<span class="team-popup-role"></span>
59-
<span class="team-popup-batch"></span>
60-
<p class="team-popup-bio"></p>
80+
<div class="team-popup-frame">
81+
<button class="team-popup-nav team-popup-nav-prev" aria-label="Previous">&#8249;</button>
82+
<div class="team-popup-card">
83+
<button class="team-popup-close" aria-label="Close">&times;</button>
84+
<div class="team-popup-media">
85+
<div class="team-popup-avatar"></div>
86+
<div class="team-popup-img-strip"></div>
87+
</div>
88+
<div class="team-popup-content">
89+
<h3 class="team-popup-name"></h3>
90+
<span class="team-popup-role"></span>
91+
<span class="team-popup-batch"></span>
92+
<span class="team-popup-spirit-dog"></span>
93+
<p class="team-popup-bio"></p>
94+
</div>
95+
</div>
96+
<button class="team-popup-nav team-popup-nav-next" aria-label="Next">&#8250;</button>
6197
</div>
6298
</div>
6399

public/.DS_Store

-18 KB
Binary file not shown.

public/dogs/.DS_Store

-12 KB
Binary file not shown.

public/dogs/images/.DS_Store

-8 KB
Binary file not shown.

public/gallery/.DS_Store

-8 KB
Binary file not shown.

public/memorial/.DS_Store

-6 KB
Binary file not shown.

public/team/.DS_Store

-10 KB
Binary file not shown.

0 commit comments

Comments
 (0)