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