mirror of
https://github.com/Brandon-Rozek/website-theme.git
synced 2025-10-20 05:41:12 +00:00
Compare commits
18 commits
864dce1ff9
...
fd4db04d3a
Author | SHA1 | Date | |
---|---|---|---|
fd4db04d3a | |||
cc623bd90a | |||
0df997d594 | |||
487cd8cbd7 | |||
9f57be5587 | |||
51b780c021 | |||
0007b4241b | |||
831dd9e10a | |||
bcbfa17bb4 | |||
d507426ff0 | |||
3bf669ac6a | |||
be207e0df3 | |||
525b28e98b | |||
615e03bca3 | |||
60b013a30f | |||
d5bd28436a | |||
6c189eb8f2 | |||
bd7e11b421 |
28 changed files with 381 additions and 234 deletions
|
@ -404,6 +404,11 @@ footer a:link, footer a:visited, footer a:hover, footer a:active {
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
column-count: 2;
|
||||
column-gap: 2rem;
|
||||
}
|
||||
|
||||
/* END FOOTER */
|
||||
|
||||
/*
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
{{ partial "head.html" . }}
|
||||
<body>
|
||||
{{ block "main" . }}{{ end }}
|
||||
{{ partial "footer.html" . }}
|
||||
</body>
|
||||
{{ partial "footer.html" . }}
|
||||
</html>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"url": "{{ .Permalink }}",
|
||||
"title": {{ .Title | jsonify }},
|
||||
"authors": [
|
||||
{{with $.Site.Author.name }}
|
||||
{{with $.Site.Params.Author }}
|
||||
{ "name": "{{ . }}" }
|
||||
{{ end }}
|
||||
],
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": {
|
||||
{{with $.Site.Author.name }}"name": "{{ . }}",{{ end }}
|
||||
{{with $.Site.Params.Author }}"name": "{{ . }}",{{ end }}
|
||||
"url": "{{ .Site.BaseURL }}"
|
||||
},
|
||||
"id": "{{ $page.Permalink }}",
|
||||
"object": {
|
||||
"attributedTo": [
|
||||
{
|
||||
{{with $.Site.Author.name }}"name": "{{ . }}",{{ end }}
|
||||
{{with $.Site.Params.Author }}"name": "{{ . }}",{{ end }}
|
||||
"type": "Person",
|
||||
"url": "{{ .Site.BaseURL }}"
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<generator uri="https://gohugo.io">Hugo</generator>
|
||||
<author>
|
||||
<uri>{{ .Site.BaseURL }}</uri>
|
||||
{{ with .Site.Author.name }}<name>{{.}}</name>{{end}}
|
||||
{{ with .Site.Params.Author }}<name>{{.}}</name>{{end}}
|
||||
</author>
|
||||
<updated>{{ now.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>
|
||||
{{ with .OutputFormats.Get "Atom" }}
|
||||
|
@ -29,7 +29,7 @@
|
|||
<author>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<uri>{{ .Site.BaseURL }}</uri>
|
||||
{{ with .Site.Author.name }}<name>{{.}}</name>{{end}}
|
||||
{{ with .Site.Params.Author }}<name>{{.}}</name>{{end}}
|
||||
</author>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/article</activity:object-type>
|
||||
<title>{{ .Title }}</title>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"icon": "{{ .Site.BaseURL }}img/{{ .Site.Params.avatar }}",
|
||||
"language": "en-US",
|
||||
"authors": [
|
||||
{{with $.Site.Author.name }}
|
||||
{{with $.Site.Params.Author }}
|
||||
{ "name": "{{ . }}" }
|
||||
{{ end }}
|
||||
],
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
</image>
|
||||
<description>{{ .Description }}</description>
|
||||
<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
|
||||
<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
|
||||
<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
|
||||
<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
|
||||
<language>{{.}}</language>{{end}}{{ with .Site.Params.email }}
|
||||
<managingEditor>{{.}}{{ with $.Site.Params.Author }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Params.email }}
|
||||
<webMaster>{{.}}{{ with $.Site.Params.Author }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
|
||||
<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
|
||||
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
|
||||
{{ with .OutputFormats.Get "RSS" }}
|
||||
|
@ -34,7 +34,7 @@
|
|||
<title>{{ .Title }}</title>
|
||||
<link>{{ .Permalink }}</link>
|
||||
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
|
||||
{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
|
||||
{{ with .Site.Params.email }}<author>{{.}}{{ with $.Site.Params.Author }} ({{.}}){{end}}</author>{{end}}
|
||||
<guid>{{ .Permalink }}</guid>
|
||||
{{ printf `<description><![CDATA[%s]]></description>` .Summary | safeHTML }}
|
||||
{{ printf `<content:encoded><![CDATA[%s]]></content:encoded>` .Content | safeHTML }}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{{ range last 1 $posts }}
|
||||
{{ $tags := .Site.Taxonomies.tags }}
|
||||
<div class='bloglist-teaser block-center'>
|
||||
Hello! This page contains <strong>{{ $postCount | lang.NumFmt 0 }} posts</strong>
|
||||
Hello! This page contains <strong>{{ $postCount | lang.FormatNumber 0 }} posts</strong>
|
||||
written in the past <strong>{{ div (sub now.Unix .Date.Unix) 31536000 }} years</strong>
|
||||
spanning
|
||||
{{ len $tags }} topics within the dropdown above.
|
||||
|
|
|
@ -36,9 +36,7 @@
|
|||
"@context": "https://schema.org",
|
||||
"@type": "NewsArticle",
|
||||
"headline": "{{ .Title }}",
|
||||
"image": [
|
||||
"{{ .Site.BaseURL }}img/{{ .Site.Params.avatar }}"
|
||||
],
|
||||
"image": [],
|
||||
"datePublished": "{{ .Date.Format "2006-01-02" }}",
|
||||
"author": [{
|
||||
"@type": "Person",
|
||||
|
|
|
@ -25,13 +25,21 @@
|
|||
</div>
|
||||
</article>
|
||||
|
||||
<div class="noprint" style="margin-bottom: 2rem; display: flex; justify-content: center; flex-wrap: wrap; gap: 20px;">
|
||||
<a href='mailto:{{ .Site.Params.email }}?subject=Re: "{{ .Title }}"' class="reply-button">Reply via Email</a>
|
||||
<a href="/support" class="reply-button" style="margin-left:2rem;"><i class="fas fa-coffee" style="margin-right: 0.5rem;"></i>Buy me a Coffee</a>
|
||||
<hr style="margin-bottom: 3rem;"/>
|
||||
|
||||
<div class="noprint" style="text-align: left;">
|
||||
<p>Have any questions or want to chat: <a href='mailto:{{ .Site.Params.email }}?subject=Re: "{{ .Title }}"' class="reply-button"><i class="fa fa-paper-plane" style="margin-right: 0.5rem;"></i>Reply via Email</a></p>
|
||||
<p>Enjoyed this post?</p>
|
||||
<ul style="list-style: none;">
|
||||
<li>Support: <a href="/support"><i class="fas fa-coffee" style="margin-right: 0.5rem;"></i>Buy me a Coffee</a></li>
|
||||
<li>{{ partial "sharepost.html" . }}</li>
|
||||
</ul>
|
||||
{{ partial "highfive.html" . }}
|
||||
{{ partial "webmentions.html" . }}
|
||||
</div>
|
||||
|
||||
{{ partial "sharepost.html" . }}
|
||||
{{ partial "webmentions.html" . }}
|
||||
|
||||
|
||||
</main>
|
||||
<!-- Metadata -->
|
||||
<script type="application/ld+json">
|
||||
|
@ -39,9 +47,7 @@
|
|||
"@context": "https://schema.org",
|
||||
"@type": "NewsArticle",
|
||||
"headline": "{{ .Title }}",
|
||||
"image": [
|
||||
"{{ .Site.BaseURL }}img/{{ .Site.Params.avatar }}"
|
||||
],
|
||||
"image": [],
|
||||
"datePublished": "{{ .Date.Format "2006-01-02" }}",
|
||||
"author": [{
|
||||
"@type": "Person",
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
{{ define "main"}}
|
||||
{{ partial "header.html" . }}
|
||||
<main>
|
||||
<style>main { text-align: left; }</style>
|
||||
{{ $posts := where site.RegularPages "Type" "observations" }}
|
||||
{{ $postCount := len $posts }}
|
||||
{{ range last 1 $posts }}
|
||||
{{ $scratch := newScratch }}
|
||||
{{ $scratch.Set "researchGradeCount" 0 }}
|
||||
{{ range .Pages }}
|
||||
{{ if (eq .quality_grade "research")}}
|
||||
{{ $scratch.Add "researchGradeCount" 1 }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ $tags := .Site.Taxonomies.tags }}
|
||||
<div style="text-align: center; margin-bottom: 2rem;" class='bloglist-teaser block-center'>
|
||||
Hello! This page contains {{ $postCount | lang.NumFmt 0 }} observations,
|
||||
{{ len $tags }} of which are <strong><a href="https://www.inaturalist.org/posts/39072-research-grade">research grade</a></strong>.
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ .Content }}
|
||||
{{ $listPageDateFormat := .Site.Params.listPageDateFormat | default "January, 2006"}}
|
||||
{{ range $index, $value := .Pages.GroupByDate $listPageDateFormat }}
|
||||
{{ range .Pages }}
|
||||
{{ $dataJ := dict "created_at" .Date
|
||||
"url" .Params.syndication
|
||||
"quality_grade" .Params.quality_grade
|
||||
"place_guess" .Params.place_guess
|
||||
"photos" .Params.photos
|
||||
"taxon" .Params.taxon
|
||||
}}
|
||||
|
||||
{{ .Scratch.Set "obs" $dataJ }}
|
||||
{{ .Scratch.Set "obs_url" .Permalink }}
|
||||
{{ partial "external/inat.html" . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
|
@ -1,39 +0,0 @@
|
|||
{{ define "main"}}
|
||||
{{ .Scratch.Set "customTitleHeaderSet" true }}
|
||||
{{ .Scratch.Set "customTitleHeader" "Observations" }}
|
||||
{{ .Scratch.Set "customTitleHeaderLink" "/observations/" }}
|
||||
{{ partial "header.html" . }}
|
||||
<main>
|
||||
{{ with .Params }}
|
||||
|
||||
<article class="h-entry">
|
||||
{{ $name := "" }}
|
||||
{{ if (ne .taxon.common_name "") }}
|
||||
{{ $name = .taxon.common_name }}
|
||||
{{ else }}
|
||||
{{ $name = .taxon.name }}
|
||||
{{ end }}
|
||||
{{ $obsURL := .url }}
|
||||
<h1 class='title p-name'>{{ title $name }}</h1>
|
||||
{{ if (eq .quality_grade "research")}}
|
||||
<span>Research Grade </span><i class="fa fa-solid fa-clipboard-check"></i></i>
|
||||
<br/>
|
||||
{{ end }}
|
||||
<p class="date">Observed on <time class="dt-published" datetime='{{ .date }}'>{{ dateFormat "January 2, 2006 15:04" (time .date) }}</time></p>
|
||||
<p>Location: {{ .place_guess }}</p>
|
||||
<span>Also on: <a class="u-syndication" href="{{ .syndication }}">iNaturalist</a></span>
|
||||
|
||||
<div class="e-content">
|
||||
{{ if gt (len .photos) 0 }}
|
||||
{{ range .photos}}
|
||||
{{ $medium_url := replace .url "square" "medium"}}
|
||||
{{ $original_url := replace .url "square" "original"}}
|
||||
<a href="{{ $original_url }}"><img src="{{ $medium_url }}" alt="Photo of {{ .species_guess }}"/></a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<a class="u-url" style="display: none">{{ .Permalink }}</a>
|
||||
</article>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
2
layouts/partials/external/inat.html
vendored
2
layouts/partials/external/inat.html
vendored
|
@ -6,7 +6,7 @@
|
|||
<article class="toot h-entry">
|
||||
{{ $name := "" }}
|
||||
{{ if (ne .taxon.common_name "") }}
|
||||
{{ $name = .taxon.common_name }}
|
||||
{{ $name = .taxon.preferred_common_name }}
|
||||
{{ else }}
|
||||
{{ $name = .taxon.name }}
|
||||
{{ end }}
|
||||
|
|
31
layouts/partials/external/twitter_cards.html
vendored
31
layouts/partials/external/twitter_cards.html
vendored
|
@ -1,31 +0,0 @@
|
|||
<!-- https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/twitter_cards.html -->
|
||||
{{- with $.Params.images -}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="{{ index . 0 | absURL }}">
|
||||
{{ else -}}
|
||||
{{- $images := $.Resources.ByType "image" -}}
|
||||
{{- $featured := $images.GetMatch "*feature*" -}}
|
||||
{{- if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end -}}
|
||||
{{- with $featured -}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="{{ $featured.Permalink }}">
|
||||
{{- else -}}
|
||||
{{- with $.Site.Params.images -}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="{{ index . 0 | absURL }}">
|
||||
{{ else -}}
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:image" content="{{ .Site.BaseURL }}img/{{ .Site.Params.avatar }}">
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
<meta name="twitter:title" content="{{ .Title }}">
|
||||
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end -}}">
|
||||
{{ with .Site.Social.twitter -}}
|
||||
<meta name="twitter:site" content="@{{ . }}">
|
||||
{{ end -}}
|
||||
{{ range .Site.Authors }}
|
||||
{{ with .twitter -}}
|
||||
<meta name="twitter:creator" content="@{{ . }}">
|
||||
{{ end -}}
|
||||
{{ end -}}
|
|
@ -16,15 +16,6 @@
|
|||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav>
|
||||
<p>Social</p>
|
||||
<ul>
|
||||
{{ range .Site.Menus.profile }}
|
||||
<li><a href="{{ .URL }}" rel="me" aria-label="{{ .Identifier }}">{{ .Name }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav>
|
||||
<p>More Pages</p>
|
||||
<ul>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div class="footer-navs block-center">
|
||||
<nav>
|
||||
<p>Content Pages</p>
|
||||
<ul id="footer-nav-sections">
|
||||
<ul class="footer-nav">
|
||||
{{ range .Site.Sections }}
|
||||
{{ if not .Params.hidden }}
|
||||
<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
|
||||
|
@ -16,18 +16,9 @@
|
|||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav>
|
||||
<p>Social</p>
|
||||
<ul>
|
||||
{{ range .Site.Menus.profile }}
|
||||
<li><a href="{{ .URL }}" rel="me" aria-label="{{ .Identifier }}">{{ .Name }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<nav>
|
||||
<p>More Pages</p>
|
||||
<ul>
|
||||
<ul class="footer-nav">
|
||||
{{ $sectionPages := where .Site.Pages "Section" "" }}
|
||||
{{ range $sectionPages }}
|
||||
{{ if and (not .Params.hidden) (ne .Permalink .Site.BaseURL)}}
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
{{/* NOTE: These Hugo Internal Templates can be found starting at https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates */}}
|
||||
{{- template "_internal/opengraph.html" . -}}
|
||||
{{- template "_internal/schema.html" . -}}
|
||||
{{- partial "external/twitter_cards.html" . -}}
|
||||
|
||||
{{ partial "citation.html" . }}
|
||||
|
||||
|
|
|
@ -42,6 +42,23 @@
|
|||
{{ end -}}
|
||||
|
||||
<!-- CSS -->
|
||||
<!-- Immediately show background/text color to prevent flashing white -->
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #faf9f6;
|
||||
--text-color: #020101;
|
||||
}
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #0e0e0e;
|
||||
--text-color: #8f8f8f;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{- $style := resources.Get "css/style.css" -}}
|
||||
{{- $markdown := resources.Get "css/markdown.css" -}}
|
||||
{{- $fontawesome := resources.Get "css/fontawesome.css" -}}
|
||||
|
@ -62,8 +79,6 @@
|
|||
{{/* NOTE: These Hugo Internal Templates can be found starting at https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates */}}
|
||||
{{- template "_internal/opengraph.html" . -}}
|
||||
{{- template "_internal/schema.html" . -}}
|
||||
<!-- {{- template "_internal/twitter_cards.html" . -}} -->
|
||||
{{- partial "external/twitter_cards.html" . -}}
|
||||
|
||||
{{ partial "citation.html" . }}
|
||||
|
||||
|
|
238
layouts/partials/highfive.html
Normal file
238
layouts/partials/highfive.html
Normal file
|
@ -0,0 +1,238 @@
|
|||
<style>
|
||||
.highfive-container {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.highfive-message {
|
||||
margin-left: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.highfive-message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.highfive-message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.highfive-btn {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.highfive-btn.no-js {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{ $apiUrl := printf "https://api.brandonrozek.com/highfive%s" .RelPermalink }}
|
||||
{{ $initialCount := 0 }}
|
||||
{{ with try (resources.GetRemote $apiUrl) }}
|
||||
{{ with .Err }}
|
||||
{{ warnf "Failed to fetch highfive data from %s" $apiUrl }}
|
||||
{{ else }}
|
||||
{{ $resource := .Value }}
|
||||
{{ $highfiveData := $resource | transform.Unmarshal }}
|
||||
{{ if $highfiveData.count }}
|
||||
{{ $initialCount = $highfiveData.count }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<div class="highfive-container">
|
||||
<p>
|
||||
Give me a high five (it's free):
|
||||
<a href="#" id="highfive-btn" class="reply-button highfive-btn no-js" role="button" aria-label="Give high five">
|
||||
<i class="fa fa-hand-paper" style="margin-right: 0.5rem;" aria-hidden="true"></i>(<span id="highfive-count" aria-live="polite">{{ $initialCount }}</span>)
|
||||
</a>
|
||||
<span id="highfive-message" class="highfive-message" role="status" aria-live="polite"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
API_BASE: 'https://api.brandonrozek.com/highfive',
|
||||
TIMEOUTS: {
|
||||
SUCCESS_MESSAGE: 3000,
|
||||
ERROR_MESSAGE: 3000
|
||||
},
|
||||
SELECTORS: {
|
||||
BUTTON: '#highfive-btn',
|
||||
COUNT: '#highfive-count',
|
||||
MESSAGE: '#highfive-message'
|
||||
}
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
const elements = {};
|
||||
|
||||
// State
|
||||
let isLoading = false;
|
||||
let currentCount = {{ $initialCount }};
|
||||
|
||||
/**
|
||||
* Initialize the high five functionality
|
||||
*/
|
||||
function init() {
|
||||
// Cache DOM elements
|
||||
elements.button = document.querySelector(CONFIG.SELECTORS.BUTTON);
|
||||
elements.count = document.querySelector(CONFIG.SELECTORS.COUNT);
|
||||
elements.message = document.querySelector(CONFIG.SELECTORS.MESSAGE);
|
||||
|
||||
if (!elements.button || !elements.count || !elements.message) {
|
||||
console.error('High five: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable button now that JavaScript is available
|
||||
elements.button.classList.remove('no-js');
|
||||
elements.button.addEventListener('click', handleHighFiveClick);
|
||||
|
||||
// Load initial count
|
||||
loadHighFiveCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request with error handling
|
||||
*/
|
||||
async function makeApiRequest(method = 'GET') {
|
||||
const response = await fetch(`${CONFIG.API_BASE}${window.location.pathname}`, {
|
||||
method,
|
||||
mode: 'cors',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
// Handle 409 Conflict (already high-fived) as a special case
|
||||
if (response.status === 409) {
|
||||
const data = await response.json();
|
||||
return { ...data, alreadyHighFived: true };
|
||||
}
|
||||
|
||||
if (response.status === 400) {
|
||||
return {count: 0}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current high five count
|
||||
*/
|
||||
async function loadHighFiveCount() {
|
||||
try {
|
||||
const data = await makeApiRequest('GET');
|
||||
currentCount = data.count || currentCount;
|
||||
updateButtonContent('hand-paper');
|
||||
} catch (error) {
|
||||
console.error('Failed to load high five count:', error);
|
||||
updateButtonContent('hand-paper');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle high five button click
|
||||
*/
|
||||
async function handleHighFiveClick(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
setButtonState('loading');
|
||||
|
||||
try {
|
||||
const data = await makeApiRequest('POST');
|
||||
const newCount = data.count || currentCount;
|
||||
currentCount = newCount;
|
||||
|
||||
// Check if user already gave a high five (HTTP 409 response)
|
||||
if (data.alreadyHighFived) {
|
||||
updateButtonContent('check');
|
||||
showMessage('👋 Already high-fived!', 'success');
|
||||
setButtonState('disabled');
|
||||
} else {
|
||||
updateButtonContent('check');
|
||||
showMessage('🎉 Thanks!', 'success');
|
||||
setButtonState('disabled');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to send high five:', error);
|
||||
|
||||
showMessage('❌ Failed to send', 'error', CONFIG.TIMEOUTS.ERROR_MESSAGE);
|
||||
updateButtonContent('hand-paper');
|
||||
setButtonState('disabled');
|
||||
|
||||
setTimeout(() => {
|
||||
setButtonState('enabled');
|
||||
}, CONFIG.TIMEOUTS.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button content with icon and count
|
||||
*/
|
||||
function updateButtonContent(icon) {
|
||||
elements.count.textContent = currentCount;
|
||||
const iconClass = icon === 'loading' ? 'fa-spinner fa-spin' : `fa-${icon}`;
|
||||
const text = icon === 'loading' ? 'Sending...' : `(${currentCount})`;
|
||||
|
||||
elements.button.innerHTML = `<i class="fa ${iconClass}" style="margin-right: 0.5rem;" aria-hidden="true"></i>${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set button state (enabled, disabled, loading)
|
||||
*/
|
||||
function setButtonState(state) {
|
||||
isLoading = state === 'loading';
|
||||
|
||||
const isInteractive = state === 'enabled';
|
||||
elements.button.style.pointerEvents = isInteractive ? 'auto' : 'none';
|
||||
elements.button.style.opacity = isInteractive ? '1' : '0.6';
|
||||
|
||||
if (state === 'loading') {
|
||||
updateButtonContent('loading');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show message with optional auto-hide
|
||||
*/
|
||||
function showMessage(text, type, timeout) {
|
||||
elements.message.textContent = text;
|
||||
elements.message.className = `highfive-message ${type}`;
|
||||
elements.message.style.display = 'inline';
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
elements.message.style.display = 'none';
|
||||
elements.message.className = 'highfive-message';
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
</script>
|
|
@ -1,5 +1,4 @@
|
|||
<div class="noprint" style="margin-bottom: 1rem;">Was this useful? Feel free to share:
|
||||
Share:
|
||||
<a href="https://news.ycombinator.com/submitlink?u={{ .Permalink }}&t={{ .Title }}">Hacker News</a>
|
||||
<a href="https://www.reddit.com/%73%75%62%6d%69%74?url={{ .Permalink }}&title={{ .Title }}">Reddit</a>
|
||||
<a href="https://twitter.com/intent/tweet?url={{ .Permalink }}&text={{ .Title }}">Twitter</a>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<input type="hidden" name="target" value="{{ .Permalink }}">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</main>
|
||||
{{ $webmentionjs := resources.Get "js/webmention.min.js" }}
|
||||
{{ $aliasesCombined := delimit (apply .Aliases "absURL" ".") "|" }}
|
||||
<script src="{{ $webmentionjs.Permalink }}" {{ printf "add-urls=%s" $aliasesCombined | safeHTMLAttr }} async></script>
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
{{ $url := urls.Parse (.Get 0) }}
|
||||
{{ $status_id := index (last 1 (split $url.Path "/")) 0 }}
|
||||
{{ $api_url := printf "%s://%s/api/v1/statuses/%s" $url.Scheme $url.Host $status_id }}
|
||||
{{ $dataJ := getJSON $api_url }}
|
||||
{{ $dataJ := dict }}
|
||||
{{ $url := $api_url }}
|
||||
{{ with try (resources.GetRemote $url) }}
|
||||
{{ with .Err }}
|
||||
{{ errorf "Unable to get remote resource %s: %s" $url . }}
|
||||
{{ else with .Value }}
|
||||
{{ $dataJ = . | transform.Unmarshal }}
|
||||
{{ else }}
|
||||
{{ errorf "Unable to get remote resource %s" $url }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ .Scratch.Set "toot" $dataJ }}
|
||||
{{ partial "external/toot.html" . }}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
{{ $toot_reference := .Get 0 }}
|
||||
{{ $url := printf "static/data/toots/%s.json" $toot_reference }}
|
||||
{{ $dataJ := getJSON $url }}
|
||||
|
||||
{{ $url := printf "data/toots/%s.json" $toot_reference }}
|
||||
{{ $dataJ := dict }}
|
||||
{{ with try (resources.Get $url) }}
|
||||
{{ with .Err }}
|
||||
{{ errorf "Unable to get resource %s: %s" $url . }}
|
||||
{{ else with .Value }}
|
||||
{{ $dataJ = . | transform.Unmarshal }}
|
||||
{{ else }}
|
||||
{{ errorf "Unable to get resource %s" $url }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
w
|
||||
{{ .Scratch.Set "toot" $dataJ }}
|
||||
{{ partial "external/toot.html" . }}
|
||||
|
|
|
@ -6,26 +6,34 @@
|
|||
Encrypted Result:
|
||||
<pre id="pgpresult" class="pgpform"></pre>
|
||||
<script>
|
||||
function encrypt() {
|
||||
let textarea = document.querySelector("#pgpcleartext");
|
||||
async function encrypt() {
|
||||
let resultarea = document.querySelector('#pgpresult');
|
||||
let pubKeyURL = "{{ .Get 0 }}";
|
||||
resultarea.textContent = "";
|
||||
fetch(pubKeyURL).then(function(response) {
|
||||
return response.text().then(function(text) {
|
||||
const pubKey = openpgp.readKey({ armoredKey: text });
|
||||
const message = openpgp.createMessage({ text: textarea.value })
|
||||
return Promise.all([message, pubKey]).then(function(mp) {
|
||||
const encryptionParameters = {
|
||||
message: mp[0],
|
||||
encryptionKeys: mp[1]
|
||||
};
|
||||
return openpgp.encrypt(encryptionParameters).then(function(encryptedMessage) {
|
||||
resultarea.textContent = encryptedMessage;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let textarea = document.querySelector("#pgpcleartext");
|
||||
if (textarea.value.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pubKeyURL = "{{ .Get 0 }}";
|
||||
let pubKey;
|
||||
|
||||
try {
|
||||
const response = await fetch(pubKeyURL);
|
||||
const text = await response.text();
|
||||
pubKey = await openpgp.readKey({ armoredKey: text });
|
||||
} catch {
|
||||
resultarea.textContent = "Error: Unable to obtain key";
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await openpgp.createMessage({ text: textarea.value });
|
||||
const encryptedMessage = await openpgp.encrypt({message: message, encryptionKeys: pubKey});
|
||||
resultarea.textContent = encryptedMessage;
|
||||
} catch {
|
||||
resultarea.textContent = "Error: Unable to encrypt message"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function genEmail() {
|
||||
|
|
39
layouts/shortcodes/pgpverify.html
Normal file
39
layouts/shortcodes/pgpverify.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{{- $openPGP := resources.Get "js/openpgp.min.js" -}}
|
||||
<script src="{{ $openPGP.Permalink }}"></script>
|
||||
<textarea id="pgpsignedtext" class="pgpform" style="width: 100%; min-height: 10rem;"></textarea>
|
||||
<button class="pgpverifybutton" onclick="verify()">Verify</button>
|
||||
<br/>
|
||||
Verification Result:
|
||||
<pre id="pgpverifyresult" class="pgpform"></pre>
|
||||
<script>
|
||||
async function verify() {
|
||||
let resultarea = document.querySelector('#pgpverifyresult');
|
||||
resultarea.textContent = "";
|
||||
|
||||
let textarea = document.querySelector("#pgpsignedtext");
|
||||
if (textarea.value.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pubKeyURL = "{{ .Get 0 }}";
|
||||
let pubKey;
|
||||
|
||||
try {
|
||||
const response = await fetch(pubKeyURL);
|
||||
const text = await response.text();
|
||||
pubKey = await openpgp.readKey({ armoredKey: text });
|
||||
} catch {
|
||||
resultarea.textContent = "Error: Unable to obtain key";
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await openpgp.readCleartextMessage({ cleartextMessage: textarea.value });
|
||||
const verifyResult = await openpgp.verify({message: message, verificationKeys: pubKey});
|
||||
const validResult = await verifyResult.signatures[0].verified;
|
||||
resultarea.textContent = (validResult)? "Valid": "Invalid";
|
||||
} catch {
|
||||
resultarea.textContent = "Invalid";
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
|
@ -2,12 +2,12 @@
|
|||
{{ partial "header.html" . }}
|
||||
<main>
|
||||
<div class='bloglist-teaser block-center' style="margin-top: 2rem;">
|
||||
Here you'll find <strong>{{ len .Pages | lang.NumFmt 0 }} posts</strong>
|
||||
Here you'll find <strong>{{ len .Pages | lang.FormatNumber 0 }} posts</strong>
|
||||
about <u>{{ .Title }}</u> </strong>.
|
||||
</div>
|
||||
<!-- contents -->
|
||||
<div class="bloglist">
|
||||
{{ $listPageDateFormat := .Site.Params.listPageDateFormat | default "January, 2006"}}
|
||||
{{ $listPageDateFormat := .Site.Params.listPageDateFormat | default "2006"}}
|
||||
{{ range $index, $value := .Pages.GroupByDate $listPageDateFormat }}
|
||||
{{ if (ne $index 0) }}
|
||||
<br/>
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
{{ define "main"}}
|
||||
{{ partial "header.html" . }}
|
||||
<main>
|
||||
<style>main { text-align: left; }</style>
|
||||
{{ partial "search.html" . }}
|
||||
{{ .Content }}
|
||||
{{ $listPageDateFormat := .Site.Params.listPageDateFormat | default "January, 2006"}}
|
||||
{{ range $index, $value := .Pages.GroupByDate $listPageDateFormat }}
|
||||
{{ range .Pages }}
|
||||
{{ $dataJ := dict "account" .Params.account
|
||||
"account.display_name" .Params.account.display_name
|
||||
"created_at" .Date
|
||||
"content" .Content
|
||||
"media_attachments" .Params.media_attachments
|
||||
"Permalink" .Permalink
|
||||
"tags" .Params.tags
|
||||
"url" .Params.syndication
|
||||
"replies_count" .Params.replies_count
|
||||
"reblogs_count" .Params.reblogs_count
|
||||
"favourites_count" .Params.favourites_count
|
||||
}}
|
||||
|
||||
{{ .Scratch.Set "toot" $dataJ }}
|
||||
{{ partial "external/toot.html" . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
|
@ -1,23 +0,0 @@
|
|||
{{ define "main"}}
|
||||
{{ .Scratch.Set "customTitleHeaderSet" true }}
|
||||
{{ .Scratch.Set "customTitleHeader" "Toots" }}
|
||||
{{ .Scratch.Set "customTitleHeaderLink" "/toots" }}
|
||||
{{ partial "header.html" . }}
|
||||
<main>
|
||||
{{ $dataJ := dict "account" .Params.account
|
||||
"account.display_name" .Params.account.display_name
|
||||
"created_at" .Date
|
||||
"content" .Content
|
||||
"media_attachments" .Params.media_attachments
|
||||
"Permalink" .Permalink
|
||||
"tags" .Params.tags
|
||||
"url" .Params.syndication
|
||||
"replies_count" .Params.replies_count
|
||||
"reblogs_count" .Params.reblogs_count
|
||||
"favourites_count" .Params.favourites_count
|
||||
}}
|
||||
|
||||
{{ .Scratch.Set "toot" $dataJ }}
|
||||
{{ partial "external/toot.html" . }}
|
||||
</main>
|
||||
{{ end }}
|
Loading…
Add table
Add a link
Reference in a new issue