fix .beattime calculation error

This commit is contained in:
Görkem 2026-03-29 12:17:57 -07:00
parent f4fb98c7eb
commit cff599d837
5 changed files with 280 additions and 14 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
guestbook/*
import/*
import/*
**/.DS_Store

View File

@ -2,19 +2,18 @@
![Human coded](https://img.shields.io/badge/human-coded-green?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1wZXJzb24tc3RhbmRpbmctaWNvbiBsdWNpZGUtcGVyc29uLXN0YW5kaW5nIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjUiIHI9IjEiLz48cGF0aCBkPSJtOSAyMCAzLTYgMyA2Ii8+PHBhdGggZD0ibTYgOCA2IDIgNi0yIi8+PHBhdGggZD0iTTEyIDEwdjQiLz48L3N2Zz4=)
[![License: GNU General Public License v3.0](https://img.shields.io/badge/License-GNU%20GPL--v3.0-yellow)](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text)
OpenGuestbook is a self-hosted lightweight Guestbook for small static websites made using python and flask. OpenGuestbook does not use databases or an admin panel for managing entries. Instead it saves each guestbook entry as a file with a human readable format in a /guestbook folder in your server. The comments can be moderated by deleting or editing files manually.
OpenGuestbook is a self-hosted lightweight open-source Guestbook for small static websites made using python & flask. OpenGuestbook does not use a database or an admin panel for managing entries. Instead it saves each guestbook entry as a file with a human readable format in a `/guestbook` folder in your server. The comments can be moderated by deleting or editing files manually.
## Features
- No login required
- No database required
- Limited total comments per day.
- Easy setup, and low CPU and RAM use.
- Privacy Friendly: No tracking, no cookies, AD blocker friendly.
- Notification support via Notfy.sh!
- Notification support via ntfy.sh!
## Deployment
OpenGuestbook comes in two parts, a backend that needs to be hosted on a server, and a JS file that cn be embedded directly on your website.
### Backend deployment with Docker
OpenGuestbook comes in two parts, a backend that needs to be hosted on a server, and a JS file that can be embedded directly on your website.
### Deployment with Docker
For production, running via Docker Compose is recommended.
ssh in to your server and clone the repository:
```
@ -26,7 +25,7 @@ cd OpenGuestbook
nano docker-compose.yml
```
Replace` <https://yourwebsite.com>` with your static website url and `<ntfy-topic>` with your [ntfy.sh topic](https://docs.ntfy.sh) to enable notificatons.
#### 1) Create docker-compose.yml
#### Create docker-compose.yml
```yaml
version: '3.8'
services:
@ -49,4 +48,4 @@ docker compose up -d --build
and your guesbook server should be up and running on port 5000 on your machine!
### Front-end Script
-To-do

18
app.py
View File

@ -13,9 +13,7 @@ app = Flask(__name__)
DAILY_LIMIT = 50
DATA_DIR = 'guestbook'
#get current UTC time and convert it to BMT
now_utc = datetime.now(timezone.utc)
bmt = now_utc + timedelta(hours=1)
currentDate = bmt.strftime('%Y-%m-%d')
submissionCountDay = 0
@ -63,9 +61,9 @@ def addComment():
return jsonify({"error": "Guestbook full for the day, try tomorrow!"}), 403
data = request.json
name = data.get('name', '').strip()
message = data.get('message', '').strip()
website = data.get('website', '').strip()
name = cleanText(data.get('name', '').strip())
message = cleanText(data.get('message', '').strip())
website = cleanText(data.get('website', '').strip())
if not name or not message:
return jsonify({"error": "Missing fields"}), 400
@ -123,6 +121,10 @@ def itime():
:returns: No. of beats (Swatch Internet Time)
:rtype: float
"""
#get current UTC time and convert it to BMT
now_utc = datetime.now(timezone.utc)
bmt = now_utc + timedelta(hours=1)
midnight = bmt.replace(hour=0, minute=0, second=0, microsecond=0)
seconds_passed = (bmt - midnight).total_seconds()
beats = int(math.floor(seconds_passed / 86.4))
@ -135,6 +137,10 @@ def itime():
return beats
def cleanText(text):
cleanText = (text.replace("<", "&lt;").replace(">", "&gt;"))
return cleanText
def send_ntfy_notification(name, message):
if not topic:
return

160
example.html Normal file
View File

@ -0,0 +1,160 @@
<link rel="stylesheet" href="https://unpkg.com/98.css">
<style>
html{
/* background-color: #008080; */
background-image: url(https://i.imgur.com/98qaCGo.jpeg);
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
}
.guestBookMessageContainer{
width:400px;
margin:auto;
}
</style>
<div class="guestBookMessageContainer" style="margin-bottom: 10px;">
<div class="window">
<div class="title-bar">
<div class="title-bar-text">New message</div>
<div class="title-bar-controls">
<button aria-label="Help"></button>
</div>
</div>
<div class="window-body">
<form id="gb-form">
<div style="margin-bottom: 10px;">
<label for="gb-name" style="display:block; margin-bottom:5px;">Name:</label>
<input type="text" id="gb-name" placeholder="name" required style="width: 100%; box-sizing: border-box;height:40px;">
</div>
<div style="margin-bottom: 10px;">
<label for="gb-site" style="display:block; margin-bottom:5px;">Your Website (Optional):</label>
<input type="text" id="gb-site" placeholder="your website url (optional)" style="width: 100%; box-sizing: border-box; height:40px;">
</div>
<div style="margin-bottom: 10px;">
<label for="gb-msg" style="display:block; margin-bottom:5px;">Message:</label>
<textarea id="gb-msg" placeholder="write something" required rows="4" style="width: 100%; box-sizing: border-box; resize: vertical;"></textarea>
</div>
<!-- <label for="daily-limit" style="display:block; margin-bottom:5px;">Daily limit:</label>
<div id="daily-limit"class="progress-indicator segmented">
<span class="progress-indicator-bar" style="width: 80%;" />
</div> -->
<div style="display: flex; justify-content: center; margin-top: 10px; ">
<button type="submit">Sign Guestbook</button>
</div>
<span id="gb-status"></span>
</form>
</div>
</div>
<div style="text-align: center; margin-top: 10px; margin-bottom: 30px; font-family: sans-serif; font-size: 12px; opacity: 0.8;">
Powered by <a href="https://code.gorkyver.com/Gorkem/OpenGuestbook" target="_blank">OpenGuestbook</a>
</div>
<div id="comment-section">
<div id="gb-entries">
<p>Loading entries...</p>
</div>
</div>
</div>
<script>
(function() {
const API_URL = 'http://127.0.0.1:5001/comments';
const form = document.getElementById('gb-form');
const list = document.getElementById('gb-entries');
const status = document.getElementById('gb-status');
async function loadComments() {
try {
const res = await fetch(API_URL);
if (!res.ok) throw new Error("Could not fetch guesbook server");
const data = await res.json();
if (data.length === 0) {
list.innerHTML = "<p>No entries yet. Be the first!</p>";
return;
}
list.innerHTML = data.map(c => {
let nameDisplay = c.name
if (c.website) {
nameDisplay = `${c.name} (<a href="${c.website}" target="_blank" style="">${c.website}</a>)`;
}
return `
<div class="window" style="margin-bottom: 20px;">
<div class="title-bar grey">
<div class="title-bar-text">
<span>${nameDisplay}</span>
</div>
<div style="margin-right:0px" class="title-bar-text">
<span>${c.date}</span>
</div>
</div>
<div class="window-body">
${c.message}
</div>
</div>
`;
}).join('');
} catch (err) {
console.error(err);
list.innerHTML = "<p style='color: red;'>Error loading guestbook.</p>";
}
}
form.onsubmit = async (e) => {
e.preventDefault();
const btn = form.querySelector('button');
const nameInput = document.getElementById('gb-name');
const msgInput = document.getElementById('gb-msg');
const siteInput = document.getElementById('gb-site');
if (btn.disabled) return;
btn.disabled = true;
btn.innerText = "Sending...";
status.innerText = "";
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: nameInput.value,
message: msgInput.value,
website: siteInput.value
})
});
const data = await res.json();
if (!res.ok) {
alert(data.error || "Error posting comment");
} else {
form.reset();
loadComments();
}
} catch (err) {
alert("Network error.");
} finally {
btn.disabled = false;
btn.innerText = "Post Comment";
}
};
loadComments();
})();
</script>

100
guestbook.js Normal file
View File

@ -0,0 +1,100 @@
(function() {
const API_URL = '<your-url>';
const form = document.getElementById('gb-form');
const list = document.getElementById('gb-entries');
const status = document.getElementById('gb-status');
function escapeHtml(text) {
if (!text) return "";
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async function loadComments() {
try {
const res = await fetch(API_URL);
if (!res.ok) throw new Error("Failed to load");
const data = await res.json();
if (data.length === 0) {
list.innerHTML = "<p>No entries yet. Be the first!</p>";
return;
}
list.innerHTML = data.map(c => {
let nameDisplay = escapeHtml(c.name);
if (c.website) {
const safeUrl = c.website.replace(/["<>;]/g, "");
nameDisplay = `${escapeHtml(c.name)} (<a href="${safeUrl}" target="_blank" style="">${safeUrl}</a>)`;
}
return `
<div style="margin-bottom: 20px;">
<div class="title-bar grey">
<div style="font-size:15px;" class="title-bar-text">
<span>${nameDisplay}</span>
</div>
<div>
<span style="font-size:15px;" class="title-bar-text">${c.date}</span>
</div>
</div>
<div class="window main-left gbComments" style="padding: 10px;">
${escapeHtml(c.message)}
</div>
</div>
`;
}).join('');
} catch (err) {
console.error(err);
list.innerHTML = "<p style='color: red;'>Error loading guestbook.</p>";
}
}
form.onsubmit = async (e) => {
e.preventDefault();
const btn = form.querySelector('button');
const nameInput = document.getElementById('gb-name');
const msgInput = document.getElementById('gb-msg');
const siteInput = document.getElementById('gb-site');
if (btn.disabled) return;
btn.disabled = true;
btn.innerText = "Sending...";
status.innerText = "";
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: nameInput.value,
message: msgInput.value,
website: siteInput.value
})
});
const data = await res.json();
if (!res.ok) {
alert(data.error || "Error posting comment");
} else {
form.reset();
loadComments();
}
} catch (err) {
alert("Network error.");
} finally {
btn.disabled = false;
btn.innerText = "Post Comment";
}
};
loadComments();
})();