broken but cool htmx! also improved templating

Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
ari melody 2024-03-22 07:30:22 +00:00
parent 5c59348362
commit c1ff03c4e5
18 changed files with 297 additions and 97 deletions

16
main.go
View file

@ -6,6 +6,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -34,6 +35,7 @@ var base_template = template.Must(template.ParseFiles(
"views/base.html", "views/base.html",
"views/header.html", "views/header.html",
"views/footer.html", "views/footer.html",
"views/prideflag.html",
)) ))
var htmx_template = template.Must(template.New("root").Parse(`<head>{{block "head" .}}{{end}}</head>{{block "content" .}}{{end}}`)) var htmx_template = template.Must(template.New("root").Parse(`<head>{{block "head" .}}{{end}}</head>{{block "content" .}}{{end}}`))
@ -59,11 +61,21 @@ func handle_request(writer http.ResponseWriter, req *http.Request) {
uri := req.URL.Path uri := req.URL.Path
start_time := time.Now() start_time := time.Now()
hx_boosted := len(req.Header["Hx-Boosted"]) > 0 && req.Header["Hx-Boosted"][0] == "true" hx_request := len(req.Header["Hx-Request"]) > 0 && req.Header["Hx-Request"][0] == "true"
// don't bother fulfilling requests to a page that's already loaded on the client!
if hx_request && len(req.Header["Referer"]) > 0 && len(req.Header["Hx-Current-Url"]) > 0 {
regex := regexp.MustCompile(`https?:\/\/[^\/]+`)
current_location := regex.ReplaceAllString(req.Header["Hx-Current-Url"][0], "")
if current_location == req.URL.Path {
writer.WriteHeader(204);
return
}
}
code := func(writer http.ResponseWriter, req *http.Request) int { code := func(writer http.ResponseWriter, req *http.Request) int {
var root *template.Template var root *template.Template
if hx_boosted { if hx_request {
root = template.Must(htmx_template.Clone()) root = template.Must(htmx_template.Clone())
} else { } else {
root = template.Must(base_template.Clone()) root = template.Must(base_template.Clone())

View file

@ -1,4 +1,3 @@
const header_home = document.getElementById("header-home");
const header_links = document.getElementById("header-links"); const header_links = document.getElementById("header-links");
const hamburger = document.getElementById("header-links-toggle"); const hamburger = document.getElementById("header-links-toggle");
@ -7,14 +6,9 @@ function toggle_header_links() {
} }
document.addEventListener("click", event => { document.addEventListener("click", event => {
if (!header_links.contains(event.target) && !hamburger.contains(event.target)) { if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) {
header_links.classList.remove("open"); header_links.classList.remove("open");
} }
}); });
hamburger.addEventListener("click", event => { toggle_header_links(); }); hamburger.addEventListener("click", event => { toggle_header_links(); });
header_home.addEventListener("click", event => {
event.preventDefault();
location.href = "/";
});

View file

@ -0,0 +1,141 @@
//==========================================================
// head-support.js
//
// An extension to htmx 1.0 to add head tag merging.
//==========================================================
(function(){
var api = null;
function log() {
//console.log(arguments);
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html");
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
// extract head tag
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
// if the head tag exists...
if (headTag) {
var added = []
var removed = []
var preserved = []
var nodesToAppend = []
htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector("head");
var currentHead = document.head;
if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}
// determine merge strategy
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt);
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
preserved.push(currentHeadElt);
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt);
nodesToAppend.push(currentHeadElt);
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt);
}
}
}
}
// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log("to append: ", nodesToAppend);
for (const newNode of nodesToAppend) {
log("adding: ", newNode);
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
log(newElt);
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt);
added.push(newElt);
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement);
}
}
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
}
}
}
htmx.defineExtension("head-support", {
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
htmx.on('htmx:afterSwap', function(evt){
var serverResponse = evt.detail.xhr.response;
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
}
})
htmx.on('htmx:historyRestore', function(evt){
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, "merge");
} else {
mergeHead(evt.detail.item.head, "merge");
}
}
})
htmx.on('htmx:historyItemCreated', function(evt){
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
})
}
});
})()

View file

@ -63,3 +63,17 @@ document.addEventListener("htmx:afterSwap", async event => {
} }
window.scrollY = 0; window.scrollY = 0;
}); });
const top_button = document.getElementById("backtotop");
window.onscroll = () => {
if (!top_button) return;
const btt_threshold = 100;
if (
document.body.scrollTop > btt_threshold ||
document.documentElement.scrollTop > btt_threshold
) {
top_button.classList.add("active");
} else {
top_button.classList.remove("active");
}
}

