From 8d13c52d18f3c42a5efc6cc7a64d194e5ea184c4 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 11 Jan 2026 09:52:49 +0000 Subject: [PATCH 1/2] small mobile pass --- assets/sass/pages/lavalamp-adoptable.scss | 13 +++++++++---- assets/sass/pages/resources.scss | 2 -- layouts/shortcodes/lavalamp-adoptable.html | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/assets/sass/pages/lavalamp-adoptable.scss b/assets/sass/pages/lavalamp-adoptable.scss index c05f4d1..dac61c8 100644 --- a/assets/sass/pages/lavalamp-adoptable.scss +++ b/assets/sass/pages/lavalamp-adoptable.scss @@ -70,6 +70,13 @@ .preview-100 { width: 100px; height: 200px; + + @include media-down(md) { + position: absolute; + top: 10px; + right: 10px; + z-index: 900; + } } .preview-200 { @@ -185,12 +192,10 @@ transform: translate(var(--start-x), var(--start-y)) scale(1); } 25% { - transform: translate(var(--mid1-x), var(--mid1-y)) - scale(var(--scale1, 1.1)); + transform: translate(var(--mid1-x), var(--mid1-y)) scale(var(--scale1, 1.1)); } 50% { - transform: translate(var(--mid2-x), var(--mid2-y)) - scale(var(--scale2, 0.9)); + transform: translate(var(--mid2-x), var(--mid2-y)) scale(var(--scale2, 0.9)); } 75% { transform: translate(var(--mid3-x), var(--mid3-y)) diff --git a/assets/sass/pages/resources.scss b/assets/sass/pages/resources.scss index 727c698..70c7fbf 100644 --- a/assets/sass/pages/resources.scss +++ b/assets/sass/pages/resources.scss @@ -811,7 +811,6 @@ width: 5px; height: 100%; background: linear-gradient(180deg, transparent, #0096ff, transparent); - //box-shadow: 0 0 10px rgba(0, 150, 255, 0.8); } // Orange line on the right @@ -824,7 +823,6 @@ width: 5px; height: 100%; background: linear-gradient(180deg, transparent, #ff7800, transparent); - //box-shadow: 0 0 10px rgba(255, 120, 0, 0.8); } @include media-down(md) { diff --git a/layouts/shortcodes/lavalamp-adoptable.html b/layouts/shortcodes/lavalamp-adoptable.html index 5d7fbbd..c256317 100644 --- a/layouts/shortcodes/lavalamp-adoptable.html +++ b/layouts/shortcodes/lavalamp-adoptable.html @@ -1,9 +1,9 @@
-

Preview

+

Preview

-
+
- 100px × 200px + 100px × 200px
-
+
Date: Sun, 11 Jan 2026 12:31:35 +0000 Subject: [PATCH 2/2] Adding guestbook --- assets/js/guestbook.js | 371 +++++++++++++++++++++++++++++ assets/sass/pages/guestbook.scss | 336 ++++++++++++++++++++++++++ assets/sass/pages/homepage.scss | 46 +++- assets/sass/style.scss | 1 + content/guestbook.md | 11 + layouts/_default/baseof.html | 6 + layouts/guestbook/single.html | 112 +++++++++ layouts/index.html | 14 +- layouts/partials/site-scripts.html | 2 +- 9 files changed, 895 insertions(+), 4 deletions(-) create mode 100644 assets/js/guestbook.js create mode 100644 assets/sass/pages/guestbook.scss create mode 100644 content/guestbook.md create mode 100644 layouts/guestbook/single.html diff --git a/assets/js/guestbook.js b/assets/js/guestbook.js new file mode 100644 index 0000000..747a323 --- /dev/null +++ b/assets/js/guestbook.js @@ -0,0 +1,371 @@ +/** + * Guestbook functionality for ritual.sh + * Custom implementation that calls the guestbook API directly + */ + +class GuestbookManager { + constructor() { + // Configuration - Update this URL when the backend is deployed + this.apiUrl = "https://guestbook.ritual.sh"; + this.perPage = 20; + this.currentPage = 1; + this.totalPages = 1; + + // DOM elements + this.form = document.getElementById("guestbook-form"); + this.entriesList = document.getElementById("entries-list"); + this.entriesLoading = document.getElementById("entries-loading"); + this.entriesError = document.getElementById("entries-error"); + this.pagination = document.getElementById("pagination"); + this.formFeedback = document.getElementById("form-feedback"); + this.submitBtn = document.getElementById("submit-btn"); + + this.init(); + } + + init() { + if (!this.form) return; + + // Attach event listeners + this.form.addEventListener("submit", (e) => this.handleSubmit(e)); + + // Load initial entries + this.loadEntries(); + } + + /** + * Load guestbook entries from the API + */ + async loadEntries(page = 1) { + this.currentPage = page; + + // Show loading state + this.showLoading(); + + try { + const response = await fetch( + `${this.apiUrl}/entries?page=${page}&per_page=${this.perPage}`, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Handle the actual API response structure + if (data.entries !== undefined) { + // API returns entries directly + this.renderEntries(data.entries || []); + this.totalPages = data.total_pages || 1; + + // Create pagination object from the flat response + const pagination = { + current_page: data.page || 1, + total_pages: data.total_pages || 1, + total_entries: data.total || 0, + per_page: data.per_page || this.perPage + }; + this.renderPagination(pagination); + } else if (data.success === false) { + // API returned an error + throw new Error(data.error || "Failed to load entries"); + } else { + throw new Error("Unexpected API response format"); + } + } catch (error) { + console.error("Error loading entries:", error); + this.showError(); + } + } + + /** + * Render entries to the DOM + */ + renderEntries(entries) { + // Hide loading and error states + this.entriesLoading.style.display = "none"; + this.entriesError.style.display = "none"; + this.entriesList.style.display = "block"; + + if (entries.length === 0) { + this.entriesList.innerHTML = ` +
+

No entries yet. Be the first to sign the guestbook!

+ _ +
+ `; + return; + } + + // Build entries HTML + const entriesHTML = entries + .map((entry) => this.renderEntry(entry)) + .join(""); + + this.entriesList.innerHTML = entriesHTML; + } + + /** + * Render a single entry + */ + renderEntry(entry) { + const date = this.formatDate(entry.timestamp); + const nameHTML = entry.website + ? `${this.escapeHtml(entry.name)}` + : this.escapeHtml(entry.name); + + // Display website URL without https:// protocol + const websiteDisplay = entry.website + ? `|${this.formatWebsiteUrl(entry.website)}` + : ""; + + return ` +
+
+ ${nameHTML} + ${websiteDisplay} + +
+
${this.escapeHtml(entry.message)}
+
+ `; + } + + /** + * Render pagination controls + */ + renderPagination(pagination) { + if (pagination.total_pages <= 1) { + this.pagination.style.display = "none"; + return; + } + + this.pagination.style.display = "flex"; + + const prevDisabled = pagination.current_page === 1 ? "disabled" : ""; + const nextDisabled = + pagination.current_page === pagination.total_pages ? "disabled" : ""; + + let pagesHTML = ""; + + // Show page numbers (max 5) + const startPage = Math.max(1, pagination.current_page - 2); + const endPage = Math.min(pagination.total_pages, startPage + 4); + + for (let i = startPage; i <= endPage; i++) { + const active = i === pagination.current_page ? "active" : ""; + pagesHTML += ` + + `; + } + + this.pagination.innerHTML = ` +
+ Page ${pagination.current_page} of ${pagination.total_pages} + (${pagination.total_entries} entries) +
+
+ + ${pagesHTML} + +
+ `; + + // Attach click handlers to pagination buttons + this.pagination.querySelectorAll(".pagination-button").forEach((btn) => { + btn.addEventListener("click", (e) => { + const page = parseInt(e.target.dataset.page); + if (page && page !== pagination.current_page) { + this.loadEntries(page); + // Scroll to top of entries + document + .querySelector(".guestbook-entries-container") + .scrollIntoView({ behavior: "smooth" }); + } + }); + }); + } + + /** + * Handle form submission + */ + async handleSubmit(e) { + e.preventDefault(); + + // Disable submit button + this.submitBtn.disabled = true; + this.submitBtn.querySelector(".button-text").textContent = "[ SENDING... ]"; + + // Clear previous feedback + this.formFeedback.className = "form-feedback"; + this.formFeedback.textContent = ""; + + // Get form data + const formData = new FormData(this.form); + const data = { + name: formData.get("name"), + email: formData.get("email"), + website: formData.get("website"), + message: formData.get("message"), + }; + + try { + const response = await fetch(`${this.apiUrl}/submit`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + const result = await response.json(); + + // Handle success response + if (result.success || response.ok) { + this.showSuccess( + result.message || + "Entry submitted! It will appear after moderation. Thank you!", + ); + this.form.reset(); + + // If entry was auto-approved, reload entries + if (result.status === "approved") { + setTimeout(() => this.loadEntries(1), 1000); + } + } else { + this.showFormError(result.error || result.message || "Failed to submit entry"); + } + } catch (error) { + console.error("Error submitting entry:", error); + this.showFormError( + "Network error. Please check your connection and try again.", + ); + } finally { + // Re-enable submit button + this.submitBtn.disabled = false; + this.submitBtn.querySelector(".button-text").textContent = "[ SUBMIT ]"; + } + } + + /** + * Show loading state + */ + showLoading() { + this.entriesLoading.style.display = "block"; + this.entriesError.style.display = "none"; + this.entriesList.style.display = "none"; + this.pagination.style.display = "none"; + } + + /** + * Show error state + */ + showError() { + this.entriesLoading.style.display = "none"; + this.entriesError.style.display = "block"; + this.entriesList.style.display = "none"; + this.pagination.style.display = "none"; + } + + /** + * Show form success message + */ + showSuccess(message) { + this.formFeedback.className = "form-feedback success"; + this.formFeedback.textContent = `SUCCESS: ${message}`; + + // Auto-hide after 10 seconds + setTimeout(() => { + this.formFeedback.className = "form-feedback"; + }, 10000); + } + + /** + * Show form error message + */ + showFormError(message) { + this.formFeedback.className = "form-feedback error"; + this.formFeedback.textContent = `ERROR: ${message}`; + + // Auto-hide after 8 seconds + setTimeout(() => { + this.formFeedback.className = "form-feedback"; + }, 8000); + } + + /** + * Format date to readable format + */ + formatDate(dateString) { + if (!dateString) return "Unknown date"; + + const date = new Date(dateString); + + // Check if date is valid + if (isNaN(date.getTime())) { + return "Unknown date"; + } + + const now = new Date(); + const diffTime = Math.abs(now - date); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return "Today"; + } else if (diffDays === 1) { + return "Yesterday"; + } else if (diffDays < 7) { + return `${diffDays} days ago`; + } else { + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + }); + } + } + + /** + * Format website URL for display (remove protocol) + */ + formatWebsiteUrl(url) { + if (!url) return ""; + + try { + const urlObj = new URL(url); + // Return hostname + pathname, removing trailing slash + let display = urlObj.hostname + urlObj.pathname; + return this.escapeHtml(display.replace(/\/$/, "")); + } catch (e) { + // If URL parsing fails, just remove common protocols + return this.escapeHtml( + url.replace(/^https?:\/\//, "").replace(/\/$/, "") + ); + } + } + + /** + * Escape HTML to prevent XSS + */ + escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } +} + +// Initialize guestbook when DOM is ready +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => { + new GuestbookManager(); + }); +} else { + new GuestbookManager(); +} diff --git a/assets/sass/pages/guestbook.scss b/assets/sass/pages/guestbook.scss new file mode 100644 index 0000000..b640a00 --- /dev/null +++ b/assets/sass/pages/guestbook.scss @@ -0,0 +1,336 @@ +@import "../mixins"; + +.guestbook-page { + color: white; + margin: auto; + + .content-screen { + position: relative !important; + } + + > .guestbook-content { + width: 50%; + margin: auto; + padding: 2rem; + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; + + @include media-down(lg) { + width: 100%; + grid-template-columns: 1fr; + } + > .wide-item { + grid-column: 1 / -1; + } + + .guestbook-floppy { + margin: auto; + padding: 2rem; + width: 90%; + max-width: 300px; + transform: rotate(5deg); + } + } +} + +// Terminal output styling +.terminal-output { + font-family: monospace; + line-height: 1.6; + + .info { + color: #0f0; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); + } + + .dim { + opacity: 0.7; + } + + .warning { + color: #ff9900; + text-shadow: 0 0 5px rgba(255, 153, 0, 0.5); + } + + .error-text { + color: #ff0000; + text-shadow: 0 0 5px rgba(255, 0, 0, 0.5); + } +} + +// Guestbook Form +.guestbook-form { + margin-top: 0.5rem; + + .form-row { + margin-bottom: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.2rem; + + label { + font-family: monospace; + color: #0f0; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); + font-size: 0.85rem; + } + + input, + textarea { + background: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(0, 255, 0, 0.3); + color: #0f0; + padding: 0.4rem; + font-family: monospace; + font-size: 0.85rem; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.3); + border-radius: 2px; + outline: none; + transition: all 0.3s ease; + + &::placeholder { + color: rgba(0, 255, 0, 0.4); + text-shadow: none; + } + + &:focus { + border-color: rgba(0, 255, 0, 0.6); + box-shadow: + 0 0 10px rgba(0, 255, 0, 0.3), + inset 0 0 10px rgba(0, 255, 0, 0.1); + } + } + + textarea { + resize: vertical; + min-height: 60px; + } + } + + .form-feedback { + margin: 0.5rem 0; + padding: 0.5rem; + border-radius: 3px; + font-family: monospace; + font-size: 0.8rem; + display: none; + + &.success { + display: block; + background: rgba(0, 255, 0, 0.1); + border: 1px solid rgba(0, 255, 0, 0.3); + color: #0f0; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); + } + + &.error { + display: block; + background: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + color: #ff0000; + text-shadow: 0 0 5px rgba(255, 0, 0, 0.5); + } + + &.pending { + display: block; + background: rgba(255, 153, 0, 0.1); + border: 1px solid rgba(255, 153, 0, 0.3); + color: #ff9900; + text-shadow: 0 0 5px rgba(255, 153, 0, 0.5); + } + } + + .terminal-button { + background: rgba(0, 255, 0, 0.1); + border: 2px solid rgba(0, 255, 0, 0.4); + color: #0f0; + padding: 0.5rem 1rem; + font-family: monospace; + font-size: 0.9rem; + font-weight: bold; + cursor: pointer; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); + transition: all 0.3s ease; + border-radius: 3px; + letter-spacing: 2px; + + &:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.2); + border-color: rgba(0, 255, 0, 0.7); + box-shadow: + 0 0 15px rgba(0, 255, 0, 0.4), + inset 0 0 10px rgba(0, 255, 0, 0.2); + text-shadow: 0 0 10px rgba(0, 255, 0, 0.8); + transform: translateY(-1px); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + border-color: rgba(0, 255, 0, 0.2); + } + } +} + +// Entries List +.entries-list { + margin-top: 1rem; + + .entry { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid rgba(0, 255, 0, 0.2); + font-family: monospace; + + &:last-child { + border-bottom: none; + } + + .entry-header { + margin-bottom: 0.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: baseline; + + .entry-name { + color: #0f0; + font-weight: bold; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); + + a { + color: #0f0; + text-decoration: none; + border-bottom: 1px dotted rgba(0, 255, 0, 0.5); + transition: all 0.3s ease; + + &:hover { + border-bottom-style: solid; + text-shadow: 0 0 10px rgba(0, 255, 0, 0.8); + } + } + } + + .entry-website { + color: rgba(0, 255, 0, 0.6); + font-size: 0.85rem; + font-style: italic; + } + + .entry-separator { + color: rgba(0, 255, 0, 0.4); + } + + .entry-date { + color: rgba(0, 255, 0, 0.6); + font-size: 0.85rem; + margin-left: auto; + } + } + + .entry-message { + color: #0f0; + line-height: 1.6; + opacity: 0.9; + white-space: pre-wrap; + word-wrap: break-word; + } + } +} + +// Loading state +.loading-state { + padding: 2rem; + text-align: center; + font-family: monospace; + + .loading-text { + color: #0f0; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.5); + } + + .loading-dots { + color: #0f0; + animation: loading-blink 1.5s infinite; + } +} + +@keyframes loading-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +// Error state +.error-state { + padding: 2rem; + text-align: center; + font-family: monospace; +} + +// Pagination +.pagination { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid rgba(0, 255, 0, 0.2); + display: flex; + justify-content: space-between; + align-items: center; + font-family: monospace; + gap: 1rem; + flex-wrap: wrap; + + .pagination-info { + color: rgba(0, 255, 0, 0.7); + font-size: 0.85rem; + } + + .pagination-controls { + display: flex; + gap: 0.5rem; + } + + .pagination-button { + background: rgba(0, 255, 0, 0.1); + border: 1px solid rgba(0, 255, 0, 0.3); + color: #0f0; + padding: 0.5rem 1rem; + font-family: monospace; + font-size: 0.85rem; + cursor: pointer; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.5); + transition: all 0.3s ease; + border-radius: 2px; + + &:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.2); + border-color: rgba(0, 255, 0, 0.5); + box-shadow: 0 0 10px rgba(0, 255, 0, 0.3); + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + &.active { + background: rgba(0, 255, 0, 0.3); + border-color: rgba(0, 255, 0, 0.6); + font-weight: bold; + } + } +} + +// Scrollbar customization for entries container +.guestbook-entries-container .screen-display { + @include scrollbar-custom(#0f0, 5px); +} diff --git a/assets/sass/pages/homepage.scss b/assets/sass/pages/homepage.scss index e06bfd3..b68cb33 100644 --- a/assets/sass/pages/homepage.scss +++ b/assets/sass/pages/homepage.scss @@ -171,7 +171,7 @@ grid-template-columns: 1fr 1fr; @include media-up(lg) { - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(6, 1fr); position: absolute; bottom: 5%; width: 100%; @@ -186,6 +186,46 @@ justify-content: center; } + .nav-floppy { + width: 44%; + transform: rotate(5deg); + position: relative; + + &:hover { + .nav-floppy-text { + opacity: 1; + } + } + + .nav-floppy-text { + position: absolute; + display: block; + bottom: 0; + right: 0; + color: white; + font-size: 20px; + font-weight: bold; + z-index: 8000; + transform: rotate(10deg); + border: 1px solid #0f0; + padding: 2px; + padding-left: 5px; + padding-right: 5px; + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.7); + opacity: 0; + transition: opacity 0.3s ease; + text-align: center; + + @include media-down(lg) { + opacity: 1; + //transform: rotate(0deg); + bottom: 0; + font-size: 14px; + } + } + } + .nav-cube { width: 106.5px; position: relative; @@ -224,6 +264,10 @@ transition: opacity 0.3s ease; text-align: center; + &:hover::after { + opacity: 1; + } + @include media-down(lg) { opacity: 1; //transform: rotate(0deg); diff --git a/assets/sass/style.scss b/assets/sass/style.scss index 527a2dc..8860c32 100644 --- a/assets/sass/style.scss +++ b/assets/sass/style.scss @@ -29,6 +29,7 @@ @import "pages/resources"; @import "pages/button-generator"; @import "pages/lavalamp-adoptable"; +@import "pages/guestbook"; @import url(https://fonts.bunny.net/css?family=abel:400|barlow-condensed:400,500|caveat:400|lato:300,300i,400,400i|neonderthaw:400); diff --git a/content/guestbook.md b/content/guestbook.md new file mode 100644 index 0000000..9c11eae --- /dev/null +++ b/content/guestbook.md @@ -0,0 +1,11 @@ +--- +title: "Guestbook" +author: Dan +type: guestbook +date: 2026-01-11 +comments: false +--- + +I'd love to hear from you if you've passed by my corner of the internet. + +If you want to say hi privately you can [email me](mailto:dan@ritual.sh) instead. diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index ea512a3..d7d98c5 100755 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -45,5 +45,11 @@ {{ $lavalampAdoptable := resources.Get "js/adoptables/lavalamp-adoptable.js" | resources.Minify | resources.Fingerprint }} {{ end }} + + + {{ if eq .Type "guestbook" }} + {{ $guestbook := resources.Get "js/guestbook.js" | resources.Minify | resources.Fingerprint }} + + {{ end }} diff --git a/layouts/guestbook/single.html b/layouts/guestbook/single.html new file mode 100644 index 0000000..68bd581 --- /dev/null +++ b/layouts/guestbook/single.html @@ -0,0 +1,112 @@ +{{ define "main" }} +
+
+ +
+
+
+
+ > guestbook --info
+ {{ .Content }}
+
+ _ +
+
+
+
+ {{ partial "elements/floppy.html" (dict "title" "Guestbook Entries" + "lines" (slice "" "" "DO NOT DELETE" "" "") "bgColor" "#1a4d8f" + "bgColorDark" "#0d2747") }} +
+
+ + +
+
+
> new-entry --interactive
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+
+ + +
+
+
+ > list-entries --all
+
+
+ +
+ Loading entries... + _ +
+ + + + + + +
+
+
+
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html index 8b05950..b5ac2f2 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -268,12 +268,22 @@ +
{{ partial "elements/crt-tv.html" . }}
diff --git a/layouts/partials/site-scripts.html b/layouts/partials/site-scripts.html index d940fd3..f61de47 100644 --- a/layouts/partials/site-scripts.html +++ b/layouts/partials/site-scripts.html @@ -7,7 +7,7 @@ {{ $filtered := slice }} {{ range $remaining }} {{ $path := .RelPermalink }} - {{ if and (not (strings.Contains $path "/terminal.js")) (not (strings.Contains $path "/init.js")) (not (strings.Contains $path "/button-generator.js")) (not (strings.Contains $path "/adoptables/")) }} + {{ if and (not (strings.Contains $path "/terminal.js")) (not (strings.Contains $path "/init.js")) (not (strings.Contains $path "/button-generator.js")) (not (strings.Contains $path "/adoptables/")) (not (strings.Contains $path "/guestbook.js")) }} {{ $filtered = $filtered | append . }} {{ end }} {{ end }}