add guestbook
This commit is contained in:
parent
6c52d3d375
commit
1105ceab9e
|
|
@ -51,6 +51,7 @@
|
||||||
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link" data-href="/music">Music</a>
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
<a class="nav-link selected" data-href="/88x31">88x31</a>
|
<a class="nav-link selected" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link" data-href="/music">Music</a>
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
<a class="nav-link" data-href="/88x31">88x31</a>
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link" data-href="/music">Music</a>
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
<a class="nav-link" data-href="/88x31">88x31</a>
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
<a class="nav-link selected" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link selected" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link" data-href="/music">Music</a>
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
<a class="nav-link" data-href="/88x31">88x31</a>
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link" data-href="/music">Music</a>
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
<a class="nav-link" data-href="/88x31">88x31</a>
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Guestbook</title>
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<meta name="description" content="Sign Clove's guestbook">
|
||||||
|
<meta name="keywords" content="Guestbook, Personal, Developer">
|
||||||
|
<meta name="author" content="doughmination">
|
||||||
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href="https://c.stupid.cat/guestbook">
|
||||||
|
|
||||||
|
<!-- Alternate for mobile -->
|
||||||
|
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat">
|
||||||
|
|
||||||
|
<!-- Theme Color -->
|
||||||
|
<meta name="theme-color" content="#f5c2e7">
|
||||||
|
|
||||||
|
<!-- Open Graph / Discord / Facebook -->
|
||||||
|
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
|
||||||
|
<meta property="og:site_name" content="c.stupid.cat">
|
||||||
|
<meta property="og:title" content="Guestbook | Clove Twilight">
|
||||||
|
<meta property="og:description" content="Leave a message in Clove's guestbook!">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://c.stupid.cat/guestbook">
|
||||||
|
<meta property="og:locale" content="en_GB">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="Guestbook | Clove Twilight">
|
||||||
|
<meta name="twitter:description" content="Leave a message in Clove's guestbook!">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="nav">
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a class="nav-link" data-href="/">Link Center</a>
|
||||||
|
<a class="nav-link" data-href="/cool-people">Cool People</a>
|
||||||
|
<a class="nav-link" data-href="/dev-info">Dev Info</a>
|
||||||
|
<a class="nav-link" data-href="/discord">Discord</a>
|
||||||
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link selected" data-href="/guestbook">Guestbook</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="hub friends-wrap guestbook-wrap">
|
||||||
|
<header class="hub-header">
|
||||||
|
<h1>Guestbook</h1>
|
||||||
|
<p class="tagline">Leave a little note before you go <3</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Sign form -->
|
||||||
|
<form id="gb-form" class="gb-form" autocomplete="off" novalidate>
|
||||||
|
<div class="gb-field">
|
||||||
|
<label for="gb-name">Name</label>
|
||||||
|
<input type="text" id="gb-name" name="name" maxlength="40" required
|
||||||
|
placeholder="what should I call you?">
|
||||||
|
</div>
|
||||||
|
<div class="gb-field">
|
||||||
|
<label for="gb-website">Website <span class="gb-optional">(optional)</span></label>
|
||||||
|
<input type="url" id="gb-website" name="website" maxlength="200"
|
||||||
|
placeholder="https://your-cool-site.com">
|
||||||
|
</div>
|
||||||
|
<div class="gb-field">
|
||||||
|
<label for="gb-message">Message</label>
|
||||||
|
<textarea id="gb-message" name="message" maxlength="500" rows="3" required
|
||||||
|
placeholder="say hi!"></textarea>
|
||||||
|
<span class="gb-counter" id="gb-counter">0 / 500</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Honeypot: hidden from humans, bots tend to fill it. -->
|
||||||
|
<div class="gb-hp" aria-hidden="true">
|
||||||
|
<label for="gb-url2">Leave this empty</label>
|
||||||
|
<input type="text" id="gb-url2" name="url2" tabindex="-1" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Turnstile widget renders here when a site key is configured. -->
|
||||||
|
<div id="gb-turnstile" class="gb-turnstile"></div>
|
||||||
|
|
||||||
|
<div class="gb-actions">
|
||||||
|
<button type="submit" id="gb-submit">Sign guestbook</button>
|
||||||
|
<span class="gb-status" id="gb-status" role="status"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Entries rendered by js/guestbook.js -->
|
||||||
|
<div id="gb-entries" class="gb-entries" aria-live="polite">
|
||||||
|
<p class="gb-empty">Loading messages…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/cat.js" data-cat="/assets/oneko/classics/classic.png"></script>
|
||||||
|
<script src="/js/nav.js"></script>
|
||||||
|
<script src="/js/flavors.js"></script>
|
||||||
|
<script src="/js/dev-mode.js"></script>
|
||||||
|
<script src="/js/site-switcher.js"></script>
|
||||||
|
<script src="/js/guestbook.js"
|
||||||
|
data-api="https://guestbook.doughmination.uk"
|
||||||
|
data-turnstile-key="0x4AAAAAAB08ZhSxKn5rAD3d"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link" data-href="/music">Music</a>
|
<a class="nav-link" data-href="/music">Music</a>
|
||||||
<a class="nav-link" data-href="/88x31">88x31</a>
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// ---- config (from the <script> data-* attributes) ------------------
|
||||||
|
var script =
|
||||||
|
document.currentScript ||
|
||||||
|
document.querySelector('script[src*="guestbook.js"]');
|
||||||
|
var API = (script && script.dataset.api) || "";
|
||||||
|
var TURNSTILE_KEY = (script && script.dataset.turnstileKey) || "";
|
||||||
|
|
||||||
|
var STYLE_ID = "guestbook-styles";
|
||||||
|
|
||||||
|
// ---- styles (injected, mirrors visitor-counter.js pattern) ----------
|
||||||
|
function injectStyles() {
|
||||||
|
if (document.getElementById(STYLE_ID)) return;
|
||||||
|
var s = document.createElement("style");
|
||||||
|
s.id = STYLE_ID;
|
||||||
|
s.textContent = [
|
||||||
|
".guestbook-wrap { max-width: 640px; }",
|
||||||
|
".gb-form {",
|
||||||
|
" display: flex; flex-direction: column; gap: 0.85rem;",
|
||||||
|
" background: var(--surface-0); border: 1px solid var(--surface-1);",
|
||||||
|
" border-radius: 14px; padding: 1.1rem 1.2rem; margin-bottom: 2rem;",
|
||||||
|
"}",
|
||||||
|
".gb-field { display: flex; flex-direction: column; gap: 0.3rem; position: relative; }",
|
||||||
|
".gb-field label { font-size: 0.8rem; color: var(--subtext-0); letter-spacing: 0.03em; }",
|
||||||
|
".gb-optional { color: var(--overlay-0); }",
|
||||||
|
".gb-form input, .gb-form textarea {",
|
||||||
|
" font-family: inherit; font-size: 0.95rem; color: var(--text);",
|
||||||
|
" background: var(--mantle); border: 1px solid var(--surface-1);",
|
||||||
|
" border-radius: 9px; padding: 0.55rem 0.7rem; width: 100%; resize: vertical;",
|
||||||
|
" transition: border-color 0.15s ease, box-shadow 0.15s ease;",
|
||||||
|
"}",
|
||||||
|
".gb-form input:focus, .gb-form textarea:focus {",
|
||||||
|
" outline: none; border-color: var(--pink);",
|
||||||
|
" box-shadow: inset 0 0 0 1px var(--pink);",
|
||||||
|
"}",
|
||||||
|
".gb-counter { align-self: flex-end; font-size: 0.7rem; color: var(--overlay-0); }",
|
||||||
|
".gb-turnstile:empty { display: none; }",
|
||||||
|
".gb-actions { display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap; }",
|
||||||
|
".gb-form button {",
|
||||||
|
" font-family: inherit; font-size: 0.9rem; color: var(--crust);",
|
||||||
|
" background: var(--pink); border: none; border-radius: 9px;",
|
||||||
|
" padding: 0.55rem 1.1rem; font-weight: 700;",
|
||||||
|
" transition: transform 0.12s ease, opacity 0.12s ease;",
|
||||||
|
"}",
|
||||||
|
".gb-form button:hover:not(:disabled) { transform: translateY(-1px); }",
|
||||||
|
".gb-form button:disabled { opacity: 0.55; }",
|
||||||
|
".gb-status { font-size: 0.82rem; color: var(--subtext-0); }",
|
||||||
|
".gb-status.gb-err { color: var(--red); }",
|
||||||
|
".gb-status.gb-ok { color: var(--green); }",
|
||||||
|
// honeypot: visually hidden but still in the DOM for bots
|
||||||
|
".gb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }",
|
||||||
|
".gb-entries { display: flex; flex-direction: column; gap: 0.9rem; padding-bottom: 4.5rem; }",
|
||||||
|
".gb-empty { color: var(--subtext-0); text-align: center; font-size: 0.9rem; }",
|
||||||
|
".gb-entry {",
|
||||||
|
" background: var(--surface-0); border: 1px solid var(--surface-1);",
|
||||||
|
" border-radius: 12px; padding: 0.85rem 1rem;",
|
||||||
|
" transition: border-color 0.15s ease, transform 0.15s ease;",
|
||||||
|
"}",
|
||||||
|
".gb-entry:hover { border-color: rgb(var(--accent-rgb)); transform: translateY(-2px); }",
|
||||||
|
".gb-entry-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.35rem; }",
|
||||||
|
".gb-entry-name { font-weight: 700; color: rgb(var(--accent-rgb)); }",
|
||||||
|
".gb-entry-name a { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--overlay-1); }",
|
||||||
|
".gb-entry-name a:hover { border-bottom-color: var(--pink); }",
|
||||||
|
".gb-entry-time { font-size: 0.72rem; color: var(--overlay-0); margin-left: auto; }",
|
||||||
|
".gb-entry-msg { color: var(--text); font-size: 0.92rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }",
|
||||||
|
].join("\n");
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers --------------------------------------------------------
|
||||||
|
function esc(str) {
|
||||||
|
return String(str == null ? "" : str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function relTime(ts) {
|
||||||
|
var diff = Math.floor((Date.now() - ts) / 1000);
|
||||||
|
if (diff < 60) return "just now";
|
||||||
|
if (diff < 3600) return Math.floor(diff / 60) + "m ago";
|
||||||
|
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
||||||
|
if (diff < 604800) return Math.floor(diff / 86400) + "d ago";
|
||||||
|
try {
|
||||||
|
return new Date(ts).toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryHTML(e) {
|
||||||
|
var name = esc(e.name);
|
||||||
|
var nameHTML = e.website
|
||||||
|
? '<a href="' + esc(e.website) + '" target="_blank" rel="noopener nofollow ugc">' + name + "</a>"
|
||||||
|
: name;
|
||||||
|
return (
|
||||||
|
'<div class="gb-entry">' +
|
||||||
|
'<div class="gb-entry-head">' +
|
||||||
|
'<span class="gb-entry-name">' + nameHTML + "</span>" +
|
||||||
|
'<span class="gb-entry-time">' + esc(relTime(e.ts)) + "</span>" +
|
||||||
|
"</div>" +
|
||||||
|
'<div class="gb-entry-msg">' + esc(e.message) + "</div>" +
|
||||||
|
"</div>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- rendering ------------------------------------------------------
|
||||||
|
var entriesEl, formEl, statusEl, submitEl, counterEl, msgEl;
|
||||||
|
|
||||||
|
function setStatus(text, kind) {
|
||||||
|
if (!statusEl) return;
|
||||||
|
statusEl.textContent = text || "";
|
||||||
|
statusEl.className = "gb-status" + (kind ? " gb-" + kind : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEntries(list) {
|
||||||
|
if (!entriesEl) return;
|
||||||
|
if (!list || !list.length) {
|
||||||
|
entriesEl.innerHTML = '<p class="gb-empty">No messages yet — be the first to sign!</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entriesEl.innerHTML = list.map(entryHTML).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEntries() {
|
||||||
|
if (!API) {
|
||||||
|
renderEntries([]);
|
||||||
|
setStatus("Guestbook API not configured yet.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var res = await fetch(API + "/?limit=100", { method: "GET" });
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
var data = await res.json();
|
||||||
|
renderEntries(data.entries || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[guestbook] load failed", err);
|
||||||
|
entriesEl.innerHTML = '<p class="gb-empty">Could not load messages right now.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Turnstile (optional) ------------------------------------------
|
||||||
|
function turnstileToken() {
|
||||||
|
try {
|
||||||
|
if (global.turnstile && typeof global.turnstile.getResponse === "function") {
|
||||||
|
return global.turnstile.getResponse() || "";
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
// Fallback: the widget injects a hidden input named cf-turnstile-response
|
||||||
|
var input = document.querySelector('[name="cf-turnstile-response"]');
|
||||||
|
return input ? input.value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTurnstile() {
|
||||||
|
if (!TURNSTILE_KEY) return;
|
||||||
|
var holder = document.getElementById("gb-turnstile");
|
||||||
|
if (holder) {
|
||||||
|
holder.className = "cf-turnstile gb-turnstile";
|
||||||
|
holder.setAttribute("data-sitekey", TURNSTILE_KEY);
|
||||||
|
holder.setAttribute("data-theme", "dark");
|
||||||
|
}
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
||||||
|
s.async = true;
|
||||||
|
s.defer = true;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- submit ---------------------------------------------------------
|
||||||
|
async function onSubmit(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (!API) {
|
||||||
|
setStatus("Guestbook API not configured yet.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = {
|
||||||
|
name: formEl.name.value,
|
||||||
|
website: formEl.website.value,
|
||||||
|
message: formEl.message.value,
|
||||||
|
url2: formEl.url2.value, // honeypot
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!payload.name.trim() || !payload.message.trim()) {
|
||||||
|
setStatus("Name and message are both required.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TURNSTILE_KEY) {
|
||||||
|
var token = turnstileToken();
|
||||||
|
if (!token) {
|
||||||
|
setStatus("Please complete the captcha first.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.turnstileToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitEl.disabled = true;
|
||||||
|
setStatus("Signing…");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var res = await fetch(API + "/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
var data = await res.json().catch(function () { return {}; });
|
||||||
|
if (!res.ok) {
|
||||||
|
setStatus(data.error || "Something went wrong. Try again.", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("Thanks for signing! 💕", "ok");
|
||||||
|
formEl.reset();
|
||||||
|
if (counterEl) counterEl.textContent = "0 / 500";
|
||||||
|
try {
|
||||||
|
if (global.turnstile && global.turnstile.reset) global.turnstile.reset();
|
||||||
|
} catch (_) {}
|
||||||
|
await loadEntries();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[guestbook] submit failed", err);
|
||||||
|
setStatus("Network error — please try again.", "err");
|
||||||
|
} finally {
|
||||||
|
submitEl.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- init -----------------------------------------------------------
|
||||||
|
function init() {
|
||||||
|
injectStyles();
|
||||||
|
entriesEl = document.getElementById("gb-entries");
|
||||||
|
formEl = document.getElementById("gb-form");
|
||||||
|
statusEl = document.getElementById("gb-status");
|
||||||
|
submitEl = document.getElementById("gb-submit");
|
||||||
|
counterEl = document.getElementById("gb-counter");
|
||||||
|
msgEl = document.getElementById("gb-message");
|
||||||
|
|
||||||
|
if (msgEl && counterEl) {
|
||||||
|
var update = function () {
|
||||||
|
counterEl.textContent = msgEl.value.length + " / 500";
|
||||||
|
};
|
||||||
|
msgEl.addEventListener("input", update);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formEl) formEl.addEventListener("submit", onSubmit);
|
||||||
|
|
||||||
|
loadTurnstile();
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Guestbook = { reload: loadEntries };
|
||||||
|
})(typeof window !== "undefined" ? window : this);
|
||||||
|
|
@ -38,15 +38,6 @@
|
||||||
return '<img class="t-social-ic" src="/assets/socials/' + (SOCIAL_ICON[key] || "site") + '.svg" alt="">';
|
return '<img class="t-social-ic" src="/assets/socials/' + (SOCIAL_ICON[key] || "site") + '.svg" alt="">';
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache = null;
|
|
||||||
async function checkDomain(subdomain) {
|
|
||||||
if (!cache) {
|
|
||||||
const response = await fetch("https://raw.is-a.dev/v2.json");
|
|
||||||
cache = await response.json();
|
|
||||||
}
|
|
||||||
return cache.some((d) => d.subdomain === subdomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// arch.ascii (hyfetch format) is fetched once at startup for `hyfetch`.
|
// arch.ascii (hyfetch format) is fetched once at startup for `hyfetch`.
|
||||||
let archLines = null;
|
let archLines = null;
|
||||||
function loadArt() {
|
function loadArt() {
|
||||||
|
|
@ -130,29 +121,15 @@
|
||||||
out += "\n\nTip: type a social's name (try 'socials') to open it.";
|
out += "\n\nTip: type a social's name (try 'socials') to open it.";
|
||||||
return { text: out };
|
return { text: out };
|
||||||
},
|
},
|
||||||
ls() {
|
ls() {return this.help()},
|
||||||
const rows = [
|
|
||||||
["help", "show this list"],
|
|
||||||
["code", "Shows the website source code"],
|
|
||||||
["socials", "list all socials"],
|
|
||||||
["<social>", "show a social & ask to open it (append -open to do directly)"],
|
|
||||||
["system", "open my system website (append a person's name to open their page)"],
|
|
||||||
["about", "a little about me"],
|
|
||||||
["hyfetch", "system info, with flair"]
|
|
||||||
];
|
|
||||||
let out = "Available commands:\n";
|
|
||||||
out += rows.map((r) => " " + r[0].padEnd(12) + r[1]).join("\n");
|
|
||||||
out += "\n\nTip: type a social's name (try 'socials') to open it.";
|
|
||||||
return { text: out };
|
|
||||||
},
|
|
||||||
code() {
|
code() {
|
||||||
window.open("https://git.gay/doughmination/clove-is-a-dev", "_blank");
|
window.open("https://git.gay/doughmination/c.stupid.cat", "_blank");
|
||||||
return { text: "Opening site source code..." }
|
return { text: "Opening site source code..." }
|
||||||
},
|
},
|
||||||
async system(args) {
|
async system(args) {
|
||||||
const who = (args[0] || "").toLowerCase();
|
const who = (args[0] || "").toLowerCase();
|
||||||
if (!who) {
|
if (!who) {
|
||||||
window.open("https://system.doughmination.co.uk/", "_blank");
|
window.open("https://doughmination.co.uk/", "_blank");
|
||||||
return { text: "Opening system site..." };
|
return { text: "Opening system site..." };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -188,9 +165,7 @@
|
||||||
"Clove Twilight — fae/faer\n" +
|
"Clove Twilight — fae/faer\n" +
|
||||||
"Transfem developer from Southampton, UK. I make Discord bots,\n" +
|
"Transfem developer from Southampton, UK. I make Discord bots,\n" +
|
||||||
"personal-site nonsense, and run a small corner of the internet\n" +
|
"personal-site nonsense, and run a small corner of the internet\n" +
|
||||||
"under the trade mark 'doughmination system'. Big on Linux, Catppuccin, and cats.\n\n" +
|
"under the trade mark 'doughmination system'. Big on Linux, Catppuccin, and cats."
|
||||||
"This site is the beta playground for c.stupid.cat — expect things\n" +
|
|
||||||
"to break in funny ways. Type 'socials' to find me elsewhere."
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
hyfetch() {
|
hyfetch() {
|
||||||
|
|
@ -222,23 +197,6 @@
|
||||||
'<pre class="hf-info">' + info + "</pre></div>"
|
'<pre class="hf-info">' + info + "</pre></div>"
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async isadotdev(parts) {
|
|
||||||
const arg = (parts[0] || "").toLowerCase().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
||||||
if (!arg || !arg.endsWith(".is-a.dev")) {
|
|
||||||
return { text: "usage: isadotdev <subdomain>.is-a.dev", error: true };
|
|
||||||
}
|
|
||||||
const sub = arg.replace(/\.is-a\.dev$/, "");
|
|
||||||
if (["clove", "doughmination"].indexOf(sub) >= 0) {
|
|
||||||
return { text: "nice try 👀", error: true };
|
|
||||||
}
|
|
||||||
showResult({ text: "checking " + arg + "…" });
|
|
||||||
let found;
|
|
||||||
try { found = await checkDomain(sub); }
|
|
||||||
catch (e) { return { text: "couldn't reach the is-a.dev registry — try again later.", error: true }; }
|
|
||||||
if (!found) return { text: arg + " isn't registered on is-a.dev.", error: true };
|
|
||||||
window.open("https://" + arg, "_blank", "noopener");
|
|
||||||
return { html: 'opening <b class="t-accent">' + esc(arg) + "</b> …" };
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- runtime ------------------------------------------------------------
|
// ---- runtime ------------------------------------------------------------
|
||||||
|
|
@ -305,11 +263,6 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.endsWith(".is-a.dev")) {
|
|
||||||
runCommand(COMMANDS.isadotdev, [name]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (COMMANDS[name]) { runCommand(COMMANDS[name], parts.slice(1)); return; }
|
if (COMMANDS[name]) { runCommand(COMMANDS[name], parts.slice(1)); return; }
|
||||||
|
|
||||||
showResult({ text: "clovesh: command not found: " + name + "\nType 'help' for a list, or 'socials' to browse.", error: true });
|
showResult({ text: "clovesh: command not found: " + name + "\nType 'help' for a list, or 'socials' to browse.", error: true });
|
||||||
|
|
@ -352,7 +305,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- tab-complete + history --------------------------------------------
|
// ---- tab-complete + history --------------------------------------------
|
||||||
const COMPLETIONS = Object.keys(COMMANDS).concat(["open", "socials", "isadotdev"], Object.keys(SOCIALS), Object.keys(ALIASES));
|
const COMPLETIONS = Object.keys(COMMANDS).concat(["open", "socials"], Object.keys(SOCIALS), Object.keys(ALIASES));
|
||||||
function complete(prefix) {
|
function complete(prefix) {
|
||||||
if (!prefix) return null;
|
if (!prefix) return null;
|
||||||
const hits = COMPLETIONS.filter((c) => c.indexOf(prefix) === 0);
|
const hits = COMPLETIONS.filter((c) => c.indexOf(prefix) === 0);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
<a class="nav-link" data-href="/discord-bots">Discord Bots</a>
|
||||||
<a class="nav-link selected" data-href="/music">Music</a>
|
<a class="nav-link selected" data-href="/music">Music</a>
|
||||||
<a class="nav-link" data-href="/88x31">88x31</a>
|
<a class="nav-link" data-href="/88x31">88x31</a>
|
||||||
|
<a class="nav-link" data-href="/guestbook">Guestbook</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue