Improve clap counter & write up post

This commit is contained in:
JP Hastings-Spital 2024-04-07 10:46:19 +01:00
parent 38751361ff
commit 4501a1e41d
10 changed files with 163 additions and 41 deletions

View file

@ -1,3 +1,5 @@
/* Accent colour */
function updateAccentColor() {
const now = new Date()
let start = new Date(now)
@ -8,25 +10,49 @@ function updateAccentColor() {
setInterval(updateAccentColor, 15000)
updateAccentColor()
/* Clap counter */
const performClap = (e) => {
e.preventDefault();
const btn = e.currentTarget;
btn.disabled = true;
fetch(btn.parentElement.action, { method: 'POST', headers: new Headers({ 'Accept': 'application/json' }) })
.then(res => res.json())
fetch(btn.parentElement.action, {
method: 'POST',
headers: new Headers({ 'Accept': 'application/json' }),
signal: AbortSignal.timeout(2000),
}).then(res => res.json().then((data) => ([res, data])))
.then(([res, data]) => {
if (res.status !== 200) {
const errorDesc = data.error || JSON.stringify(data)
throw new Error(`Received HTTP ${res.status} from clap counter: ${errorDesc}`)
}
if (typeof data.claps !== 'number') {
throw new Error(`Clap count response isn't a number: ${JSON.stringify(data)}`)
}
return data;
})
.then(data => {
localStorage.setItem(clapKey(btn.parentElement.action), data.claps);
setClapCount(btn, data.claps);
btn.parentElement.classList.add('clapped')
btn.disabled = false;
})
.catch(e => console.error(`Failed to automate the clap: ${e}`))
.catch(e => {
if (e.name === 'AbortError') {
e = new Error("Fetch exceeded timeout")
}
console.error(`Failed to automate the clap: ${e.message}`)
alert("Sorry friend! It looks like my clap-o-meter has broken on us.\n\n"
+ "Instead, you could reach out to me via the sites listed on my homepage — to share appreciation for this post, or to tell me about this break!\n\n"
+ "(And if you're technically inclined,the console will list the error's details!)")
})
}
const clapKey = (action) => `clap:${(new URL(action)).pathname}`;
const setClapCount = (btn, clapCount) => {
const count = btn.querySelector('span');
const count = clapCountEl(btn);
if (count) {
count.textContent = Math.max(count.textContent, clapCount)
} else {
@ -36,11 +62,15 @@ const setClapCount = (btn, clapCount) => {
}
}
for (const btn of document.querySelectorAll('form.claps button')) {
const lastClappedTo = localStorage.getItem(clapKey(btn.parentElement.action));
if (lastClappedTo) {
btn.parentElement.classList.add('clapped')
setClapCount(btn, lastClappedTo)
const clapCountEl = (btn) => btn.querySelector('span')
document.addEventListener("DOMContentLoaded", () => {
for (const btn of document.querySelectorAll('form.claps button')) {
const lastClappedTo = localStorage.getItem(clapKey(btn.parentElement.action));
if (lastClappedTo) {
btn.parentElement.classList.add('clapped')
setClapCount(btn, lastClappedTo)
}
btn.addEventListener("click", performClap)
}
btn.addEventListener("click", performClap)
}
})

View file

@ -167,6 +167,10 @@ a.noaccent {
text-decoration: none;
}
.accent {
color: var(--accent);
}
img.profile {
float: left;
height: 4em;
@ -612,8 +616,7 @@ a[href^="#fn:"] {
}
form.claps {
float: right;
display: inline-block;
&.clapped {
color: var(--accent);
}
@ -631,6 +634,10 @@ form.claps {
}
}
.post-info form.claps {
float: right;
}
.post.poetry {
width: auto;
@ -866,6 +873,29 @@ mark {
}
}
.small-post-list {
position: relative;
text-align: left;
margin: 1em 1.5em;
list-style: none;
@media #{$media-size-phone} {
font-size: 1rem;
padding: 0;
width: fit-content;
max-width: calc(100% - 2em);
margin-inline: auto;
}
li > svg {
color: var(--accent);
position: absolute;
left: -1.5em;
margin-top: 0.25em;
text-align: right;
}
}
.homepage {
.content {
align-items: center;
@ -875,27 +905,8 @@ mark {
position: relative;
}
.recent-posts {
position: relative;
text-align: left;
.small-post-list {
padding: 0 50% 0 0;
margin: 1em 1.5em;
list-style: none;
@media #{$media-size-phone} {
font-size: 1rem;
padding: 0;
width: fit-content;
max-width: calc(100% - 2em);
margin-inline: auto;
}
li svg{
position: absolute;
left: -1.5em;
margin-top: 0.25em;
text-align: right;
}
& .summary {
position: absolute;
@ -908,7 +919,7 @@ mark {
color: $light-color-dim;
background-color: $light-background;
@media (prefers-color-scheme: dark) {
background-color: $dark-background;
color: $dark-color-dim;

View file

@ -64,7 +64,6 @@
}
svg {
color: var(--accent);
vertical-align:middle;
margin-left: 0.2em;
}
@ -72,6 +71,10 @@
p.reference-to, p.next-visit {
font-size: small;
font-style: italic;
svg {
color: var(--accent);
}
}
}

View file

@ -0,0 +1,34 @@
---
title: Easy appreciation
emoji: 👏
date: 2024-04-06T23:48:55+01:00
summary: I've built a "clap" feature into my blog so you can show appreciation anonymously and easily, if you want.
tags:
- community
- ValTown
- ProgressiveEnhancement
---
Here in my little career break I'm spending a lot of time thinking about _community_. This blog hasn't had much of a need for community (there's such an ecclectic mix of stuff here that people stumble upon it rather than frequent it) but as I've been building in tools like [webmentions](https://indieweb.org/Webmention), and pulling comments from other sites (like [on this post](/posts/chef-gpt/#interactions)), I've noticed the absence of an easy "I appreciate this" mechanism for passers-through.
Yesterday I built a clap button, the one you can see below this paragraph, and on every page at the bottom of the post. If you click it it lets me know you appreciate my post, and keeps track of how many times that's happened (you can press it more than once if you really like something!)
<figure style="text-align:center">{{< claps >}}<br/><small style="font-style: italic">Press this to show appreciation!</small></figure>
I decided to make this button totally anonymous; this makes it much easier for you to use, but the lack of tracking also means that—for me—it's a feel good vibe rather than data I can get analytical over. If you like the sound of it, you can try it out 😜
## Going a little deeper
For those more technically inclined, there are some minor smarts behind the scenes here. My site tries to be as decentralised as possible, but recording & retrieving claps needed something centralised (distributed systems are complex!) so I ended up using [val.town](https://val.town). These folks offer an _awesome_ platform for simple lambda-like web functions; I _love_ that they can be totally public & transparent — you can see (and copy!) the one that [drives my claps here](https://www.val.town/v/byjp/claps).
That 'val' accepts `POST` requests (incrementing the number of claps), `GET` requests (telling you how many claps a post has), and the special `GET /` which lists the claps for every post. That last endpoint is useful for the code behind my static site builder ([hugo](https://gohugo.io)), it [pulls that data](https://github.com/by-jp/www.byjp.me/blob/38751361ff6b8730428d8227f98189312576a709/layouts/partials/claps.html#L8-L17) as the site is being built (at least once a day) so the clap counts are accurate reasonably quickly without needing any javascript.
Speaking of not needing javascript, the button is also a `<form>` and the 'val' also handles `text/html` requests, so you can use this button even if you have javascript turned off. ([Progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement) for the win!)
…but of course javascript gives me more flexibility. I can record your clap without you leaving the page, and give the button an accented colour to give you some feedback. It also means that, when you click that button, I can recording your appreciation in your browser's local database. This means it'll remain accented for you ("you already showed your appreciation for this at least once") when you _next_ visit the same page, but while maintaining your anonymity and control. (If you ever 'clear browsing data' for this site, they'll stop being accented, but the clap still remains, anonymously, in my 'val').
## What's appreciated?
I doubt this little tool will get much use — my site is a lovely wild garden for me, rather than a valuable resource for others — but to end with a little fun, here's a list of the six most appreciated posts across my site right now.
{{< topclaps 6 >}}

View file

@ -1 +1 @@
{"interactions":[{"guid":"webmentions.io#1795504","emoji":"💬","url":"https://bsky.app/profile/davidcondemarin.bsky.social/post/3ko2zz752p72q","comment":"Ha! Just read this and I have to say Im not impressed at all. I like my cooking a bit too much to let ChatGPT getting anywhere near it! I dont think my commitment to science would have been as good as yours, I draw the line at throwing almonds and chickpeas on salmon ????","author":{"name":"David","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:44Z"},{"guid":"webmentions.io#1795367","emoji":"♥️","url":"https://bsky.app/profile/byjp.me/post/3knrhqsj6ac23#liked_by_did:plc:yqsc5vd6x67yofv5fmu5qkax","author":{"name":"","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:14Z"},{"guid":"webmentions.io#1794075","emoji":"⭐️","url":"https://hachyderm.io/@byjp/112102305952063134#favorited-by-109290756203712091","author":{"name":"Maria Neumayer","url":"https://androiddev.social/@marianeum"},"timestamp":"2024-03-17T02:11:11Z"}]}
{"interactions":[{"guid":"webmentions.io#1795504","emoji":"💬","url":"https://bsky.app/profile/davidcondemarin.bsky.social/post/3ko2zz752p72q","comment":"Ha! Just read this and I have to say Im not impressed at all. I like my cooking a bit too much to let ChatGPT getting anywhere near it! I dont think my commitment to science would have been as good as yours, I draw the line at throwing almonds and chickpeas on salmon 😂","author":{"name":"David","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:44Z"},{"guid":"webmentions.io#1795367","emoji":"♥️","url":"https://bsky.app/profile/byjp.me/post/3knrhqsj6ac23#liked_by_did:plc:yqsc5vd6x67yofv5fmu5qkax","author":{"name":"","url":"https://bsky.app/profile/davidcondemarin.bsky.social"},"timestamp":"2024-03-19T18:53:14Z"},{"guid":"webmentions.io#1794075","emoji":"⭐️","url":"https://hachyderm.io/@byjp/112102305952063134#favorited-by-109290756203712091","author":{"name":"Maria Neumayer","url":"https://androiddev.social/@marianeum"},"timestamp":"2024-03-17T02:11:11Z"}]}

View file

@ -1,7 +1,7 @@
{{- $relperma := .url -}}
{{- $style := .style | default "clap" -}}
<form class="claps" action="https://{{ site.Params.clapsHost }}{{ $relperma }}" method="post">
<button type="submit">
<button type="submit" title="Show appreciation for the post on this page">
{{- partial "svg.html" (dict "name" $style) -}}
{{- $url := printf "https://%s" site.Params.clapsHost -}}
{{- $cacheKey := print $url (now.Format "-2006-01-02") -}}

View file

@ -12,7 +12,7 @@
{{- $index := (split .Path "/") | after 1 -}}
{{- $interactions := index .Site.Data.interactions $index -}}
{{ with $interactions }}
<div class="interactions">
<div class="interactions" id="interactions">
{{ $most := 0 }}
{{ $emoji := "" }}
{{ range $thisEm, $count := .reactions }}

View file

@ -3,7 +3,7 @@
This site is a much loved wild garden; things aren't perfectly orgnaised, there are lots of nooks and crannies to explore. Follow your nose and see where you end up.
</p>
<ol class="recent-posts">
<ol class="small-post-list">
{{- $posts := where .Site.RegularPages "Section" "in" (slice "posts" "bookmarks" "poetry") -}}
{{- range $posts.ByPublishDate.Reverse | first 6 -}}
{{- $postType := partial "post-type.txt" . -}}
@ -17,8 +17,9 @@
<div>
<span class="always-accented">{{ $postType }}</span>
<date datetime="{{ dateFormat "2006-01-02T15:04:05-0700" .Date }}" title="Posted on {{ dateFormat "Monday Jan 2 2006 at 03:04 MST" .Date }}" data-pagefind-sort="date">
{{ partial "year-relative-date.html" .Date }}</date>
</div>
{{ partial "year-relative-date.html" .Date }}
</date>
</div>
{{- index (split .Summary "\n") 0 | htmlUnescape -}}
</div>

View file

@ -0,0 +1 @@
{{ partial "claps.html" (dict "url" .Page.RelPermalink) }}

View file

@ -0,0 +1,42 @@
{{- $count := .Get 0 | default 5 -}}
{{- $url := printf "https://%s" site.Params.clapsHost -}}
{{- $cacheKey := print $url (now.Format "-2006-01-02") -}}
{{- with resources.GetRemote $url (dict "key" $cacheKey) -}}
{{- with .Err -}}
{{- errorf "%s" . -}}
{{- else -}}
{{- $data := .Content | transform.Unmarshal -}}
{{- $claps := slice -}}
{{- range $path, $count := $data -}}
{{ $claps = $claps | append (dict "path" $path "count" $count) }}
{{- end -}}
<ol class="small-post-list">
{{- range (sort $claps "count" "desc" | first $count) -}}
{{- $count := .count -}}
{{- with site.GetPage .path -}}
{{- $postType := partial "post-type.txt" . -}}
{{- $typeSVG := index site.Params.defaultSVG ( partial "kebab.txt" $postType) -}}
<li>
{{ partial "svg.html" (dict "name" $typeSVG) }}
<a href="{{.RelPermalink}}">
{{- with .Title -}}{{ . }}{{ else }}{{ .Type | singularize | title }}{{ end -}}
</a>
<small style="opacity: 0.6">{{ partial "svg.html" (dict "name" "clap") }} {{ $count }}</small>
</li>
{{- end -}}
{{- end }}
</ol>
<script>
const list = document.currentScript.previousSibling;
for (const clap of list.querySelectorAll('li')) {
// This clapKey is authoritative in main.js
const clapKey = `clap:${clap.children[1].pathname}`;
if (localStorage.getItem(clapKey)) {
clap.children[2].classList.add('accent')
}
}
</script>
{{- end -}}
{{- else -}}
{{- errorf "Unable to get all clap counts" -}}
{{- end -}}