Skip to content

Commit fb68245

Browse files
committed
feat (ui): mobile optimization for memoriam page
1 parent ec4138a commit fb68245

2 files changed

Lines changed: 201 additions & 5 deletions

File tree

js/memoriam.js

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,96 @@ function initMemorialModals() {
101101
const grid = document.getElementById('memoriam-grid');
102102
if (!grid) return;
103103

104+
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
105+
const LONG_PRESS_MS = 500;
106+
const MOVE_THRESHOLD = 10; // px — cancel if finger moves too far
107+
108+
/* ── Touch: long-press toggles .lit, quick tap opens modal ── */
109+
if (isTouch) {
110+
let pressTimer = null;
111+
let startX = 0, startY = 0;
112+
let didLongPress = false;
113+
let activeCard = null;
114+
115+
// Show a one-time hint on first visit
116+
const hintKey = 'pawsitive-memoriam-hint-shown';
117+
if (!sessionStorage.getItem(hintKey)) {
118+
requestAnimationFrame(() => {
119+
const hint = document.createElement('div');
120+
hint.className = 'memoriam-hint';
121+
hint.textContent = 'hold a card to light a candle 🕯️';
122+
document.body.appendChild(hint);
123+
setTimeout(() => hint.classList.add('show'), 600);
124+
setTimeout(() => {
125+
hint.classList.remove('show');
126+
setTimeout(() => hint.remove(), 500);
127+
}, 4000);
128+
sessionStorage.setItem(hintKey, '1');
129+
});
130+
}
131+
132+
grid.addEventListener('touchstart', e => {
133+
const card = e.target.closest('.dog-card');
134+
if (!card) return;
135+
activeCard = card;
136+
didLongPress = false;
137+
const touch = e.touches[0];
138+
startX = touch.clientX;
139+
startY = touch.clientY;
140+
141+
pressTimer = setTimeout(() => {
142+
didLongPress = true;
143+
card.classList.toggle('lit');
144+
if (navigator.vibrate) navigator.vibrate(50);
145+
}, LONG_PRESS_MS);
146+
}, { passive: true });
147+
148+
grid.addEventListener('touchmove', e => {
149+
if (!pressTimer) return;
150+
const touch = e.touches[0];
151+
const dx = Math.abs(touch.clientX - startX);
152+
const dy = Math.abs(touch.clientY - startY);
153+
if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
154+
clearTimeout(pressTimer);
155+
pressTimer = null;
156+
}
157+
}, { passive: true });
158+
159+
grid.addEventListener('touchend', e => {
160+
clearTimeout(pressTimer);
161+
pressTimer = null;
162+
if (didLongPress) {
163+
didLongPress = false;
164+
return;
165+
}
166+
if (activeCard) openMemorialModal(activeCard);
167+
activeCard = null;
168+
});
169+
170+
grid.addEventListener('touchcancel', () => {
171+
clearTimeout(pressTimer);
172+
pressTimer = null;
173+
didLongPress = false;
174+
activeCard = null;
175+
});
176+
177+
// Suppress native context menu on long press within the grid
178+
grid.addEventListener('contextmenu', e => {
179+
if (e.target.closest('.dog-card')) e.preventDefault();
180+
});
181+
}
182+
183+
/* ── Desktop: click opens modal (hover handles candle/colour via CSS) ── */
104184
grid.addEventListener('click', e => {
185+
if (isTouch) return;
105186
const card = e.target.closest('.dog-card');
106187
if (!card) return;
188+
openMemorialModal(card);
189+
});
190+
}
107191

