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});