From 87bc37e1248ac18be5fec8af6cc5e470b76845cb Mon Sep 17 00:00:00 2001
From: Alex Selimov
Date: Thu, 19 Mar 2026 23:31:30 -0400
Subject: [PATCH] Add anonymous upvote functionality
---
assets/herman.css | 45 ++++++++++++++++++++++++++++++++++++
layouts/_default/single.html | 9 ++++++++
static/upvote.js | 37 +++++++++++++++++++++++++++++
3 files changed, 91 insertions(+)
create mode 100644 static/upvote.js
diff --git a/assets/herman.css b/assets/herman.css
index f2573c3..8a5ca66 100644
--- a/assets/herman.css
+++ b/assets/herman.css
@@ -190,6 +190,51 @@ td {
transform: translateY(0%);
}
+.upvote {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: fit-content;
+ gap: 0.2rem;
+ margin-block: var(--spacing);
+}
+
+#upvote-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--color-light);
+ font-size: calc(var(--size) * 4);
+ line-height: 0.8;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0;
+ opacity: 0.4;
+ transition: opacity 0.15s, color 0.15s;
+}
+
+#upvote-btn:hover {
+ opacity: 0.8;
+}
+
+#upvote-btn[voted] {
+ color: var(--color-primary);
+ opacity: 1;
+}
+
+#upvote-chevron {
+ display: block;
+ line-height: 1;
+ margin-bottom: -0.3em;
+}
+
+#upvote-count {
+ font-size: calc(var(--size) * 1.6);
+ line-height: 1;
+}
+
figure {
margin-inline-start: 0em;
margin-inline-end: 0em;
diff --git a/layouts/_default/single.html b/layouts/_default/single.html
index 0c85516..7a5fd7b 100644
--- a/layouts/_default/single.html
+++ b/layouts/_default/single.html
@@ -18,6 +18,15 @@
#{{ lower .LinkTitle }}
{{ end }}
+{{ if .Site.Params.upvotes }}
+
+
+
+
+{{ end }}
{{ if not .Params.hideReply }}
{{ with .Site.Params.author.email }}
diff --git a/static/upvote.js b/static/upvote.js
new file mode 100644
index 0000000..5abf255
--- /dev/null
+++ b/static/upvote.js
@@ -0,0 +1,37 @@
+(async function () {
+ // Define the slug
+ const slug = location.pathname.replace(/^\/|\/$/g, "").replaceAll("/", "-");
+ const API = "http://localhost:3000";
+
+ const btn = document.getElementById("upvote-btn");
+ const count = document.getElementById("upvote-count");
+
+ // On load fetch curent vote count and whether the button should be clicked on or off
+ const res = await fetch(`${API}/posts/${slug}/votes`, {
+ credentials: "include",
+ });
+ const data = await res.json();
+
+ count.textContent = data.vote_count;
+ if (data.voted) btn.setAttribute("voted", true);
+
+ btn.addEventListener("click", async () => {
+ const alreadyVoted = btn.hasAttribute("voted");
+
+ const method = alreadyVoted ? "DELETE" : "POST";
+ const r = await fetch(`${API}/posts/${slug}/vote`, {
+ method,
+ credentials: "include",
+ });
+
+ if (r.ok) {
+ const updated = await fetch(`${API}/posts/${slug}/votes`, {
+ credentials: "include",
+ });
+ const d = await updated.json();
+ count.textContent = d.vote_count;
+ if (d.voted) btn.setAttribute("voted", false);
+ else btn.removeAttribute("voted");
+ }
+ });
+})();