🎉 Initial commit

This commit is contained in:
Marc Koch 2023-09-01 22:54:04 +02:00 committed by Marc Koch
commit 55eebe2603
13 changed files with 631 additions and 0 deletions

8
.idea/html.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,26 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="placeholder" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="pandas" />
<item index="1" class="java.lang.String" itemvalue="PyPDF2" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="ReassignedToPlainText" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/html.iml" filepath="$PROJECT_DIR$/.idea/html.iml" />
</modules>
</component>
</project>

19
.idea/php.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

129
.idea/workspace.xml Normal file
View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b737d0e3-1d2f-4475-8572-57c46c9b84d7" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/index.html" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ComposerSettings">
<execution />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="JavaScript File" />
<option value="CSS File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitLabServerManager">
<selected>-1</selected>
<servers />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="PhpDebugGeneral" listening_started="true" />
<component name="PhpServers">
<servers>
<server host="localhost" id="1d235f0c-fe6b-4b3e-b325-f93fda89810c" name="localhost" />
</servers>
</component>
<component name="PhpWorkspaceProjectConfiguration" interpreter_name="PHP 7.4" />
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 8
}</component>
<component name="ProjectId" id="2MMcvjcsDBvhOAbulzATKjUcXSP" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "main",
"last_opened_file_path": "/var/www/html/resources/images",
"list.type.of.created.stylesheet": "CSS",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "reference.settings.ide.settings.web.browsers",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/resources/images" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/resources/sounds" />
</key>
</component>
<component name="RunManager">
<configuration name="localhost" type="JavascriptDebugType" uri="http://localhost">
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-php-predefined-ba97393d7c68-6f8e3395a2b4-com.jetbrains.php.sharedIndexes-PS-233.14475.35" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="b737d0e3-1d2f-4475-8572-57c46c9b84d7" name="Changes" comment="" />
<created>1677581124694</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1677581124694</updated>
<workItem from="1677581125789" duration="6392000" />
<workItem from="1677601006571" duration="771000" />
<workItem from="1677751931686" duration="5887000" />
<workItem from="1693601579836" duration="12311000" />
<workItem from="1702037752390" duration="651000" />
<workItem from="1702145953417" duration="1797000" />
<workItem from="1702374711277" duration="607000" />
<workItem from="1708611490183" duration="21982000" />
<workItem from="1708700627345" duration="5000" />
<workItem from="1708700650122" duration="8872000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="XDebuggerManager">
<watches-manager>
<configuration name="JavascriptDebugType">
<watch expression="&#10;entr" language="HTML" />
</configuration>
</watches-manager>
</component>
<component name="XPathView.XPathProjectComponent">
<history />
<find-history />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 marc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Guest Book
A small project to train my JavaScript skills.
This project consists of a simple HTML page with some CSS and JavaScript.
The user can input a name and a message which will be displayed on the site. The
message will be sent to a [ntfy](https://ntfy.sh/) server. Messages will be
streamed from the server and displayed immediately on the page.

33
index.html Executable file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Hi!</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/resources/images/favicon.png">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="container">
<div id="hi">
<h1><span id="rotate">Hi! 🙃</span></h1>
</div>
<div class="guestbook">
<h2>Guest Book</h2>
<table id="entryTable"></table>
</div>
<div class="guestbook-form">
<label for="name"></label>
<input type="text" id="name" placeholder="Your name">
<label for="entryText"></label>
<input type="text" id="entryText" placeholder="Your message...">
<button id="submitEntry">Submit</button>
</div>
<p id="error-message">The name field contains invalid characters.</p>
</div>
<script src="script.js" type="text/javascript"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

198
script.js Normal file
View File

@ -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)

186
style.css Normal file
View File

@ -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;
}
}