@@ -5,6 +5,9 @@ function randBetween(min, max) {
55 return Math . random ( ) * ( max - min ) + min ;
66}
77
8+ /** Row unit must match grid-auto-rows in CSS (px) */
9+ const MEMORIAL_GRID_ROW_UNIT = 4 ;
10+
811/** Build a memorial tile card for a single dog */
912function buildMemorialCard ( meta , body ) {
1013 // Do NOT recalculate age — memorial dogs show the age at time of passing,
@@ -26,7 +29,7 @@ function buildMemorialCard(meta, body) {
2629
2730 const photoArea = meta . image
2831 ? `<div class="dog-tile-photo">
29- <img src="${ meta . image } " alt="${ esc ( rawName ) } " class="dog-photo memorial-photo" loading="lazy">
32+ <img src="${ meta . image } " alt="${ esc ( rawName ) } " class="dog-photo memorial-photo" loading="lazy" onerror="_cldImgError(this)" >
3033 </div>`
3134 : `<div class="dog-tile-emoji" style="background:${ meta . bgLight || '' } ">
3235 <span class="dog-emoji-big">${ meta . emoji || '🐕' } </span>
@@ -93,18 +96,20 @@ function initMemorialPhotos() {
9396
9497/** Watch grid for width changes and recalculate spans */
9598function initMemorialMasonryResize ( ) {
96- const grid = document . getElementById ( ' memoriam-grid' ) ;
97- if ( ! grid || ! window . ResizeObserver ) return ;
99+ const grids = Array . from ( document . querySelectorAll ( '# memoriam-grid, #memoriam-grid-asawarpur' ) ) ;
100+ if ( grids . length === 0 || ! window . ResizeObserver ) return ;
98101 const ro = new ResizeObserver ( ( ) => {
99- grid . querySelectorAll ( '.dog-card' ) . forEach ( setCardSpan ) ;
102+ grids . forEach ( grid => {
103+ grid . querySelectorAll ( '.dog-card' ) . forEach ( setCardSpan ) ;
104+ } ) ;
100105 } ) ;
101- ro . observe ( grid ) ;
106+ grids . forEach ( grid => ro . observe ( grid ) ) ;
102107}
103108
104109/** Open a full-colour memorial modal when a card is clicked */
105110function initMemorialModals ( ) {
106- const grid = document . getElementById ( ' memoriam-grid' ) ;
107- if ( ! grid ) return ;
111+ const grids = Array . from ( document . querySelectorAll ( '# memoriam-grid, #memoriam-grid-asawarpur' ) ) ;
112+ if ( grids . length === 0 ) return ;
108113
109114 const isTouch = 'ontouchstart' in window || navigator . maxTouchPoints > 0 ;
110115 let lastTouchOpenAt = 0 ;
@@ -135,79 +140,89 @@ function initMemorialModals() {
135140 } ) ;
136141 }
137142
138- grid . addEventListener ( 'touchstart' , e => {
139- const card = e . target . closest ( '.dog-card' ) ;
140- if ( ! card ) return ;
141- activeCard = card ;
142- didLongPress = false ;
143- const touch = e . touches [ 0 ] ;
144- startX = touch . clientX ;
145- startY = touch . clientY ;
146-
147- pressTimer = setTimeout ( ( ) => {
148- didLongPress = true ;
149- card . classList . add ( 'lit' ) ;
150- if ( navigator . vibrate ) navigator . vibrate ( 50 ) ;
151- } , LONG_PRESS_MS ) ;
152- } , { passive : true } ) ;
153-
154- grid . addEventListener ( 'touchmove' , e => {
155- if ( ! pressTimer ) return ;
156- const touch = e . touches [ 0 ] ;
157- const dx = Math . abs ( touch . clientX - startX ) ;
158- const dy = Math . abs ( touch . clientY - startY ) ;
159- if ( dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD ) {
143+ grids . forEach ( grid => {
144+ if ( grid . dataset . modalBound === '1' ) return ;
145+ grid . dataset . modalBound = '1' ;
146+
147+ grid . addEventListener ( 'touchstart' , e => {
148+ const card = e . target . closest ( '.dog-card' ) ;
149+ if ( ! card ) return ;
150+ activeCard = card ;
151+ didLongPress = false ;
152+ const touch = e . touches [ 0 ] ;
153+ startX = touch . clientX ;
154+ startY = touch . clientY ;
155+
156+ pressTimer = setTimeout ( ( ) => {
157+ didLongPress = true ;
158+ card . classList . add ( 'lit' ) ;
159+ if ( navigator . vibrate ) navigator . vibrate ( 50 ) ;
160+ } , LONG_PRESS_MS ) ;
161+ } , { passive : true } ) ;
162+
163+ grid . addEventListener ( 'touchmove' , e => {
164+ if ( ! pressTimer ) return ;
165+ const touch = e . touches [ 0 ] ;
166+ const dx = Math . abs ( touch . clientX - startX ) ;
167+ const dy = Math . abs ( touch . clientY - startY ) ;
168+ if ( dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD ) {
169+ clearTimeout ( pressTimer ) ;
170+ pressTimer = null ;
171+ }
172+ } , { passive : true } ) ;
173+
174+ grid . addEventListener ( 'touchend' , ( ) => {
160175 clearTimeout ( pressTimer ) ;
161176 pressTimer = null ;
162- }
163- } , { passive : true } ) ;
177+ if ( didLongPress ) {
178+ didLongPress = false ;
179+ if ( activeCard ) activeCard . classList . remove ( 'lit' ) ;
180+ activeCard = null ;
181+ return ;
182+ }
183+ if ( activeCard ) {
184+ openMemorialModal ( activeCard ) ;
185+ lastTouchOpenAt = Date . now ( ) ;
186+ }
187+ activeCard = null ;
188+ } ) ;
164189
165- grid . addEventListener ( 'touchend' , e => {
166- clearTimeout ( pressTimer ) ;
167- pressTimer = null ;
168- if ( didLongPress ) {
169- didLongPress = false ;
190+ grid . addEventListener ( 'touchcancel' , ( ) => {
191+ clearTimeout ( pressTimer ) ;
192+ pressTimer = null ;
170193 if ( activeCard ) activeCard . classList . remove ( 'lit' ) ;
194+ didLongPress = false ;
171195 activeCard = null ;
172- return ;
173- }
174- if ( activeCard ) {
175- openMemorialModal ( activeCard ) ;
176- lastTouchOpenAt = Date . now ( ) ;
177- }
178- activeCard = null ;
179- } ) ;
180-
181- grid . addEventListener ( 'touchcancel' , ( ) => {
182- clearTimeout ( pressTimer ) ;
183- pressTimer = null ;
184- if ( activeCard ) activeCard . classList . remove ( 'lit' ) ;
185- didLongPress = false ;
186- activeCard = null ;
187- } ) ;
196+ } ) ;
188197
189- // Suppress native context menu on long press within the grid
190- grid . addEventListener ( 'contextmenu' , e => {
191- if ( e . target . closest ( '.dog-card' ) ) e . preventDefault ( ) ;
198+ // Suppress native context menu on long press within the grid
199+ grid . addEventListener ( 'contextmenu' , e => {
200+ if ( e . target . closest ( '.dog-card' ) ) e . preventDefault ( ) ;
201+ } ) ;
192202 } ) ;
193203 }
194204
195205 /* ── Desktop: click opens modal (hover handles candle/colour via CSS) ── */
196- grid . addEventListener ( 'click' , e => {
197- // Ignore synthetic click events that follow a touch tap.
198- if ( Date . now ( ) - lastTouchOpenAt < 700 ) return ;
199- const card = e . target . closest ( '.dog-card' ) ;
200- if ( ! card ) return ;
201- openMemorialModal ( card ) ;
202- } ) ;
206+ grids . forEach ( grid => {
207+ if ( grid . dataset . modalBound === '1' ) return ;
208+ grid . dataset . modalBound = '1' ;
203209
204- // Keyboard support for focused cards.
205- grid . addEventListener ( 'keydown' , e => {
206- if ( e . key !== 'Enter' && e . key !== ' ' ) return ;
207- const card = e . target . closest ( '.dog-card' ) ;
208- if ( ! card ) return ;
209- e . preventDefault ( ) ;
210- openMemorialModal ( card ) ;
210+ grid . addEventListener ( 'click' , e => {
211+ // Ignore synthetic click events that follow a touch tap.
212+ if ( Date . now ( ) - lastTouchOpenAt < 700 ) return ;
213+ const card = e . target . closest ( '.dog-card' ) ;
214+ if ( ! card ) return ;
215+ openMemorialModal ( card ) ;
216+ } ) ;
217+
218+ // Keyboard support for focused cards.
219+ grid . addEventListener ( 'keydown' , e => {
220+ if ( e . key !== 'Enter' && e . key !== ' ' ) return ;
221+ const card = e . target . closest ( '.dog-card' ) ;
222+ if ( ! card ) return ;
223+ e . preventDefault ( ) ;
224+ openMemorialModal ( card ) ;
225+ } ) ;
211226 } ) ;
212227}
213228
@@ -266,33 +281,105 @@ function openMemorialModal(card) {
266281/** Main entry: fetch manifest, load all memorial dogs, render to grid */
267282async function loadMemorial ( ) {
268283 const grid = document . getElementById ( 'memoriam-grid' ) ;
284+ let asawarpurGrid = document . getElementById ( 'memoriam-grid-asawarpur' ) ;
285+ let asawarpurSection = document . getElementById ( 'memoriam-asawarpur-section' ) ;
269286 if ( ! grid ) return ;
270287
288+ // Handle cached/older memoriam HTML by creating subsection nodes on the fly.
289+ if ( ! asawarpurSection || ! asawarpurGrid ) {
290+ const wrapper = grid . closest ( '.memoriam-page-wrap' ) ;
291+ if ( wrapper ) {
292+ if ( ! asawarpurSection ) {
293+ asawarpurSection = document . createElement ( 'div' ) ;
294+ asawarpurSection . id = 'memoriam-asawarpur-section' ;
295+ asawarpurSection . className = 'memoriam-subsection-block' ;
296+ asawarpurSection . style . display = 'none' ;
297+ asawarpurSection . innerHTML = `
298+ <h2 class="memoriam-subsection-title">Asawarpur Puppies</h2>
299+ <p class="memoriam-subsection-note">As an animal welfare club that operates within a limited jurisdiction, we are often called upon to assist with emergencies in neighboring areas. It was under such circumstances that we were asked to help with the burial of these puppies. Because of this, we never had the chance to truly know them. Our members have tried to recreate their likeness through illustrations drawn from memory, in the hope of keeping their memory alive. All of them were named posthumously.</p>
300+ ` ;
301+ wrapper . appendChild ( asawarpurSection ) ;
302+ }
303+ if ( ! asawarpurGrid ) {
304+ asawarpurGrid = document . createElement ( 'div' ) ;
305+ asawarpurGrid . id = 'memoriam-grid-asawarpur' ;
306+ asawarpurGrid . className = 'memoriam-grid' ;
307+ asawarpurGrid . style . display = 'none' ;
308+ wrapper . appendChild ( asawarpurGrid ) ;
309+ }
310+ }
311+ }
312+
271313 grid . innerHTML = '<div class="dogs-loading">🕯️ loading…</div>' ;
314+ if ( asawarpurGrid ) asawarpurGrid . innerHTML = '<div class="dogs-loading">🕯️ loading…</div>' ;
272315
273316 try {
274- const manifestRes = await fetch ( 'public/memorial/manifest.json' ) ;
317+ const bust = `?v=${ Date . now ( ) } ` ;
318+ const manifestRes = await fetch ( 'public/memorial/manifest.json' + bust ) ;
275319 if ( ! manifestRes . ok ) throw new Error ( 'memorial manifest not found' ) ;
276320 const { dogs : files } = await manifestRes . json ( ) ;
277321
278322 const results = await Promise . allSettled (
279323 files . map ( async filename => {
280- const res = await fetch ( 'public/memorial/content/' + filename ) ;
324+ const res = await fetch ( 'public/memorial/content/' + filename + bust ) ;
281325 if ( ! res . ok ) throw new Error ( 'Could not load ' + filename ) ;
282- return parseDogMd ( await res . text ( ) ) ;
326+ return { filename , parsed : parseDogMd ( await res . text ( ) ) } ;
283327 } )
284328 ) ;
285329
286- const cards = results
330+ const entries = results
287331 . filter ( r => r . status === 'fulfilled' )
288- . map ( r => buildMemorialCard ( r . value . meta , r . value . body ) ) ;
332+ . map ( r => r . value ) ;
289333
290- if ( cards . length === 0 ) {
334+ const asawarpurPuppyFiles = new Set ( [
335+ 'chocolate.md' ,
336+ 'moon.md' ,
337+ 'iris.md' ,
338+ 'gypsy.md' ,
339+ 'chikku.md'
340+ ] ) ;
341+
342+ const regularEntries = entries . filter ( entry => ! asawarpurPuppyFiles . has ( entry . filename ) ) ;
343+ const asawarpurEntries = entries . filter ( entry => asawarpurPuppyFiles . has ( entry . filename ) ) ;
344+
345+ const regularCards = regularEntries . map ( entry =>
346+ buildMemorialCard ( entry . parsed . meta , entry . parsed . body )
347+ ) ;
348+
349+ const asawarpurCards = asawarpurEntries . map ( entry =>
350+ buildMemorialCard ( entry . parsed . meta , entry . parsed . body )
351+ ) ;
352+
353+ if ( regularCards . length === 0 && asawarpurCards . length === 0 ) {
291354 grid . innerHTML = '<div class="dogs-loading" style="opacity:0.6">no entries yet 🕯️</div>' ;
355+ if ( asawarpurGrid ) {
356+ asawarpurGrid . style . display = 'none' ;
357+ }
358+ if ( asawarpurSection ) {
359+ asawarpurSection . style . display = 'none' ;
360+ }
292361 return ;
293362 }
294363
295- grid . innerHTML = cards . join ( '' ) ;
364+ grid . innerHTML = regularCards . length > 0
365+ ? regularCards . join ( '' )
366+ : '<div class="dogs-loading" style="opacity:0.6">no entries yet 🕯️</div>' ;
367+
368+ if ( asawarpurGrid ) {
369+ if ( asawarpurCards . length > 0 ) {
370+ asawarpurGrid . innerHTML = asawarpurCards . join ( '' ) ;
371+ asawarpurGrid . style . display = '' ;
372+ if ( asawarpurSection ) asawarpurSection . style . display = '' ;
373+ } else {
374+ asawarpurGrid . style . display = 'none' ;
375+ if ( asawarpurSection ) asawarpurSection . style . display = 'none' ;
376+ }
377+ } else {
378+ // Fallback for older cached HTML: append Asawarpur cards in main grid
379+ if ( asawarpurCards . length > 0 ) {
380+ grid . innerHTML = [ grid . innerHTML , ...asawarpurCards ] . join ( '' ) ;
381+ }
382+ }
296383 initMemorialPhotos ( ) ;
297384 initMemorialModals ( ) ;
298385 initMemorialMasonryResize ( ) ;
@@ -301,6 +388,9 @@ async function loadMemorial() {
301388 requestAnimationFrame ( ( ) => {
302389 requestAnimationFrame ( ( ) => {
303390 grid . querySelectorAll ( '.dog-card' ) . forEach ( setCardSpan ) ;
391+ if ( asawarpurGrid ) {
392+ asawarpurGrid . querySelectorAll ( '.dog-card' ) . forEach ( setCardSpan ) ;
393+ }
304394 } ) ;
305395 } ) ;
306396
0 commit comments