templates for self-hosting game jams (or any other kind of jam tbh)
1import { AFS } from './afs.modern.js'; 2import games from '../data/games.json' with {type: 'json'}; 3 4// Basic variable replacements 5const numJoined = 0; 6 7// SEE HERE FOR DATE FORMAT INFO: 8// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format 9const startDate = '2026-02-01T00:00:00'; 10const endDate = '2026-02-28T23:59:59'; 11 12// Date formatting stuff. You probably only need to touch locale and time zone. 13const locale = 'en-US'; 14const timeZone = 'America/Chicago'; 15const dateOptions = { 16 month: 'long', 17 day: 'numeric', 18 year: 'numeric', 19 hour: 'numeric', 20 minute: '2-digit', 21 timeZone: timeZone 22}; 23 24// DON'T EDIT BELOW THIS LINE 25// unless you know what you're doing. 26 27const start = new Date(startDate); 28const end = new Date(endDate); 29 30const startDateElt = document.getElementById('startDate'); 31const endDateElt = document.getElementById('endDate'); 32const dateElt = document.getElementById('dates'); 33 34const joinedElt = document.getElementById('joinedCount'); 35const entriesElt = document.getElementById('entriesCount'); 36 37const daysElt = document.getElementById('days'); 38const hoursElt = document.getElementById('hours'); 39const minutesElt = document.getElementById('minutes'); 40const secondsElt = document.getElementById('seconds'); 41 42const startString = start.toLocaleString(locale, dateOptions); 43const endString = end.toLocaleString(locale, dateOptions); 44 45const list = document.getElementById('list'); 46 47const allTags = games.map(g => g.tags).reduce((g1, g2) => { 48 return g1.concat(g2); 49}).filter((t, i, arr) => { return arr.indexOf(t) === i }).sort(); 50 51const dayMult = 24*60*60; 52const hourMult = 60*60; 53const minuteMult = 60; 54 55if (joinedElt) joinedElt.textContent = numJoined; 56if (entriesElt) entriesElt.textContent = games.length; 57 58if (startDateElt) startDateElt.textContent = startString; 59if (endDateElt) endDateElt.textContent = endString; 60 61const countdownTick = () => { 62 const now = Date.now(); 63 let diff; 64 if (now < start.getTime()) { 65 // Jam hasn't started yet 66 diff = (start.getTime() - now) / 1000; // get total # of seconds 67 } else if (now < end.getTime()) { 68 // Jam has started but not ended 69 diff = (end.getTime() - now) / 1000; 70 } else { 71 // Jam has ended 72 dates.innerHTML = `The jam is now over. It ran from <b>${startString}</b> to <b>${endString}</b>. <a href="submissions.html">View ${numEntries} ${numEntries !== 1 ? 'entries' : 'entry'}` 73 } 74 75 if (diff) { 76 const days = Math.floor(diff / dayMult); 77 diff = diff - (days * dayMult); 78 const hours = Math.floor(diff / hourMult); 79 diff = diff - (hours * hourMult); 80 const minutes = Math.floor(diff / minuteMult); 81 diff = diff - (minutes * minuteMult); 82 const seconds = Math.floor(diff); 83 daysElt.textContent = days; 84 hoursElt.textContent = hours; 85 minutesElt.textContent = minutes; 86 secondsElt.textContent = seconds; 87 } 88} 89 90if (document.querySelector('.clock')) { 91 countdownTick(); 92 setInterval(() => { 93 countdownTick(); 94 }, 1000); 95} 96 97if (list) { 98 list.innerHTML = games.map((item, i) => { 99 return `<div class="item" id="item-${i}" data-authors="${item.authors.map(a => a.name)}" data-categories="${item.tags.map(t => 'tags:'+t).join(" ")} ${item.platforms.map(p => 'platforms:'+p).join(" ")}" data-title="${item.title}" data-date="${item.submitTime}"> 100 <div class="thumb"> 101 <a href="${item.page}"><img src="${item.thumbnail}" alt="${item.title} thumbnail image" /></a> 102 </div> 103 <h3><a href="${item.page}">${item.title}</a></h3> 104 <div class="authors">${item.authors.map((auth) => { return `<a href="${auth.link}" target="_blank">${auth.name}</a>`; }).join(", ")}</div> 105 <div class="blurb">${item.blurb}</div> 106</div>`; 107 }).join(""); 108 109 const tagsElt = document.getElementById('tags'); 110 tagsElt.innerHTML = `<button class="afs-btn-filter" data-filter="*">all</button>`+allTags.map((t) => { 111 return `<button class="afs-btn-filter" data-filter="tags:${t}">#${t}</button>` 112 }).join(""); 113 114 const afs = new AFS({ 115 // Required Selectors 116 containerSelector: '#list', 117 itemSelector: '.item', 118 119 // CSS Classes 120 activeClass: 'active', 121 hiddenClass: 'hidden', 122 transitionClass: 'afs-transition', 123 searchKeys: ['title', 'authors', 'tags'], 124 debounceTime: 200, // search input delay 125 126 // Date Handling 127 dateFormat: 'YYYY-MM-DD', 128 dateFilter: { 129 enabled: true, 130 format: 'YYYY-MM-DDThh:mm:ss' 131 }, 132 133 // Counter Configuration 134 counter: { 135 template: 'Showing {visible} of {total}', 136 showFiltered: true, 137 filteredTemplate: '({filtered} filtered)', 138 noResultsTemplate: 'No items found', 139 formatter: (num) => num.toLocaleString() 140 }, 141 142 sort: { 143 enabled: true, 144 buttonSelector: '.afs-btn-sort' 145 }, 146 147 // Animation Configuration 148 animation: { 149 type: 'fade', 150 duration: 200, 151 easing: 'ease-out', 152 inClass: 'afs-animation-enter', 153 outClass: 'afs-animation-leave' 154 }, 155 156 // Lifecycle Options 157 responsive: true, 158 preserveState: false, 159 stateExpiry: 86400000, // 24 hours 160 observeDOM: false, 161 162 // Style Configuration 163 styles: { 164 colors: { 165 primary: '#000', 166 background: '#e5e7eb', 167 text: '#000', 168 textHover: '#fff' 169 } 170 } 171 }); 172 173 // add date range filter 174 afs.dateFilter.addDateRange({ 175 key: 'date', 176 container: document.querySelector('#date-filter'), 177 format: 'YYYY-MM-DD', 178 minDate: new Date(startDate.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0]), 179 maxDate: new Date(endDate.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0]) 180 }); 181 182 // fixes some behavior of the sort buttons 183 document.querySelectorAll('button.custom-sort:not([data-sort-key="shuffle"])').forEach((elt) => { 184 elt.addEventListener('click', (e) => { 185 e.preventDefault(); 186 e.stopPropagation(); 187 const btn = e.target.closest('button'); 188 document.querySelectorAll('button.custom-sort:not([data-sort-key="'+btn.getAttribute('data-sort-key')+'"])').forEach((s) => { 189 s.classList.remove('sort-active'); 190 }); 191 if (btn.classList.contains('sort-active')) { 192 if (btn.getAttribute('data-sort-direction') === 'asc') { 193 btn.setAttribute('data-sort-direction', 'desc'); 194 btn.querySelector('img').src = './images/sort-desc.svg'; 195 } else { 196 btn.setAttribute('data-sort-direction', 'asc'); 197 btn.querySelector('img').src = './images/sort-asc.svg'; 198 } 199 afs.sort.sort(btn.getAttribute('data-sort-key'), btn.getAttribute('data-sort-direction')); 200 } else { 201 btn.classList.add('sort-active'); 202 afs.sort.sort(btn.getAttribute('data-sort-key'), btn.getAttribute('data-sort-direction')); 203 } 204 }); 205 }); 206 207 // random sort button 208 document.querySelector('[data-sort-key="shuffle"]').addEventListener('click', (e) => { 209 afs.sort.shuffle(); 210 }); 211}