commit 55eebe260311d955e94eb41291b1bdcacc63fa14 Author: Marc Michalsky Date: Fri Sep 1 22:54:04 2023 +0200 🎉 Initial commit diff --git a/.idea/html.iml b/.idea/html.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/html.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44fc506 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e5e05d2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..d58740c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + -1 + + + + + + + + + + + + { + "customColor": "", + "associatedIndex": 8 +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1677581124694 + + + + + + + + + + + + + + + + + + + + + + + +

The name field contains invalid characters.

+ + + + + diff --git a/resources/images/favicon.png b/resources/images/favicon.png new file mode 100644 index 0000000..107edcf Binary files /dev/null and b/resources/images/favicon.png differ diff --git a/resources/sounds/notification.ogg b/resources/sounds/notification.ogg new file mode 100644 index 0000000..45b5eb2 Binary files /dev/null and b/resources/sounds/notification.ogg differ diff --git a/script.js b/script.js new file mode 100644 index 0000000..a101de5 --- /dev/null +++ b/script.js @@ -0,0 +1,198 @@ +// Enable debug mode if the URL contains the debug parameter +let debug = false; +if (window.location.href.includes('debug')) { + console.log('Debug mode enabled'); + debug = true; +} + +// Initialize the EventSource and the HTML elements +const eventSource = new EventSource('https://ntfy.example.org/guestbook/sse?since=all'); +const entryText = document.getElementById('entryText'); +const name = document.getElementById('name'); +const submitEntry = document.getElementById('submitEntry'); +const entryTable = document.getElementById('entryTable'); +const errorMessage = document.getElementById('error-message'); +const bell = new Audio('/resources/sounds/notification.ogg'); + +// Disable the notification sound for the first 2 seconds until the guestbook is loaded initially +let bellEnabled = false; + +// Event listener for the Enter key +function handleKeydown(event) { + if (event.key === 'Enter') { + sendMessage(); + } +} + +// Check if the name is valid +function checkName(name) { + if (name.match(/:\s/)) { + errorMessage.classList.add('visible'); + console.error('Invalid name'); + return false; + } + return true; +} + +// Send new entry to the ntfy server +function sendMessage() { + const text = entryText.value.trim(); + const author = name.value.trim() ? name.value.trim() : 'Anonymous'; + if (checkName(author)) { + errorMessage.classList.remove('visible'); + const message = author + ': ' + text; + if (text.length > 0) { + fetch('https://ntfy.example.org/guestbook', { + method: 'POST', + body: message + }) + .then(() => { + // Empty the input field + entryText.value = ''; + // Save the name in the local storage + localStorage.setItem('name', author); + }) + .catch(error => console.error(error)); + } + } +} + +// Calculate time passed since the message was sent +function timeSince(date) { + let seconds = Math.floor(Date.now() / 1000 - date); + let interval = Math.floor(seconds / 3600); + if (interval >= 2) { + return interval + " hours ago"; + } + if (interval === 1) { + return "1 hour ago" + } + interval = Math.floor(seconds / 60); + if (interval > 1) { + return interval + " minutes ago"; + } + if (interval === 1) { + return "1 minute ago" + } + if (seconds > 10) { + return Math.floor(seconds) + " seconds ago"; + } + if (seconds > -10) { + return "just now" + } + if (seconds > -60) { + return "in " + Math.floor(-seconds) + " seconds"; + } + interval = Math.floor(-seconds / 60); + if (interval === 1) { + return "in 1 minute"; + } + if (interval < 60) { + return "in " + interval + " minutes"; + } + interval = Math.floor(-seconds / 3600); + if (interval === 1) { + return "in 1 hour"; + } + if (interval > 1) { + return "in " + interval + " hours"; + } +} + +// Parse the time from the message +function parseTimeString(data) { + return ` | ${timeSince(data.time)}, expires in ${timeSince(data.expires)}` +} + +// Update time +function updateTime() { + const timeElements = document.getElementsByClassName('time'); + for (let i = 0; i < timeElements.length; i++) { + const time = timeElements[i]; + time.textContent = parseTimeString(time.dataset); + } +} + +// Add a new entry to the list +function addEntry(data) { + const row = document.createElement('tr'); + + // Create a new cell for the author + const authorCell = document.createElement('td'); + authorCell.classList.add('author'); + authorCell.style.fontWeight = 'italic'; + + // Create a new cell for the message + const messageCell = document.createElement('td'); + messageCell.classList.add('message'); + + // Split the message into author and text + const [name, message] = data.message.split(/:\s(.*)/); + + // Create span with the time that passed since the message was sent + // and when the message will expire + const time = document.createElement('span'); + time.classList.add('time'); + time.dataset.time = data.time; + time.dataset.expires = data.expires; + time.textContent = parseTimeString(data); + + // Set the cell content + authorCell.textContent = name + ':'; + messageCell.innerHTML = message + time.outerHTML; + + // Append the cells to the row and the row to the table + row.appendChild(authorCell); + row.appendChild(messageCell); + + entryTable.appendChild(row); +} + +// Connect to the server to receive new entries +function streamEntries() { + eventSource.onmessage = (m) => { + if (debug) { + console.log(m.data); + } + const data = JSON.parse(m.data) + addEntry(data); + + // Play a notification sound for new entries + try { + if (bellEnabled) { + bell.play(); + } + } catch (DOMException) { + console.log('The notification sound was blocked by the browser'); + } + }; +} + +// Initialize the page +function setup() { + // Add event listeners + submitEntry.addEventListener('click', sendMessage); + entryText.addEventListener('keydown', handleKeydown); + + // Start streaming the entries + streamEntries(); + + // Update the time values every 10 seconds + setInterval(updateTime, 10000); + + // Load the name from the local storage + name.value = localStorage.getItem('name'); + + // Enable the notification sound after 2 seconds + setTimeout(() => { + bellEnabled = true; + }, 2000); + + // Close the connection when the page is closed + window.addEventListener('beforeunload', function (e) { + e.preventDefault(); + eventSource.close(); + }); +} + +window.addEventListener('load', setup) diff --git a/style.css b/style.css new file mode 100644 index 0000000..fdb5d3a --- /dev/null +++ b/style.css @@ -0,0 +1,186 @@ +body { + background-color: #01252b; + color: white; + font-family: monospace; +} + +h1 span { + display: inline-block; + line-height: 36px; +} + +#container { + position: absolute; + max-width: 1000px; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 84%; + height: auto; + margin: auto; + padding: 8% 4%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#hi { + display: block; + text-align: center; +} + +#rotate { + -ms-transition: 0.4s ease-in-out; + -webkit-transition: 0.4s ease-in-out; + transition: 0.4s ease-in-out; +} + +#rotate:hover, #rotate:active { + -ms-transform: rotate(180deg); /* IE 9 */ + -webkit-transform: rotate(180deg); /* Chrome, Safari, Opera */ + transform: rotate(180deg); +} + +.guestbook { + display: block; + width: 100%; + height: 100%; + min-height: 168px; + background: darkslategray; + margin: 20px auto; + box-shadow: 10px 10px 5px #162222; + border-radius: 10px; + overflow: auto; +} + +.guestbook h2 { + text-align: center; +} + +.guestbook table { + margin: 0 8px; + border: none; +} + +.guestbook table td { + padding: 8px; + text-align: left; + vertical-align: top; + white-space: preserve; +} + +.author { + font-weight: bold; + width: 110px; +} + +.guestbook-form { + display: flex; + flex-direction: row; + width: 100%; + text-align: center; + align-items: center; + margin: 10px auto; + box-shadow: 10px 10px 5px #162222; +} + +.guestbook-form input { + display: block; + float: left; + width: 90%; + resize: none; + padding: 10px; + background: darkslategray; + color: #ffffff; + border: none; + border-radius: 6px 0 0 6px; +} + +.guestbook-form #name { + float: left; + width: 120px; + border-radius: 6px; + margin-right: 8px; + font-weight: bold; +} + +.guestbook-form #entryText { + width: 80%; +} + +.guestbook-form button { + float: right; + width: 10%; + min-width: 72px; + height: auto; + padding: 10px; + border-radius: 0 6px 6px 0; + background: #2e6971;; + color: #ffffff; + border: none; + font-weight: bold; + cursor: pointer; +} + +.time { + display: inline; + opacity: 0; + color: #61959d; + font-size: small; + transition: opacity 0.5s linear; +} + +tr:hover .time { + opacity: 1; +} + +#error-message { + opacity: 0; + color: red; + display: block; + text-align: center; + margin: 10px 0; + transition: opacity 0.5s linear; +} + +#error-message.visible { + opacity: 1; +} + +@media (max-width: 640px) { + + .guestbook-form { + display: block; + box-shadow: none; + width: 100%; + padding: 0; + align-items: center; + } + + .guestbook-form input { + border-radius: 6px; + margin-bottom: 12px; + box-shadow: 10px 10px 5px #162222; + } + + .guestbook-form #name { + width: calc(100% - 20px); + margin-right: 0; + } + + .guestbook-form #entryText { + width: calc(100% - 20px); + } + + .guestbook-form button { + width: 100%; + border-radius: 6px; + box-shadow: 10px 10px 5px #162222; + } + + .time { + display: none; + } +}