View file

@ -12,7 +12,9 @@ share_btn.onclick = (e) => {
share_btn.classList.add('active'); share_btn.classList.add('active');
} }
document.getElementById("go-back").addEventListener("click", () => { const go_back_btn = document.getElementById("go-back")
go_back_btn.innerText = "<";
go_back_btn.addEventListener("click", () => {
window.history.back(); window.history.back();
}); });

View file

@ -1,15 +1,5 @@
document.querySelectorAll("h2.question").forEach(element => { import "./main.js";
element.onclick = (e) => {
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}#${e.target.id}`;
window.location = url;
};
});
document.querySelectorAll("div.music").forEach(element => { document.querySelectorAll("h1.music-title").forEach(element => {
console.log(element); element.href = "";
element.addEventListener("click", (e) => {
const url = `${window.location.protocol}//${window.location.host}/music/${element.id}`;
window.location = url;
}); });
});

View file

@ -1,6 +1,7 @@
@import url("/style/colours.css"); @import url("/style/colours.css");
@import url("/style/header.css"); @import url("/style/header.css");
@import url("/style/footer.css"); @import url("/style/footer.css");
@import url("/style/prideflag.css");
@font-face { @font-face {
font-family: "Monaspace Argon"; font-family: "Monaspace Argon";
@ -42,6 +43,32 @@ span.newchar {
animation: newchar 0.25s; animation: newchar 0.25s;
} }
a#backtotop {
position: fixed;
left: 50%;
transform: translateX(-50%);
padding: .5em .8em;
display: block;
border-radius: 2px;
border: 1px solid transparent;
text-decoration: none;
opacity: .5;
transition-property: opacity, transform, border-color, background-color, color;
transition-duration: .2s;
}
a#backtotop.active {
top: 4rem;
}
a#backtotop:hover {
color: #eee;
border-color: #eee;
background-color: var(--links);
box-shadow: 0 0 1em var(--links);
opacity: 1;
}
@keyframes newchar { @keyframes newchar {
from { from {
background: #fff8; background: #fff8;

View file

@ -241,7 +241,7 @@ div#info p {
#title { #title {
margin: 0; margin: 0;
line-height: 1em; line-height: 1em;
font-size: 3em; font-size: 2.5em;
} }
#year { #year {
@ -571,6 +571,7 @@ footer a:hover {
} }
div#info > div { div#info > div {
min-width: auto;
min-height: auto; min-height: auto;
padding: 0; padding: 0;
margin: 0; margin: 0;

View file

@ -1,5 +1,12 @@
@import url("/style/index.css"); @import url("/style/index.css");
main {
width: min(calc(100% - 4rem), 720px);
min-height: calc(100vh - 10.3rem);
margin: 0 auto 2rem auto;
padding-top: 4rem;
}
div.music { div.music {
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 1.5rem; padding: 1.5rem;
@ -114,7 +121,7 @@ h2.question {
cursor: pointer; cursor: pointer;
} }
.collapse { div.answer {
margin: -1rem 0 1rem 0; margin: -1rem 0 1rem 0;
padding: .5em 1.5em; padding: .5em 1.5em;
border-radius: 4px; border-radius: 4px;

View file

@ -0,0 +1,25 @@
#prideflag svg {
position: fixed;
top: 0;
right: 0;
width: 120px;
transform-origin: 100% 0%;
transition: transform .5s cubic-bezier(.32,1.63,.41,1.01);
z-index: 8008135;
pointer-events: none;
}
#prideflag svg:hover {
transform: scale(110%);
}
#prideflag svg:active {
transform: scale(110%);
}
#prideflag svg * {
pointer-events: all;
}
@media screen and (max-width: 950px) {
#prideflag {
display: none;
}
}

View file

