add guestbook
This commit is contained in:
244
js/guestbook.js
Normal file
244
js/guestbook.js
Normal file
@@ -0,0 +1,244 @@
|
||||
const GUESTBOOK_API = 'https://guestbook.bug-dev-mail.workers.dev';
|
||||
|
||||
const EMOTES = {
|
||||
':happy:': 'images/emotes/joy.gif',
|
||||
':love:': 'images/emotes/love.gif',
|
||||
':laugh:': 'images/emotes/laugh.gif',
|
||||
':shock:': 'images/emotes/astonished.gif',
|
||||
':hello:': 'images/emotes/wave.gif',
|
||||
':kiss:': 'images/emotes/kiss.gif',
|
||||
':clap:': 'images/emotes/applause.gif',
|
||||
':dance:': 'images/emotes/caramelldansen.gif',
|
||||
':yippee:': 'images/emotes/yippee.gif',
|
||||
};
|
||||
|
||||
function replaceLinks(text) {
|
||||
function cleanUrl(url) {
|
||||
return url.replace(/&/g, '&');
|
||||
}
|
||||
var result = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||
function(_, label, url) {
|
||||
return '<a href="' + cleanUrl(url) + '" target="_blank" rel="noopener noreferrer">' + label + '</a>';
|
||||
});
|
||||
result = result.replace(/(^|[^"'])(https?:\/\/[^\s<]+)/g,
|
||||
function(_, pre, url) {
|
||||
return pre + '<a href="' + cleanUrl(url) + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceEmotes(text) {
|
||||
let result = replaceLinks(text);
|
||||
for (const [code, src] of Object.entries(EMOTES)) {
|
||||
const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
result = result.replace(new RegExp(escaped, 'g'),
|
||||
`<img src="${src}" class="gb-emote" title="${code}" alt="${code}">`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr.includes('T') && !dateStr.includes('-')) {
|
||||
return dateStr;
|
||||
}
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now - d;
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMs / 3600000);
|
||||
const diffDay = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMin < 1) return 'just now';
|
||||
if (diffMin < 60) return diffMin + ' min ago';
|
||||
if (diffHr < 24) return diffHr + ' hr ago';
|
||||
if (diffDay === 1) return '1 day ago';
|
||||
return diffDay + ' days ago';
|
||||
}
|
||||
|
||||
function createMessageBubble(msg) {
|
||||
const entry = document.createElement('div');
|
||||
entry.style.setProperty('--col', '#90c9ff');
|
||||
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.textContent = msg.name || 'Anonymous';
|
||||
|
||||
const dateDiv = document.createElement('div');
|
||||
dateDiv.className = 'gb-date';
|
||||
dateDiv.textContent = timeAgo(msg.date);
|
||||
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'gb-msg-content';
|
||||
msgDiv.innerHTML = replaceEmotes(msg.message);
|
||||
|
||||
entry.appendChild(nameDiv);
|
||||
entry.appendChild(dateDiv);
|
||||
entry.appendChild(msgDiv);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'gb-actions';
|
||||
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'gb-like-btn';
|
||||
const likeCount = typeof msg.likes === 'number' ? msg.likes : (Array.isArray(msg.likes) ? msg.likes.length : 0);
|
||||
likeBtn.innerHTML = `<span class="gb-like-icon">♥</span> ${likeCount}`;
|
||||
likeBtn.onclick = () => likeMessage(msg.id, likeBtn);
|
||||
|
||||
actions.appendChild(likeBtn);
|
||||
entry.appendChild(actions);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
const picto = document.querySelector('#pictochat-content .picto');
|
||||
if (!picto) return;
|
||||
picto.innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(GUESTBOOK_API + '/messages');
|
||||
const messages = await res.json();
|
||||
messages.forEach(msg => {
|
||||
picto.appendChild(createMessageBubble(msg));
|
||||
});
|
||||
} catch (e) {
|
||||
const err = document.createElement('div');
|
||||
err.style.setProperty('--col', '#90c9ff');
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.textContent = 'system';
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.textContent = 'No one has commented yet. Be the first!';
|
||||
err.appendChild(nameDiv);
|
||||
err.appendChild(document.createTextNode(''));
|
||||
err.appendChild(document.createElement('br'));
|
||||
err.appendChild(msgDiv);
|
||||
picto.appendChild(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const nameInput = document.getElementById('gb-name');
|
||||
const msgInput = document.getElementById('gb-message');
|
||||
const sendBtn = document.getElementById('gb-send');
|
||||
|
||||
const message = msgInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
sendBtn.classList.add('gb-disabled');
|
||||
try {
|
||||
await fetch(GUESTBOOK_API + '/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: nameInput.value.trim() || 'Anonymous',
|
||||
message: message,
|
||||
}),
|
||||
});
|
||||
msgInput.value = '';
|
||||
await loadMessages();
|
||||
setTimeout(replaceTextWithEmotes, 500);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
sendBtn.classList.remove('gb-disabled');
|
||||
}
|
||||
|
||||
async function likeMessage(id, btn) {
|
||||
try {
|
||||
const res = await fetch(GUESTBOOK_API + `/messages/${id}/like`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
btn.innerHTML = `<span class="gb-like-icon">♥</span> ${data.likes}`;
|
||||
btn.classList.add('gb-liked');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEmoteSelector() {
|
||||
const els = document.getElementsByClassName('gb-toggle-div');
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i].style.display === 'none' || els[i].style.display === '') {
|
||||
els[i].style.display = 'block';
|
||||
} else {
|
||||
els[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addText(text) {
|
||||
const textarea = document.getElementById('gb-message');
|
||||
textarea.value += text;
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
function replaceTextWithEmotes() {
|
||||
const picto = document.querySelector('#pictochat-content .picto');
|
||||
if (!picto) return;
|
||||
|
||||
function replaceTextNodes(element) {
|
||||
element.childNodes.forEach(function (node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
let replacedText = node.textContent;
|
||||
let changed = false;
|
||||
for (const [key, src] of Object.entries(EMOTES)) {
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const imgTag = "<img src='" + src + "' title='" + key + "' class='gb-emote' />";
|
||||
const newText = replacedText.replace(new RegExp(escapedKey, 'g'), imgTag);
|
||||
if (newText !== replacedText) {
|
||||
replacedText = newText;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
const newElement = document.createElement('span');
|
||||
newElement.innerHTML = replacedText;
|
||||
node.parentNode.replaceChild(newElement, node);
|
||||
}
|
||||
} else {
|
||||
replaceTextNodes(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
replaceTextNodes(picto);
|
||||
}
|
||||
|
||||
(function () {
|
||||
const picto = document.querySelector('#pictochat-content .picto');
|
||||
if (!picto) return;
|
||||
|
||||
const sendBtn = document.getElementById('gb-send');
|
||||
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
|
||||
|
||||
const msgInput = document.getElementById('gb-message');
|
||||
if (msgInput) {
|
||||
msgInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const emoteBtn = document.getElementById('gb-emote-toggle');
|
||||
if (emoteBtn) emoteBtn.addEventListener('click', toggleEmoteSelector);
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
const selector = document.getElementById('gb-emote-selector');
|
||||
const toggle = document.getElementById('gb-emote-toggle');
|
||||
if (selector && selector.style.display === 'block' &&
|
||||
!selector.contains(e.target) && e.target !== toggle) {
|
||||
selector.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const emoteItems = document.querySelectorAll('#gb-emote-selector .gb-emote-item');
|
||||
emoteItems.forEach(function (img) {
|
||||
img.addEventListener('click', function () {
|
||||
addText(img.title + ' ');
|
||||
});
|
||||
});
|
||||
|
||||
loadMessages().then(function () {
|
||||
replaceTextWithEmotes();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user