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 filterButtonSelector: '.afs-btn-filter', 119 searchInputSelector: '.afs-filter-search', 120 counterSelector: '.afs-filter-counter', 121 122 // CSS Classes 123 activeClass: 'active', 124 hiddenClass: 'hidden', 125 transitionClass: 'afs-transition', 126 127 // Filter & Search Configuration 128 filterMode: 'OR', // or 'AND' 129 groupMode: 'AND', // or 'OR' 130 searchKeys: ['title', 'authors', 'tags'], 131 debounceTime: 200, // search input delay 132 133 // Date Handling 134 dateFormat: 'YYYY-MM-DD', 135 dateFilter: { 136 enabled: true, 137 format: 'YYYY-MM-DDThh:mm:ss' 138 }, 139 140 // Counter Configuration 141 counter: { 142 template: 'Showing {visible} of {total}', 143 showFiltered: true, 144 filteredTemplate: '({filtered} filtered)', 145 noResultsTemplate: 'No items found', 146 formatter: (num) => num.toLocaleString() 147 }, 148 149 sort: { 150 enabled: true, 151 buttonSelector: '.afs-btn-sort' 152 }, 153 154 filter: { 155 enabled: true, 156 buttonSelector: '.afs-btn-filter', 157 mode: 'AND', 158 activeClass: 'afs-active', 159 hiddenClass: 'afs-hidden' 160 }, 161 162 // Animation Configuration 163 animation: { 164 type: 'fade', 165 duration: 200, 166 easing: 'ease-out', 167 inClass: 'afs-animation-enter', 168 outClass: 'afs-animation-leave' 169 }, 170 171 // Lifecycle Options 172 responsive: true, 173 preserveState: false, 174 stateExpiry: 86400000, // 24 hours 175 observeDOM: false, 176 177 // Style Configuration 178 styles: { 179 colors: { 180 primary: '#000', 181 background: '#e5e7eb', 182 text: '#000', 183 textHover: '#fff' 184 } 185 } 186 }); 187 188 afs.dateFilter.addDateRange({ 189 key: 'date', 190 container: document.querySelector('#date-filter'), 191 format: 'YYYY-MM-DD', 192 minDate: new Date(startDate.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0]), 193 maxDate: new Date(endDate.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0]) 194 }); 195 196 afs.filter.setGroupMode('AND'); 197 198 document.querySelectorAll('button.custom-sort:not([data-sort-key="shuffle"])').forEach((elt) => { 199 elt.addEventListener('click', (e) => { 200 e.preventDefault(); 201 e.stopPropagation(); 202 const btn = e.target.closest('button'); 203 document.querySelectorAll('button.custom-sort:not([data-sort-key="'+btn.getAttribute('data-sort-key')+'"])').forEach((s) => { 204 s.classList.remove('sort-active'); 205 }); 206 if (btn.classList.contains('sort-active')) { 207 if (btn.getAttribute('data-sort-direction') === 'asc') { 208 btn.setAttribute('data-sort-direction', 'desc'); 209 btn.querySelector('img').src = './images/sort-desc.svg'; 210 } else { 211 btn.setAttribute('data-sort-direction', 'asc'); 212 btn.querySelector('img').src = './images/sort-asc.svg'; 213 } 214 afs.sort.sort(btn.getAttribute('data-sort-key'), btn.getAttribute('data-sort-direction')); 215 } else { 216 btn.classList.add('sort-active'); 217 afs.sort.sort(btn.getAttribute('data-sort-key'), btn.getAttribute('data-sort-direction')); 218 } 219 }); 220 }); 221 222 document.querySelector('[data-sort-key="shuffle"]').addEventListener('click', (e) => { 223 afs.sort.shuffle(); 224 }); 225}