@ -7,28 +7,14 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{{block "head" .}} {{block "head" .}}{{end}}
<!-- <title>ari melody 💫</title> -->
<!-- <link rel="shortcut icon" href="img/favicon.png" type="image/x-icon"> -->
<!---->
<!-- <meta name="description" content="home to your local SPACEGIRL 💫"> -->
<!---->
<!-- <meta property="og:title" content="ari melody"> -->
<!-- <meta property="og:type" content="website"> -->
<!-- <meta property="og:url" content="www.arimelody.me"> -->
<!-- <meta property="og:image" content="https://www.arimelody.me/img/favicon.png"> -->
<!-- <meta property="og:site_name" content="ari melody"> -->
<!-- <meta property="og:description" content="home to your local SPACEGIRL 💫"> -->
<!---->
<!-- <link rel="stylesheet" href="style/main.css"> -->
<!---->
<!-- <script type="module" src="/script/main.js" defer></script> -->
{{end}}
<!-- <script type="application/javascript" src="/script/lib/htmx.min.js"></script> --> <meta name="htmx-config" content='{"htmx.config.scrollIntoViewOnBoost":false}'>
<script type="application/javascript" src="/script/lib/htmx.min.js"></script>
<script type="application/javascript" src="/script/lib/htmx-head-support.js"></script>
</head> </head>
<body> <body hx-ext="head-support">
{{template "header"}} {{template "header"}}
{{block "content" .}} {{block "content" .}}
@ -43,8 +29,8 @@
{{end}} {{end}}
{{template "footer"}} {{template "footer"}}
<div id="overlay"></div> <div id="overlay"></div>
{{template "prideflag"}}
</body> </body>
</html> </html>

View file

@ -1,6 +1,6 @@
{{define "footer"}} {{define "footer"}}
<footer hx-preserve="true"> <footer>
<div id="footer"> <div id="footer">
<small><em>*made with ♥ by ari, 2024*</em></small> <small><em>*made with ♥ by ari, 2024*</em></small>
</div> </div>

View file

@ -1,8 +1,8 @@
{{define "header"}} {{define "header"}}
<header hx-preserve="true"> <header>
<nav> <nav>
<div id="header-home"> <div id="header-home" hx-get="/" hx-on="click" hx-target="main" hx-swap="outerHTML show:window:top" hx-push-url="true">
<img src="/img/favicon.png" id="header-icon" width="100" height="100" alt=""> <img src="/img/favicon.png" id="header-icon" width="100" height="100" alt="">
<div id="header-text"> <div id="header-text">
<h1>ari melody</h1> <h1>ari melody</h1>
@ -16,7 +16,7 @@
<rect y="40" width="70" height="10" rx="5" fill="#eee" /> <rect y="40" width="70" height="10" rx="5" fill="#eee" />
</svg> </svg>
</a> </a>
<ul id="header-links"> <ul id="header-links" hx-boost="true" hx-target="main" hx-swap="outerHTML show:window:top">
<li> <li>
<a href="/">home</a> <a href="/">home</a>
</li> </li>

View file

@ -1,28 +1,9 @@
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{{block "head" .}}{{end}}
{{block "head" .}}
<title>ari melody 💫</title>
<link rel="shortcut icon" href="img/favicon.png" type="image/x-icon">
<meta name="description" content="home to your local SPACEGIRL 💫">
<meta property="og:title" content="ari melody">
<meta property="og:type" content="website">
<meta property="og:url" content="www.arimelody.me">
<meta property="og:image" content="https://www.arimelody.me/img/favicon.png">
<meta property="og:site_name" content="ari melody">
<meta property="og:description" content="home to your local SPACEGIRL 💫">
<link rel="stylesheet" href="style/main.css">
<script type="module" src="/script/main.js" defer></script>
{{end}}
<script type="application/javascript" src="/script/lib/htmx.min.js"></script> <script type="application/javascript" src="/script/lib/htmx.min.js"></script>
</head> <script type="application/javascript" src="/script/lib/htmx-head-support.js"></script>
{{block "content" .}}{{end}} {{block "content" .}}{{end}}

View file

@ -12,7 +12,6 @@
<meta property="og:description" content="home to your local SPACEGIRL 💫"> <meta property="og:description" content="home to your local SPACEGIRL 💫">
<link rel="stylesheet" href="/style/index.css"> <link rel="stylesheet" href="/style/index.css">
<script type="module" src="/script/main.js" defer></script> <script type="module" src="/script/main.js" defer></script>
<link rel="me" href="https://wetdry.world/@ari"> <link rel="me" href="https://wetdry.world/@ari">
{{end}} {{end}}

View file

@ -25,7 +25,6 @@
<meta name="twitter:image:alt" content="Cover art for &quot;{{.Title}}&quot;"> <meta name="twitter:image:alt" content="Cover art for &quot;{{.Title}}&quot;">
<script type="module" src="/script/music-gateway.js" defer></script> <script type="module" src="/script/music-gateway.js" defer></script>
<script type="application/javascript" src="/script/prideflag.js" defer></script>
<link rel="stylesheet" href="/style/music-gateway.css"> <link rel="stylesheet" href="/style/music-gateway.css">
{{end}} {{end}}
@ -33,7 +32,8 @@
<main> <main>
<div id="background" data-url="{{.ResolveArtwork}}"></div> <div id="background" data-url="{{.ResolveArtwork}}"></div>
<a id="go-back" title="back to arimelody.me" href="/music">&lt;</a> <a href="/music" id="go-back" title="back to arimelody.me">back to arimelody.me</a>
<br><br>
<div id="music-container"> <div id="music-container">
<div id="art-container"> <div id="art-container">

