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}