move inline css and js into proper files

jocadbz
Joca 2026-02-20 13:16:31 -03:00
parent 78a2875958
commit 5553a8af01
Signed by: jocadbz
GPG Key ID: B1836DCE2F50BDF7
6 changed files with 1062 additions and 947 deletions

View File

@ -18,6 +18,8 @@ type PageData struct {
BasePath string BasePath string
StaticPath string StaticPath string
CurrentURL string CurrentURL string
ContentTemplate string
BodyClass string
} }
type Config struct { type Config struct {

View File

@ -238,6 +238,8 @@ func ChatHandler(app *App) http.HandlerFunc {
BasePath: app.Config.ThreadrDir, BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static", StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.Path, CurrentURL: r.URL.Path,
ContentTemplate: "chat-content",
BodyClass: "chat-page",
}, },
Board: *board, Board: *board,
Messages: messages, Messages: messages,

564
static/chat.js Normal file
View File

@ -0,0 +1,564 @@
(() => {
'use strict';
const chatContainer = document.querySelector('.chat-container');
if (!chatContainer) {
return;
}
const boardId = chatContainer.dataset.boardId;
const basePath = chatContainer.dataset.basePath || '';
const currentUsername = chatContainer.dataset.currentUsername || '';
const usernamesScript = document.getElementById('chat-usernames');
let allUsernames = [];
if (usernamesScript) {
try {
allUsernames = JSON.parse(usernamesScript.textContent || '[]');
} catch (err) {
console.error('Failed to parse chat usernames:', err);
allUsernames = [];
}
}
let ws;
let autocompleteActive = false;
let replyToId = -1;
let reconnectAttempts = 0;
let reconnectTimeout;
let isUserAtBottom = true;
let unreadCount = 0;
const maxReconnectDelay = 30000;
const typingUsers = new Set();
function updateConnectionStatus(status) {
const dot = document.getElementById('connection-dot');
const text = document.getElementById('connection-text');
if (!dot || !text) {
return;
}
dot.className = 'connection-dot ' + status;
if (status === 'connected') {
text.textContent = 'Connected';
reconnectAttempts = 0;
} else if (status === 'connecting') {
text.textContent = 'Connecting...';
} else if (status === 'disconnected') {
text.textContent = 'Disconnected';
}
}
function connectWebSocket() {
if (!boardId) {
return;
}
updateConnectionStatus('connecting');
ws = new WebSocket('ws://' + window.location.host + basePath + '/chat/?ws=true&id=' + boardId);
ws.onopen = function() {
updateConnectionStatus('connected');
reconnectAttempts = 0;
};
ws.onmessage = function(event) {
const msg = JSON.parse(event.data);
if (msg.type === 'typing') {
showTypingIndicator(msg.username);
return;
}
appendMessage(msg);
};
ws.onclose = function() {
updateConnectionStatus('disconnected');
console.log('WebSocket closed, reconnecting...');
scheduleReconnect();
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
updateConnectionStatus('disconnected');
};
}
function scheduleReconnect() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
reconnectAttempts++;
console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...`);
reconnectTimeout = setTimeout(connectWebSocket, delay);
}
function sendMessage() {
const input = document.getElementById('chat-input-text');
if (!input) {
return;
}
const content = input.value.trim();
if (content === '') {
return;
}
const msg = {
type: 'message',
content: content,
replyTo: replyToId
};
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
input.value = '';
cancelReply();
} else {
console.error('WebSocket is not open. Current state:', ws ? ws.readyState : 'undefined');
alert('Cannot send message: Not connected to chat server');
}
}
function sendTypingIndicator() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'typing', username: currentUsername }));
}
}
function showTypingIndicator(username) {
if (username === currentUsername) {
return;
}
typingUsers.add(username);
updateTypingDisplay();
setTimeout(() => {
typingUsers.delete(username);
updateTypingDisplay();
}, 3000);
}
function updateTypingDisplay() {
const indicator = document.getElementById('typing-indicator');
const usersSpan = document.getElementById('typing-users');
if (!indicator || !usersSpan) {
return;
}
if (typingUsers.size === 0) {
indicator.classList.remove('visible');
} else {
const users = Array.from(typingUsers);
if (users.length === 1) {
usersSpan.textContent = `${users[0]} is typing`;
} else if (users.length === 2) {
usersSpan.textContent = `${users[0]} and ${users[1]} are typing`;
} else {
usersSpan.textContent = `${users[0]}, ${users[1]}, and ${users.length - 2} other${users.length - 2 > 1 ? 's' : ''} are typing`;
}
indicator.classList.add('visible');
}
}
function appendMessage(msg) {
const messages = document.getElementById('chat-messages');
if (!messages) {
return;
}
if (document.getElementById('msg-' + msg.id)) {
return;
}
const wasAtBottom = isUserAtBottom;
const msgDiv = document.createElement('div');
let highlightClass = '';
if (msg.mentions && msg.mentions.includes(currentUsername)) {
highlightClass = ' chat-message-highlighted';
}
msgDiv.className = 'chat-message' + highlightClass;
msgDiv.id = 'msg-' + msg.id;
msgDiv.dataset.user = msg.username;
msgDiv.dataset.reply = msg.replyTo || 0;
let pfpHTML = '<div class="chat-message-pfp" style="background-color: #001858;"></div>';
if (msg.pfpFileId && msg.pfpFileId.Valid) {
pfpHTML = `<img src="${basePath}/file?id=${msg.pfpFileId.Int64}&t=${new Date().getTime()}" alt="PFP" class="chat-message-pfp">`;
}
const replyHTML = msg.replyTo > 0 ? `<div class="chat-message-reply" onclick="scrollToMessage(${msg.replyTo})">Replying to message...</div>` : '';
msgDiv.innerHTML = `
<div class="chat-message-header">
${pfpHTML}
<span class="chat-message-username">${msg.username}</span>
<span class="chat-message-timestamp">${new Date(msg.timestamp).toLocaleString()}</span>
</div>
${replyHTML}
<div class="chat-message-content">${msg.content}</div>
<div class="post-actions">
<a href="javascript:void(0)" onclick="replyToMessage(${msg.id}, '${msg.username}')">Reply</a>
</div>
`;
messages.appendChild(msgDiv);
applyGrouping();
if (wasAtBottom) {
messages.scrollTop = messages.scrollHeight;
unreadCount = 0;
updateUnreadBadge();
} else if (msg.username !== currentUsername) {
unreadCount++;
updateUnreadBadge();
}
}
function updateUnreadBadge() {
const badge = document.getElementById('unread-badge');
if (!badge) {
return;
}
if (unreadCount > 0) {
badge.textContent = unreadCount > 99 ? '99+' : unreadCount;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
}
function jumpToBottom() {
const messages = document.getElementById('chat-messages');
if (!messages) {
return;
}
messages.scrollTo({
top: messages.scrollHeight,
behavior: 'smooth'
});
unreadCount = 0;
updateUnreadBadge();
}
function checkScrollPosition() {
const messages = document.getElementById('chat-messages');
const jumpButton = document.getElementById('jump-to-bottom');
if (!messages || !jumpButton) {
return;
}
const threshold = 100;
isUserAtBottom = messages.scrollHeight - messages.scrollTop - messages.clientHeight < threshold;
if (isUserAtBottom) {
jumpButton.classList.remove('visible');
unreadCount = 0;
updateUnreadBadge();
} else {
jumpButton.classList.add('visible');
}
}
function replyToMessage(id, username) {
replyToId = id;
const replyIndicator = document.getElementById('reply-indicator');
const replyUsernameSpan = document.getElementById('reply-username');
if (!replyIndicator || !replyUsernameSpan) {
return;
}
replyUsernameSpan.textContent = `Replying to ${username}`;
replyIndicator.style.display = 'flex';
const input = document.getElementById('chat-input-text');
if (input) {
input.focus();
}
}
function cancelReply() {
replyToId = -1;
const replyIndicator = document.getElementById('reply-indicator');
if (replyIndicator) {
replyIndicator.style.display = 'none';
}
}
function scrollToMessage(id) {
const msgElement = document.getElementById('msg-' + id);
if (msgElement) {
msgElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
msgElement.style.transition = 'background-color 0.5s';
msgElement.style.backgroundColor = '#f582ae';
setTimeout(() => {
msgElement.style.backgroundColor = '';
}, 1000);
}
}
function showAutocompletePopup(suggestions, x, y) {
const popup = document.getElementById('autocomplete-popup');
if (!popup) {
return;
}
popup.innerHTML = '';
popup.style.position = 'fixed';
popup.style.left = x + 'px';
popup.style.top = y + 'px';
popup.style.display = 'block';
autocompleteActive = true;
suggestions.forEach(username => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
item.textContent = username;
item.onmousedown = (e) => {
e.preventDefault();
completeMention(username);
popup.style.display = 'none';
autocompleteActive = false;
};
popup.appendChild(item);
});
}
function completeMention(username) {
const input = document.getElementById('chat-input-text');
if (!input) {
return;
}
const text = input.value;
const caretPos = input.selectionStart;
const textBeforeCaret = text.substring(0, caretPos);
const atIndex = textBeforeCaret.lastIndexOf('@');
if (atIndex !== -1) {
const prefix = text.substring(0, atIndex);
const suffix = text.substring(caretPos);
input.value = prefix + '@' + username + ' ' + suffix;
const newCaretPos = prefix.length + 1 + username.length + 1;
input.focus();
input.setSelectionRange(newCaretPos, newCaretPos);
}
}
function getCaretCoordinates(element, position) {
const mirrorDivId = 'input-mirror-div';
const div = document.createElement('div');
div.id = mirrorDivId;
document.body.appendChild(div);
const style = window.getComputedStyle(element);
const properties = ['border', 'boxSizing', 'fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight', 'padding', 'textDecoration', 'textIndent', 'textTransform', 'whiteSpace', 'wordSpacing', 'wordWrap', 'width'];
properties.forEach(prop => {
div.style[prop] = style[prop];
});
div.style.position = 'absolute';
div.style.top = '-9999px';
div.style.left = '0px';
div.textContent = element.value.substring(0, position);
const span = document.createElement('span');
span.textContent = element.value.substring(position) || '.';
div.appendChild(span);
const coords = { top: span.offsetTop, left: span.offsetLeft };
document.body.removeChild(div);
return coords;
}
function applyGrouping() {
const container = document.getElementById('chat-messages');
if (!container) {
return;
}
const msgs = Array.from(container.querySelectorAll('.chat-message')).filter(el => el.id.startsWith('msg-'));
for (let i = 0; i < msgs.length; i++) {
const curr = msgs[i];
curr.classList.remove('grouped');
if (i === 0) {
continue;
}
const prev = msgs[i - 1];
const currUser = curr.dataset.user;
const prevUser = prev.dataset.user;
const currReply = parseInt(curr.dataset.reply, 10) || -1;
if (currUser === prevUser && (currReply <= 0)) {
curr.classList.add('grouped');
}
}
}
function handleAutocompleteInput(e) {
const input = e.target;
const text = input.value;
const caretPos = input.selectionStart;
const popup = document.getElementById('autocomplete-popup');
if (!popup) {
return;
}
const textBeforeCaret = text.substring(0, caretPos);
const atIndex = textBeforeCaret.lastIndexOf('@');
if (atIndex !== -1 && (atIndex === 0 || /\s/.test(text.charAt(atIndex - 1)))) {
const query = textBeforeCaret.substring(atIndex + 1);
if (!/\s/.test(query)) {
const suggestions = allUsernames.filter(u => u.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10);
if (suggestions.length > 0 && query.length > 0) {
const coords = getCaretCoordinates(input, atIndex);
const rect = input.getBoundingClientRect();
showAutocompletePopup(suggestions, rect.left + coords.left, rect.top + coords.top + 20);
} else {
popup.style.display = 'none';
autocompleteActive = false;
}
return;
}
}
popup.style.display = 'none';
autocompleteActive = false;
}
function handleAutocompleteBlur() {
setTimeout(() => {
if (!document.querySelector('.autocomplete-popup:hover')) {
const popup = document.getElementById('autocomplete-popup');
if (popup) {
popup.style.display = 'none';
}
autocompleteActive = false;
}
}, 150);
}
function handleMessageKeydown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function setupDraftAutoSave(chatInput) {
if (!chatInput) {
return;
}
const draftKey = `draft_chat_${boardId}`;
let draftSaveTimeout;
const existingDraft = loadDraft(draftKey);
const draftTimestamp = getDraftTimestamp(draftKey);
if (existingDraft && draftTimestamp) {
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
if (draftTimestamp > sevenDaysAgo) {
const indicator = showDraftIndicator(
existingDraft,
draftTimestamp,
(content) => {
chatInput.value = content;
chatInput.focus();
},
() => {
clearDraft(draftKey);
}
);
const chatInputContainer = document.querySelector('.chat-input');
if (chatInputContainer && chatInputContainer.parentNode) {
chatInputContainer.parentNode.insertBefore(indicator, chatInputContainer);
}
} else {
clearDraft(draftKey);
}
}
chatInput.addEventListener('input', () => {
clearTimeout(draftSaveTimeout);
draftSaveTimeout = setTimeout(() => {
const content = chatInput.value.trim();
if (content) {
saveDraft(draftKey, content);
} else {
clearDraft(draftKey);
}
}, 2000);
});
}
function setupTypingIndicator(chatInput) {
if (!chatInput) {
return;
}
let lastTypingTime = 0;
chatInput.addEventListener('input', () => {
const now = Date.now();
if (now - lastTypingTime > 2000) {
sendTypingIndicator();
lastTypingTime = now;
}
});
}
function setupSendMessageDraftClear(chatInput) {
if (!chatInput) {
return;
}
const draftKey = `draft_chat_${boardId}`;
const originalSendMessage = window.sendMessage;
window.sendMessage = function() {
const content = chatInput.value.trim();
if (content !== '') {
clearDraft(draftKey);
}
if (typeof originalSendMessage === 'function') {
originalSendMessage();
} else {
sendMessage();
}
};
}
function initChat() {
connectWebSocket();
const messagesContainer = document.getElementById('chat-messages');
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
applyGrouping();
messagesContainer.addEventListener('scroll', checkScrollPosition);
}
const jumpButton = document.getElementById('jump-to-bottom');
if (jumpButton) {
jumpButton.addEventListener('click', jumpToBottom);
}
const chatInput = document.getElementById('chat-input-text');
if (chatInput) {
chatInput.addEventListener('input', handleAutocompleteInput);
chatInput.addEventListener('blur', handleAutocompleteBlur);
chatInput.addEventListener('keydown', handleMessageKeydown);
}
setupTypingIndicator(chatInput);
setupDraftAutoSave(chatInput);
setupSendMessageDraftClear(chatInput);
checkScrollPosition();
}
window.sendMessage = sendMessage;
window.replyToMessage = replyToMessage;
window.cancelReply = cancelReply;
window.scrollToMessage = scrollToMessage;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChat);
} else {
initChat();
}
})();

View File

@ -6,6 +6,11 @@ body {
color: #001858; /* blue */ color: #001858; /* blue */
} }
body.chat-page {
height: 100vh;
overflow: hidden;
}
main { main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -13,6 +18,12 @@ main {
padding: 25px; padding: 25px;
} }
body.chat-page main {
padding: 0;
margin-top: 3em;
height: calc(100vh - 3em);
}
main > header { main > header {
text-align: center; text-align: center;
margin-bottom: 1em; margin-bottom: 1em;
@ -343,6 +354,344 @@ p.thread-info {
background-color: #ffe0f0; /* Light pink background */ background-color: #ffe0f0; /* Light pink background */
animation: highlight-fade 2s ease-out; animation: highlight-fade 2s ease-out;
} }
/* Chat page styles */
.chat-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
border: none;
border-radius: 0;
background-color: #fef6e4;
box-shadow: none;
}
.chat-header {
padding: 10px;
text-align: center;
border-bottom: 1px solid #001858;
position: relative;
}
.connection-status {
position: absolute;
top: 10px;
right: 10px;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85em;
padding: 4px 10px;
border-radius: 12px;
background-color: rgba(0,0,0,0.1);
}
.connection-dot {
width: 10px;
height: 10px;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
.connection-dot.connected {
background-color: #4ade80;
}
.connection-dot.connecting {
background-color: #fbbf24;
}
.connection-dot.disconnected {
background-color: #f87171;
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.typing-indicator {
padding: 8px 12px;
background-color: #f3d2c1;
border-radius: 5px;
font-size: 0.85em;
font-style: italic;
color: #001858;
margin-bottom: 8px;
display: none;
}
.typing-indicator.visible {
display: block;
}
.typing-dots {
display: inline-block;
margin-left: 4px;
}
.typing-dots span {
animation: typing-blink 1.4s infinite;
display: inline-block;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing-blink {
0%, 60%, 100% { opacity: 0.3; }
30% { opacity: 1; }
}
.jump-to-bottom {
position: absolute;
bottom: 70px;
right: 20px;
background-color: #001858;
color: #fef6e4;
border: 2px solid #001858;
border-radius: 50%;
width: 45px;
height: 45px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
font-size: 1.3em;
box-shadow: 0px 4px 12px rgba(0,0,0,0.3);
transition: transform 0.2s ease;
z-index: 100;
}
.jump-to-bottom.visible {
display: flex;
}
.jump-to-bottom:hover {
transform: translateY(2px);
background-color: #8bd3dd;
}
.jump-to-bottom .unread-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: #f582ae;
color: #fef6e4;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7em;
font-weight: bold;
}
.message-status {
font-size: 0.7em;
color: #666;
margin-left: 8px;
}
.message-status.sending {
color: #fbbf24;
}
.message-status.sent {
color: #4ade80;
}
.message-status.failed {
color: #f87171;
cursor: pointer;
}
.chat-breadcrumb {
background-color: #f3d2c1;
padding: 8px 12px;
border-bottom: 1px solid #001858;
font-size: 0.85em;
text-align: left;
}
.chat-breadcrumb a {
color: #001858;
text-decoration: none;
}
.chat-breadcrumb a:hover {
color: #f582ae;
text-decoration: underline;
}
.chat-breadcrumb-separator {
margin: 0 6px;
opacity: 0.6;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
}
.chat-message {
margin-bottom: 8px;
max-width: 90%;
position: relative;
}
.chat-message-header {
display: flex;
align-items: center;
margin-bottom: 3px;
}
.chat-message-pfp {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 8px;
}
.chat-message-username {
font-weight: bold;
color: #001858;
font-size: 0.9em;
}
.chat-message-timestamp {
font-size: 0.7em;
color: #666;
margin-left: 8px;
}
.chat-message-content {
background-color: #f3d2c1;
padding: 6px 10px;
border-radius: 5px;
line-height: 1.3;
font-size: 0.9em;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.chat-message-reply {
background-color: rgba(0,0,0,0.1);
padding: 4px 8px;
border-radius: 5px;
margin-bottom: 3px;
font-size: 0.8em;
cursor: pointer;
}
.chat-message-mention {
color: #f582ae;
font-weight: bold;
}
.chat-input {
padding: 8px;
border-top: 1px solid #001858;
display: flex;
flex-direction: column;
}
.chat-input textarea {
resize: none;
height: 50px;
margin-bottom: 8px;
font-size: 0.9em;
}
.chat-input button {
align-self: flex-end;
width: auto;
padding: 6px 12px;
font-size: 0.9em;
}
.chat-message:hover .post-actions {
opacity: 1;
}
.post-actions a {
color: #001858;
text-decoration: none;
font-size: 0.8em;
padding: 2px 5px;
border: 1px solid #001858;
border-radius: 3px;
}
.post-actions a:hover {
background-color: #8bd3dd;
color: #fef6e4;
}
.autocomplete-popup {
position: absolute;
background-color: #fff;
border: 1px solid #001858;
border-radius: 5px;
max-height: 200px;
overflow-y: auto;
box-shadow: 0px 4px 8px rgba(0,0,0,0.2);
z-index: 1000;
display: none;
}
.autocomplete-item {
padding: 6px 10px;
cursor: pointer;
font-size: 0.9em;
}
.autocomplete-item:hover {
background-color: #f3d2c1;
}
.reply-indicator {
background-color: #001858;
color: #fef6e4;
padding: 5px 10px;
border-radius: 5px;
margin-bottom: 8px;
display: none;
align-items: center;
justify-content: space-between;
}
.reply-indicator span {
font-size: 0.9em;
}
.reply-indicator button {
background: none;
border: none;
color: #fef6e4;
cursor: pointer;
font-size: 0.9em;
padding: 0 5px;
margin: 0;
width: auto;
}
.reply-indicator button:hover {
background: none;
color: #f582ae;
}
/* Grouped messages (hide header for consecutive messages from same user) */ /* Grouped messages (hide header for consecutive messages from same user) */
.chat-message.grouped { .chat-message.grouped {
margin-top: -4px; margin-top: -4px;
@ -440,6 +789,90 @@ p.thread-info {
border-color: #f582ae; border-color: #f582ae;
} }
} }
body.chat-page {
background-color: #333;
}
.chat-container {
background-color: #444;
}
.chat-breadcrumb {
background-color: #555;
}
.chat-breadcrumb a {
color: #fef6e4;
}
.typing-indicator {
background-color: #555;
color: #fef6e4;
}
.jump-to-bottom {
background-color: #fef6e4;
color: #001858;
border-color: #fef6e4;
}
.jump-to-bottom:hover {
background-color: #8bd3dd;
}
.chat-header {
border-color: #fef6e4;
}
.chat-message-username {
color: #fef6e4;
}
.chat-message-timestamp {
color: #aaa;
}
.chat-message-content {
background-color: #555;
}
.chat-input {
border-color: #fef6e4;
}
.autocomplete-popup {
background-color: #444;
border-color: #fef6e4;
color: #fef6e4;
}
.autocomplete-item:hover {
background-color: #555;
}
.post-actions a {
color: #fef6e4;
border-color: #fef6e4;
}
.post-actions a:hover {
background-color: #8bd3dd;
color: #001858;
}
.reply-indicator {
background-color: #222;
color: #fef6e4;
}
.reply-indicator button {
color: #fef6e4;
}
.reply-indicator button:hover {
color: #f582ae;
}
} }
/* Breadcrumb navigation */ /* Breadcrumb navigation */

View File

@ -5,11 +5,12 @@
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script> <script src="{{.StaticPath}}/app.js" defer></script>
<script src="{{.StaticPath}}/chat.js" defer></script>
</head> </head>
<body> <body{{if .BodyClass}} class="{{.BodyClass}}"{{end}}>
{{template "navbar" .}} {{template "navbar" .}}
<main> <main>
{{block "content" .}}{{end}} <!-- Define a block for content --> {{template .ContentTemplate .}}
</main> </main>
{{template "cookie_banner" .}} {{template "cookie_banner" .}}
</body> </body>

File diff suppressed because it is too large Load Diff