Skip to content

Commit 7281cc7

Browse files
committed
feat: support embed of javascript files
- Big feature: dynamic loading and listing of JS files on every page As an example, take a look at server-time-to-local.js. It formats the time into a specific format that the user can select on the /account or /update-account routes (settings page). You can also add any number of JS files to /assets/js/ folder and they will all be loaded on every page (which at the same time is a limitation of this particular method). Every JS file is embedded into a virtual file system and then served from there to the client in a separate request. ---- - Updated the Dockerfile to use more modern Go. - Added pre-go-generated files, too.
1 parent f286c31 commit 7281cc7

File tree

13 files changed

+422
-44
lines changed

13 files changed

+422
-44
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.20-alpine AS builder
1+
FROM golang:1.24.1-alpine3.21 AS builder
22

33
WORKDIR /app
44
COPY go.mod .

assets/js/embed.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package jsEmbed
2+
3+
import "embed"
4+
5+
//go:embed *.js
6+
var Scripts embed.FS

assets/js/server-time-to-local.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
"use strict";
2+
3+
/**
4+
* @file Manages date and time formatting across the forum based on user preferences, stores them in localStorage.
5+
* Also adds preference controls to /account and /update-account pages.
6+
*/
7+
8+
window.addEventListener("DOMContentLoaded", () => {
9+
const LOCAL_STORAGE_DATE_KEY = "userPrefDateStyle";
10+
const LOCAL_STORAGE_TIME_KEY = "userPrefTimeStyle";
11+
const PREVIEW_ELEMENT_ID = "preview-time";
12+
const ACCOUNT_PAGE_PATHS = ["/account", "/update-account"];
13+
const DATE_TIME_STYLE_OPTIONS = ["short", "medium", "long", "full"];
14+
const DEFAULT_STYLE = "medium";
15+
16+
const timeElements = document.getElementsByTagName("time");
17+
let currentPreferences = getSavedPreferences();
18+
19+
/**
20+
* Retrieves date and time style preferences from localStorage.
21+
* @returns {{dateStyle: string, timeStyle: string}} The saved preferences or defaults.
22+
*/
23+
function getSavedPreferences() {
24+
return {
25+
dateStyle: localStorage.getItem(LOCAL_STORAGE_DATE_KEY) || DEFAULT_STYLE,
26+
timeStyle: localStorage.getItem(LOCAL_STORAGE_TIME_KEY) || DEFAULT_STYLE
27+
};
28+
}
29+
30+
/**
31+
* Saves date style preference to localStorage.
32+
* @param {string} dateStyle - The selected date style.
33+
*/
34+
function saveDateStylePreference(dateStyle) {
35+
localStorage.setItem(LOCAL_STORAGE_DATE_KEY, dateStyle);
36+
currentPreferences.dateStyle = dateStyle;
37+
}
38+
39+
/**
40+
* Saves time style preference to localStorage.
41+
* @param {string} timeStyle - The selected date style.
42+
*/
43+
function saveTimeStylePreference(timeStyle) {
44+
localStorage.setItem(LOCAL_STORAGE_TIME_KEY, timeStyle);
45+
currentPreferences.timeStyle = timeStyle;
46+
}
47+
48+
/**
49+
* Formats all <time> elements on the page with the current preferences.
50+
*/
51+
function formatAllDates() {
52+
if (!timeElements || timeElements.length === 0) {
53+
console.log("[server-time-to-local.js] No <time> elements found to format.");
54+
return;
55+
}
56+
57+
try {
58+
const formatter = new Intl.DateTimeFormat(navigator.language, {
59+
dateStyle: currentPreferences.dateStyle,
60+
timeStyle: currentPreferences.timeStyle
61+
});
62+
63+
for (const timeElement of timeElements) {
64+
const dateTimeString = timeElement.getAttribute("datetime");
65+
if (dateTimeString) {
66+
try {
67+
const date = new Date(dateTimeString);
68+
timeElement.innerText = formatter.format(date);
69+
} catch (error) {
70+
console.error(`[server-time-to-local.js] Error parsing date string "${dateTimeString}":`, error, timeElement);
71+
}
72+
}
73+
}
74+
} catch (error) {
75+
console.error("[server-time-to-local.js] Error creating Intl.DateTimeFormat or formatting dates:", error, currentPreferences);
76+
}
77+
}
78+
79+
/**
80+
* Updates the preview element with the current date/time formatted using current preferences.
81+
*/
82+
function updatePreview() {
83+
const previewElement = document.getElementById(PREVIEW_ELEMENT_ID);
84+
if (!previewElement) {
85+
return;
86+
}
87+
88+
try {
89+
const formatter = new Intl.DateTimeFormat(navigator.language, {
90+
dateStyle: currentPreferences.dateStyle,
91+
timeStyle: currentPreferences.timeStyle
92+
});
93+
previewElement.innerText = formatter.format(new Date());
94+
} catch (error) {
95+
console.error("[server-time-to-local.js] Error updating preview:", error, currentPreferences);
96+
}
97+
}
98+
99+
/**
100+
* Creates a labeled dropdown select element for date/time style options.
101+
* @param {string} id - The ID for the select element.
102+
* @param {string} labelText - The text for the associated label.
103+
* @param {string} selectedValue - The currently selected value.
104+
* @returns {{wrapper: HTMLDivElement, selectElement: HTMLSelectElement}} The container div and the select element.
105+
*/
106+
function createStyleDropdown(id, labelText, selectedValue) {
107+
const wrapper = document.createElement("div");
108+
const label = document.createElement("label");
109+
label.setAttribute("for", id);
110+
label.textContent = labelText;
111+
112+
const selectElement = document.createElement("select");
113+
selectElement.setAttribute("id", id);
114+
selectElement.setAttribute("name", id);
115+
116+
DATE_TIME_STYLE_OPTIONS.forEach(optionValue => {
117+
const option = document.createElement("option");
118+
option.value = optionValue;
119+
// Capitalize first letter for display
120+
option.textContent = optionValue.charAt(0).toUpperCase() + optionValue.slice(1);
121+
if (optionValue === selectedValue) {
122+
option.selected = true;
123+
}
124+
selectElement.appendChild(option);
125+
});
126+
127+
wrapper.appendChild(label);
128+
wrapper.appendChild(selectElement);
129+
return {
130+
wrapper,
131+
selectElement
132+
};
133+
}
134+
135+
/**
136+
* Sets up the date/time preference controls on account pages.
137+
*/
138+
function setupAccountPageControls() {
139+
const form = document.querySelector("form");
140+
if (!form) {
141+
console.error("[server-time-to-local.js] Could not find form on account page to add settings.");
142+
return;
143+
}
144+
145+
const submitButton = form.querySelector('input[type="submit"]');
146+
if (!submitButton) {
147+
console.error("[server-time-to-local.js] Could not find submit button on account page to add settings.");
148+
return;
149+
}
150+
151+
const timeSettingsField = document.createElement("div");
152+
timeSettingsField.className = "field";
153+
Object.assign(timeSettingsField.style, {
154+
display: 'flex',
155+
gap: '1rem',
156+
flexWrap: 'wrap',
157+
marginBottom: '1rem'
158+
});
159+
160+
const {
161+
wrapper: dateWrapper,
162+
} = createStyleDropdown(
163+
"date-style-pref",
164+
"Preferred Date Format:",
165+
currentPreferences.dateStyle
166+
);
167+
const {
168+
wrapper: timeWrapper,
169+
} = createStyleDropdown(
170+
"time-style-pref",
171+
"Preferred Time Format:",
172+
currentPreferences.timeStyle
173+
);
174+
175+
timeSettingsField.appendChild(dateWrapper);
176+
timeSettingsField.appendChild(timeWrapper);
177+
form.insertBefore(timeSettingsField, submitButton);
178+
179+
updatePreview();
180+
}
181+
182+
if (ACCOUNT_PAGE_PATHS.includes(window.location.pathname)) {
183+
setupAccountPageControls();
184+
185+
document.addEventListener("change", (e) => {
186+
const target = e.target;
187+
let changed = false;
188+
189+
if (target.id === 'date-style-pref') {
190+
saveDateStylePreference(target.value);
191+
changed = true;
192+
} else if (target.id === 'time-style-pref') {
193+
saveTimeStylePreference(target.value);
194+
changed = true;
195+
}
196+
197+
if (changed) {
198+
updatePreview();
199+
}
200+
});
201+
}
202+
203+
formatAllDates();
204+
});

assets/static_javascript.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

generate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,5 @@ func main() {
8585
generateMap(path.Join("web", "handler", "html.go"), "handler", "TplMap", glob("web/handler/html/*.gohtml"))
8686
generateMap(path.Join("web", "handler", "common.go"), "handler", "TplCommonMap", glob("web/handler/html/common/*.gohtml"))
8787
generateMap(path.Join("assets", "static_stylesheet.go"), "assets", "AssetsMap", glob("assets/*.css"))
88+
generateMap(path.Join("assets", "static_javascript.go"), "assets", "JSMap", glob("assets/*.js"))
8889
}

storage/sql.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/handler/common.go

Lines changed: 39 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/handler/handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func New(data *storage.Storage, s *session.Manager) (http.Handler, error) {
160160

161161
// Static assets
162162
router.HandleFunc("/style.css", h.showStylesheet).Methods(http.MethodGet)
163+
router.HandleFunc("/js/{filename}", h.showJS).Methods(http.MethodGet)
163164
//router.HandleFunc("/favicon.ico", h.showFavicon).Name("favicon").Methods(http.MethodGet)
164165

165166
// Forum views

0 commit comments

Comments
 (0)