View file

@ -12,9 +12,7 @@
<meta property="og:description" content="music from your local SPACEGIRL 💫"> <meta property="og:description" content="music from your local SPACEGIRL 💫">
<link rel="stylesheet" href="/style/music.css"> <link rel="stylesheet" href="/style/music.css">
<script type="module" src="/script/music.js" defer></script>
<script type="module" src="/script/main.js" defer></script>
<script type="application/javascript" src="/script/music.js" defer></script>
{{end}} {{end}}
{{define "content"}} {{define "content"}}
@ -25,11 +23,11 @@
<div id="music-container"> <div id="music-container">
{{range $Album := .}} {{range $Album := .}}
<div class="music" id="{{$Album.Id}}"> <div class="music" id="{{$Album.Id}}" hx-get="/music/{{$Album.Id}}" hx-trigger="click" hx-target="main" hx-swap="outerHTML" hx-push-url="true">
<div class="music-artwork"> <div class="music-artwork">
<img src="{{$Album.ResolveArtwork}}" alt="{{$Album.Title}} artwork" width="128"> <img src="{{$Album.ResolveArtwork}}" alt="{{$Album.Title}} artwork" width="128">
</div> </div>
<div class="music-details"> <div class="music-details" hx-boost="true" hx-target="main" hx-swap="outerHTML">
<a href="/music/{{$Album.Id}}"><h1 class="music-title">{{$Album.Title}}</h1></a> <a href="/music/{{$Album.Id}}"><h1 class="music-title">{{$Album.Title}}</h1></a>
<h2 class="music-artist">{{$Album.PrintPrimaryArtists}}</h2> <h2 class="music-artist">{{$Album.PrintPrimaryArtists}}</h2>
<h3 class="music-type-{{.ResolveType}}">{{$Album.ResolveType}}</h3> <h3 class="music-type-{{.ResolveType}}">{{$Album.ResolveType}}</h3>
@ -45,12 +43,12 @@
{{end}} {{end}}
</div> </div>
<h2 id="usage" class="question"> <h2 id="usage" class="question" hx-get="/music#usage" hx-on="click" hx-swap="none" hx-push-url="true">
<a href="#usage"> <a href="#usage">
&gt; "can i use your music in my content?" &gt; "can i use your music in my content?"
</a> </a>
</h2> </h2>
<div class="collapse"> <div class="answer">
<p> <p>
<strong class="big">yes!</strong> well, in most cases... <strong class="big">yes!</strong> well, in most cases...
</p> </p>
@ -85,5 +83,7 @@
&gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a>
</p> </p>
</div> </div>
<a href="#" id="backtotop">back to top</a>
</main> </main>
{{end}} {{end}}

21
views/prideflag.html Normal file
View file

@ -0,0 +1,21 @@
{{define "prideflag"}}
<a href="https://github.com/mellodoot/prideflag" target="_blank" id="prideflag">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120" hx-preserve="true">
<path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/>
<path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/>
<path id="yellow" d="M120,40 V0 L60,60 L80,80 Z" style="fill:#e5fe02"/>
<path id="green" d="M120,0 H80 L40,40 L60,60 Z" style="fill:#09be01"/>
<path id="blue" d="M80,0 H40 L20,20 L40,40 Z" style="fill:#081a9a"/>
<path id="purple" d="M40,0 H0 L20,20 Z" style="fill:#76008a"/>
<rect id="black" x="60" width="60" height="60" style="fill:#010101"/>
<rect id="brown" x="70" width="50" height="50" style="fill:#603814"/>
<rect id="lightblue" x="80" width="40" height="40" style="fill:#73d6ed"/>
<rect id="pink" x="90" width="30" height="30" style="fill:#ffafc8"/>
<rect id="white" x="100" width="20" height="20" style="fill:#fff"/>
<rect id="intyellow" x="110" width="10" height="10" style="fill:#fed800"/>
<circle id="intpurple" cx="120" cy="0" r="5" stroke="#7601ad" stroke-width="2" fill="none"/>
</svg>
</a>
{{end}}