192+
/** Build and show the memorial detail modal */
193+
function openMemorialModal(card) {
108194
const dark = document.documentElement.dataset.theme === 'dark';
109195
const bg = dark ? (card.dataset.bgDark || '') : (card.dataset.bgLight || '');
110196

@@ -121,8 +207,6 @@ function initMemorialModals() {
121207
.map(t => `<span class="dog-modal-tag">${t}</span>`)
122208
.join('');
123209

124-
/* Modal photo — rendered WITHOUT the grayscale filter because the overlay
125-
is appended directly to <body>, outside .memoriam-page-wrap */
126210
const photoSection = card.dataset.image
127211
? `<div class="dog-modal-photo memorial-modal-photo-wrap">
128212
<img src="${card.dataset.image}" alt="${card.dataset.name}" style="opacity:0;transition:opacity 0.3s" onload="this.style.opacity=1">
@@ -156,7 +240,6 @@ function initMemorialModals() {
156240
document.addEventListener('keydown', e => {
157241
if (e.key === 'Escape') close();
158242
}, { signal: ac.signal });
159-
});
160243
}
161244

162245
/** Main entry: fetch manifest, load all memorial dogs, render to grid */

styles/main.css

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,60 @@ footer .footer-sep {
937937
.lb-close { width: 2.2rem; height: 2.2rem; }
938938
.video-modal-box { max-width: 95vw; border-radius: 12px; }
939939

940+
/* IN MEMORIAM — uniform grid like dogs, no masonry */
941+
.memoriam-header-section { padding: 2.5rem 1.2rem 1.2rem; }
942+
.memoriam-grid {
943+
grid-template-columns: repeat(2, 1fr);
944+
grid-auto-rows: auto;
945+
gap: 0.5rem;
946+
padding: 0 1rem 2rem;
947+
margin: 0.5rem auto;
948+
}
949+
.memorial-card {
950+
margin: 0;
951+
box-shadow: 2px 2px 0 var(--sketch);
952+
grid-row-end: auto !important;
953+
}
954+
.memorial-card .dog-tile-photo {
955+
aspect-ratio: 1 / 1 !important;
956+
}
957+
/* Disable hover color-reveal on touch — handled by long-press .lit class */
958+
.memorial-card:hover .memorial-card-inner {
959+
filter: grayscale(1) contrast(0.92);
960+
}
961+
.memorial-card:hover .memorial-candle {
962+
transform: none;
963+
filter: none;
964+
animation: candleFlicker 2.8s ease-in-out infinite;
965+
}
966+
/* Make candle tappable on mobile (larger touch target) */
967+
.memorial-candle {
968+
pointer-events: auto;
969+
padding: 0.5rem;
970+
margin: -0.5rem;
971+
font-size: 1.1rem;
972+
}
973+
974+
/* TOUCH TARGETS — ensure 48px minimum */
975+
.nav-link, .nav-donate {
976+
min-height: 48px;
977+
display: flex;
978+
align-items: center;
979+
}
980+
.chip {
981+
min-height: 44px;
982+
display: inline-flex;
983+
align-items: center;
984+
}
985+
.team-tab {
986+
min-height: 44px;
987+
display: inline-flex;
988+
align-items: center;
989+
}
990+
.form-group input, .form-group select, .form-group textarea {
991+
min-height: 48px;
992+
}
993+
940994
/* FOOTER */
941995
footer { padding: 1.5rem 1rem; font-size: 1rem; }
942996
}
@@ -1230,14 +1284,73 @@ footer .footer-sep {
12301284
/* ── Responsive tweaks ── */
12311285
@media (max-width: 480px) {
12321286
.memoriam-grid {
1233-
grid-template-columns: repeat(auto-fill, minmax(min(130px, 100%), 1fr));
1287+
grid-template-columns: repeat(2, 1fr);
1288+
gap: 0.4rem;
12341289
padding: 0.5rem 0.75rem 2rem;
12351290
}
12361291
.memoriam-header-section { padding: 3rem 1.2rem 1.2rem; }
12371292
}
12381293
@media (max-width: 360px) {
12391294
.memoriam-grid {
1240-
grid-template-columns: repeat(auto-fill, minmax(min(110px, 100%), 1fr));
1295+
grid-template-columns: repeat(2, 1fr);
1296+
gap: 0.35rem;
12411297
padding: 0.5rem 0.5rem 2rem;
12421298
}
12431299
}
1300+
1301+
/* ── .lit state — long-press colour reveal on touch devices ── */
1302+
.memorial-card.lit .memorial-card-inner {
1303+
filter: grayscale(0) contrast(1);
1304+
}
1305+
.memorial-card.lit .memorial-candle {
1306+
transform: scale(1.6);
1307+
filter: drop-shadow(0 0 4px rgba(255, 160, 0, 1))
1308+
drop-shadow(0 0 12px rgba(255, 80, 0, 0.65));
1309+
animation: candleLit 1s ease-in-out infinite;
1310+
}
1311+
1312+
/* ── Long-press hint toast ── */
1313+
.memoriam-hint {
1314+
position: fixed;
1315+
bottom: 5rem;
1316+
left: 50%;
1317+
transform: translateX(-50%) translateY(20px);
1318+
background: var(--card);
1319+
color: var(--ink);
1320+
border: 2px solid var(--border);
1321+
border-radius: 14px;
1322+
padding: 0.6rem 1.2rem;
1323+
font-family: 'Caveat', cursive;
1324+
font-size: 1.05rem;
1325+
box-shadow: 4px 4px 0 var(--sketch);
1326+
opacity: 0;
1327+
pointer-events: none;
1328+
transition: opacity 0.4s ease, transform 0.4s ease;
1329+
z-index: 500;
1330+
white-space: nowrap;
1331+
}
1332+
.memoriam-hint.show {
1333+
opacity: 1;
1334+
transform: translateX(-50%) translateY(0);
1335+
}
1336+
1337+
/* ── Landscape modal fix ── */
1338+
@media (orientation: landscape) and (max-height: 500px) {
1339+
.dog-modal-overlay, .memorial-modal-overlay {
1340+
align-items: center;
1341+
}
1342+
.dog-modal {
1343+
max-height: 95vh;
1344+
border-radius: 16px;
1345+
}
1346+
.dog-modal-photo {
1347+
aspect-ratio: 16/9;
1348+
max-height: 40vh;
1349+
}
1350+
.dog-modal-body {
1351+
padding: 0.8rem 1rem;
1352+
}
1353+
.dept-entry-modal-box {
1354+
max-height: 95vh;
1355+
}
1356+
}

0 commit comments

Comments
 (0)