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; 6const numEntries = 0; 7 8// SEE HERE FOR DATE FORMAT INFO: 9// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format 10const startDate = '2026-02-01T00:00:00'; 11const endDate = '2026-02-28T23:59:59'; 12 13// Date formatting stuff. You probably only need to touch locale and time zone. 14const locale = 'en-US'; 15const timeZone = 'America/Chicago'; 16const dateOptions = { 17 month: 'long', 18 day: 'numeric', 19 year: 'numeric', 20 hour: 'numeric', 21 minute: '2-digit', 22 timeZone: timeZone 23}; 24 25// DON'T EDIT BELOW THIS LINE 26// unless you know what you're doing. 27 28const start = new Date(startDate); 29const end = new Date(endDate); 30 31const startDateElt = document.getElementById('startDate'); 32const endDateElt = document.getElementById('endDate'); 33const dateElt = document.getElementById('dates'); 34 35const joinedElt = document.getElementById('joinedCount'); 36const entriesElt = document.getElementById('entriesCount'); 37 38const daysElt = document.getElementById('days'); 39const hoursElt = document.getElementById('hours'); 40const minutesElt = document.getElementById('minutes'); 41const secondsElt = document.getElementById('seconds'); 42 43const startString = start.toLocaleString(locale, dateOptions); 44const endString = end.toLocaleString(locale, dateOptions); 45 46const list = document.getElementById('list'); 47 48const dayMult = 24*60*60; 49const hourMult = 60*60; 50const minuteMult = 60; 51 52if (joinedElt) joinedElt.textContent = numJoined; 53if (entriesElt) entriesElt.textContent = numEntries; 54 55if (startDateElt) startDateElt.textContent = startString; 56if (endDateElt) endDateElt.textContent = endString; 57 58const countdownTick = () => { 59 const now = Date.now(); 60 let diff; 61 if (now < start.getTime()) { 62 // Jam hasn't started yet 63 diff = (start.getTime() - now) / 1000; // get total # of seconds 64 } else if (now < end.getTime()) { 65 // Jam has started but not ended 66 diff = (end.getTime() - now) / 1000; 67 } else { 68 // Jam has ended 69 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'}` 70 } 71 72 if (diff) { 73 const days = Math.floor(diff / dayMult); 74 diff = diff - (days * dayMult); 75 const hours = Math.floor(diff / hourMult); 76 diff = diff - (hours * hourMult); 77 const minutes = Math.floor(diff / minuteMult); 78 diff = diff - (minutes * minuteMult); 79 const seconds = Math.floor(diff); 80 daysElt.textContent = days; 81 hoursElt.textContent = hours; 82 minutesElt.textContent = minutes; 83 secondsElt.textContent = seconds; 84 } 85} 86 87if (document.querySelector('.clock')) { 88 countdownTick(); 89 setInterval(() => { 90 countdownTick(); 91 }, 1000); 92} 93 94if (list) { 95 list.innerHTML = games.map((item, i) => { 96 return `<div class="item" id="item-${i}"> 97 <div class="thumb"> 98 <img src="${item.thumbnail}" alt="${item.title} thumbnail image" /> 99 </div> 100 <h3><a href="${item.page}">${item.title}</a></h3> 101 <div class="authors">${item.authors.map((auth) => { return `<a href="${auth.link}" target="_blank">${auth.name}</a>`; }).join(", ")}</div> 102 <div class="blurb">${item.blurb}</div> 103</div>`; 104 }).join(""); 105} 106 107const afs = new AFS({ 108 // Required Selectors 109 containerSelector: 'main', 110 itemSelector: '.item', 111 filterButtonSelector: '.afs-btn-filter', 112 searchInputSelector: '.afs-filter-search', 113 counterSelector: '.afs-filter-counter', 114 115 // CSS Classes 116 activeClass: 'active', 117 hiddenClass: 'hidden', 118 transitionClass: 'afs-transition', 119 120 // Filter & Search Configuration 121 filterMode: 'OR', // or 'AND' 122 groupMode: 'AND', // or 'OR' 123 searchKeys: ['title', 'categories', 'tags'], 124 debounceTime: 200, // search input delay 125 126 // Debug Options 127 debug: false, 128 logLevel: 'info', // 'debug', 'info', 'warn', 'error' 129 130 // Date Handling 131 dateFormat: 'YYYY-MM-DD', 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 // Slider Configuration 143 slider: { 144 containerClass: 'afs-range-slider', 145 trackClass: 'afs-range-track', 146 thumbClass: 'afs-range-thumb', 147 valueClass: 'afs-range-value', 148 selectedClass: 'afs-range-selected', 149 ui: { 150 showHistogram: false, 151 bins: 10, 152 track: { 153 radius: '0', 154 background: '#e5e7eb' 155 }, 156 selected: { 157 background: '#000' 158 }, 159 thumb: { 160 radius: '50%', 161 size: '16px', 162 background: '#000' 163 }, 164 histogram: { 165 background: '#e5e7eb', 166 bar: { 167 background: '#000' 168 } 169 } 170 } 171 }, 172 173 // Pagination Configuration 174 pagination: { 175 enabled: false, 176 itemsPerPage: 10, 177 container: '.afs-pagination-container', 178 pageButtonClass: 'afs-page-button', 179 activePageClass: 'afs-page-active', 180 containerClass: 'afs-pagination', 181 scrollToTop: false, 182 scrollOffset: 50, 183 scrollBehavior: 'smooth' // or 'auto' 184 }, 185 186 // Animation Configuration 187 animation: { 188 type: 'fade', 189 duration: 200, 190 easing: 'ease-out', 191 inClass: 'afs-animation-enter', 192 outClass: 'afs-animation-leave' 193 }, 194 195 // Lifecycle Options 196 responsive: true, 197 preserveState: false, 198 stateExpiry: 86400000, // 24 hours 199 observeDOM: false, 200 201 // Style Configuration 202 styles: { 203 colors: { 204 primary: '#000', 205 background: '#e5e7eb', 206 text: '#000', 207 textHover: '#fff' 208 } 209 } 210});