home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

broken but cool htmx! also improved templating

Signed-off-by: ari melody <ari@arimelody.me>

+297 -97
+26 -14
main.go
··· 1 1 package main 2 2 3 3 import ( 4 - "fmt" 5 - "html/template" 6 - "log" 7 - "net/http" 8 - "os" 9 - "strconv" 10 - "strings" 11 - "time" 4 + "fmt" 5 + "html/template" 6 + "log" 7 + "net/http" 8 + "os" 9 + "regexp" 10 + "strconv" 11 + "strings" 12 + "time" 12 13 13 - "arimelody.me/arimelody.me/api/v1/music" 14 + "arimelody.me/arimelody.me/api/v1/music" 14 15 15 - "github.com/gomarkdown/markdown" 16 - "github.com/gomarkdown/markdown/html" 17 - "github.com/gomarkdown/markdown/parser" 16 + "github.com/gomarkdown/markdown" 17 + "github.com/gomarkdown/markdown/html" 18 + "github.com/gomarkdown/markdown/parser" 18 19 ) 19 20 20 21 const PORT int = 8080 ··· 34 35 "views/base.html", 35 36 "views/header.html", 36 37 "views/footer.html", 38 + "views/prideflag.html", 37 39 )) 38 40 var htmx_template = template.Must(template.New("root").Parse(`<head>{{block "head" .}}{{end}}</head>{{block "content" .}}{{end}}`)) 39 41 ··· 59 61 uri := req.URL.Path 60 62 start_time := time.Now() 61 63 62 - hx_boosted := len(req.Header["Hx-Boosted"]) > 0 && req.Header["Hx-Boosted"][0] == "true" 64 + hx_request := len(req.Header["Hx-Request"]) > 0 && req.Header["Hx-Request"][0] == "true" 65 + 66 + // don't bother fulfilling requests to a page that's already loaded on the client! 67 + if hx_request && len(req.Header["Referer"]) > 0 && len(req.Header["Hx-Current-Url"]) > 0 { 68 + regex := regexp.MustCompile(`https?:\/\/[^\/]+`) 69 + current_location := regex.ReplaceAllString(req.Header["Hx-Current-Url"][0], "") 70 + if current_location == req.URL.Path { 71 + writer.WriteHeader(204); 72 + return 73 + } 74 + } 63 75 64 76 code := func(writer http.ResponseWriter, req *http.Request) int { 65 77 var root *template.Template 66 - if hx_boosted { 78 + if hx_request { 67 79 root = template.Must(htmx_template.Clone()) 68 80 } else { 69 81 root = template.Must(base_template.Clone())
+1 -7
public/script/header.js
··· 1 - const header_home = document.getElementById("header-home"); 2 1 const header_links = document.getElementById("header-links"); 3 2 const hamburger = document.getElementById("header-links-toggle"); 4 3 ··· 7 6 } 8 7 9 8 document.addEventListener("click", event => { 10 - if (!header_links.contains(event.target) && !hamburger.contains(event.target)) { 9 + if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) { 11 10 header_links.classList.remove("open"); 12 11 } 13 12 }); 14 13 15 14 hamburger.addEventListener("click", event => { toggle_header_links(); }); 16 - 17 - header_home.addEventListener("click", event => { 18 - event.preventDefault(); 19 - location.href = "/"; 20 - });
+141
public/script/lib/htmx-head-support.js
··· 1 + //========================================================== 2 + // head-support.js 3 + // 4 + // An extension to htmx 1.0 to add head tag merging. 5 + //========================================================== 6 + (function(){ 7 + 8 + var api = null; 9 + 10 + function log() { 11 + //console.log(arguments); 12 + } 13 + 14 + function mergeHead(newContent, defaultMergeStrategy) { 15 + 16 + if (newContent && newContent.indexOf('<head') > -1) { 17 + const htmlDoc = document.createElement("html"); 18 + // remove svgs to avoid conflicts 19 + var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, ''); 20 + // extract head tag 21 + var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im); 22 + 23 + // if the head tag exists... 24 + if (headTag) { 25 + 26 + var added = [] 27 + var removed = [] 28 + var preserved = [] 29 + var nodesToAppend = [] 30 + 31 + htmlDoc.innerHTML = headTag; 32 + var newHeadTag = htmlDoc.querySelector("head"); 33 + var currentHead = document.head; 34 + 35 + if (newHeadTag == null) { 36 + return; 37 + } else { 38 + // put all new head elements into a Map, by their outerHTML 39 + var srcToNewHeadNodes = new Map(); 40 + for (const newHeadChild of newHeadTag.children) { 41 + srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); 42 + } 43 + } 44 + 45 + 46 + 47 + // determine merge strategy 48 + var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy; 49 + 50 + // get the current head 51 + for (const currentHeadElt of currentHead.children) { 52 + 53 + // If the current head element is in the map 54 + var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); 55 + var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval"; 56 + var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true"; 57 + if (inNewContent || isPreserved) { 58 + if (isReAppended) { 59 + // remove the current version and let the new version replace it and re-execute 60 + removed.push(currentHeadElt); 61 + } else { 62 + // this element already exists and should not be re-appended, so remove it from 63 + // the new content map, preserving it in the DOM 64 + srcToNewHeadNodes.delete(currentHeadElt.outerHTML); 65 + preserved.push(currentHeadElt); 66 + } 67 + } else { 68 + if (mergeStrategy === "append") { 69 + // we are appending and this existing element is not new content 70 + // so if and only if it is marked for re-append do we do anything 71 + if (isReAppended) { 72 + removed.push(currentHeadElt); 73 + nodesToAppend.push(currentHeadElt); 74 + } 75 + } else { 76 + // if this is a merge, we remove this content since it is not in the new head 77 + if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) { 78 + removed.push(currentHeadElt); 79 + } 80 + } 81 + } 82 + } 83 + 84 + // Push the tremaining new head elements in the Map into the 85 + // nodes to append to the head tag 86 + nodesToAppend.push(...srcToNewHeadNodes.values()); 87 + log("to append: ", nodesToAppend); 88 + 89 + for (const newNode of nodesToAppend) { 90 + log("adding: ", newNode); 91 + var newElt = document.createRange().createContextualFragment(newNode.outerHTML); 92 + log(newElt); 93 + if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) { 94 + currentHead.appendChild(newElt); 95 + added.push(newElt); 96 + } 97 + } 98 + 99 + // remove all removed elements, after we have appended the new elements to avoid 100 + // additional network requests for things like style sheets 101 + for (const removedElement of removed) { 102 + if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) { 103 + currentHead.removeChild(removedElement); 104 + } 105 + } 106 + 107 + api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed}); 108 + } 109 + } 110 + } 111 + 112 + htmx.defineExtension("head-support", { 113 + init: function(apiRef) { 114 + // store a reference to the internal API. 115 + api = apiRef; 116 + 117 + htmx.on('htmx:afterSwap', function(evt){ 118 + var serverResponse = evt.detail.xhr.response; 119 + if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { 120 + mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append"); 121 + } 122 + }) 123 + 124 + htmx.on('htmx:historyRestore', function(evt){ 125 + if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) { 126 + if (evt.detail.cacheMiss) { 127 + mergeHead(evt.detail.serverResponse, "merge"); 128 + } else { 129 + mergeHead(evt.detail.item.head, "merge"); 130 + } 131 + } 132 + }) 133 + 134 + htmx.on('htmx:historyItemCreated', function(evt){ 135 + var historyItem = evt.detail.item; 136 + historyItem.head = document.head.outerHTML; 137 + }) 138 + } 139 + }); 140 + 141 + })()
+14
public/script/main.js
··· 63 63 } 64 64 window.scrollY = 0; 65 65 }); 66 + 67 + const top_button = document.getElementById("backtotop"); 68 + window.onscroll = () => { 69 + if (!top_button) return; 70 + const btt_threshold = 100; 71 + if ( 72 + document.body.scrollTop > btt_threshold || 73 + document.documentElement.scrollTop > btt_threshold 74 + ) { 75 + top_button.classList.add("active"); 76 + } else { 77 + top_button.classList.remove("active"); 78 + } 79 + }
+3 -1
public/script/music-gateway.js
··· 12 12 share_btn.classList.add('active'); 13 13 } 14 14 15 - document.getElementById("go-back").addEventListener("click", () => { 15 + const go_back_btn = document.getElementById("go-back") 16 + go_back_btn.innerText = "<"; 17 + go_back_btn.addEventListener("click", () => { 16 18 window.history.back(); 17 19 }); 18 20
+3 -13
public/script/music.js
··· 1 - document.querySelectorAll("h2.question").forEach(element => { 2 - element.onclick = (e) => { 3 - const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}#${e.target.id}`; 4 - window.location = url; 5 - }; 6 - }); 1 + import "./main.js"; 7 2 8 - document.querySelectorAll("div.music").forEach(element => { 9 - console.log(element); 10 - element.addEventListener("click", (e) => { 11 - const url = `${window.location.protocol}//${window.location.host}/music/${element.id}`; 12 - window.location = url; 13 - }); 3 + document.querySelectorAll("h1.music-title").forEach(element => { 4 + element.href = ""; 14 5 }); 15 -
+27
public/style/main.css
··· 1 1 @import url("/style/colours.css"); 2 2 @import url("/style/header.css"); 3 3 @import url("/style/footer.css"); 4 + @import url("/style/prideflag.css"); 4 5 5 6 @font-face { 6 7 font-family: "Monaspace Argon"; ··· 40 41 41 42 span.newchar { 42 43 animation: newchar 0.25s; 44 + } 45 + 46 + a#backtotop { 47 + position: fixed; 48 + left: 50%; 49 + transform: translateX(-50%); 50 + padding: .5em .8em; 51 + display: block; 52 + border-radius: 2px; 53 + border: 1px solid transparent; 54 + text-decoration: none; 55 + opacity: .5; 56 + transition-property: opacity, transform, border-color, background-color, color; 57 + transition-duration: .2s; 58 + } 59 + 60 + a#backtotop.active { 61 + top: 4rem; 62 + } 63 + 64 + a#backtotop:hover { 65 + color: #eee; 66 + border-color: #eee; 67 + background-color: var(--links); 68 + box-shadow: 0 0 1em var(--links); 69 + opacity: 1; 43 70 } 44 71 45 72 @keyframes newchar {
+2 -1
public/style/music-gateway.css
··· 241 241 #title { 242 242 margin: 0; 243 243 line-height: 1em; 244 - font-size: 3em; 244 + font-size: 2.5em; 245 245 } 246 246 247 247 #year { ··· 571 571 } 572 572 573 573 div#info > div { 574 + min-width: auto; 574 575 min-height: auto; 575 576 padding: 0; 576 577 margin: 0;
+8 -1
public/style/music.css
··· 1 1 @import url("/style/index.css"); 2 2 3 + main { 4 + width: min(calc(100% - 4rem), 720px); 5 + min-height: calc(100vh - 10.3rem); 6 + margin: 0 auto 2rem auto; 7 + padding-top: 4rem; 8 + } 9 + 3 10 div.music { 4 11 margin-bottom: 1rem; 5 12 padding: 1.5rem; ··· 114 121 cursor: pointer; 115 122 } 116 123 117 - .collapse { 124 + div.answer { 118 125 margin: -1rem 0 1rem 0; 119 126 padding: .5em 1.5em; 120 127 border-radius: 4px;
+25
public/style/prideflag.css
··· 1 + #prideflag svg { 2 + position: fixed; 3 + top: 0; 4 + right: 0; 5 + width: 120px; 6 + transform-origin: 100% 0%; 7 + transition: transform .5s cubic-bezier(.32,1.63,.41,1.01); 8 + z-index: 8008135; 9 + pointer-events: none; 10 + } 11 + #prideflag svg:hover { 12 + transform: scale(110%); 13 + } 14 + #prideflag svg:active { 15 + transform: scale(110%); 16 + } 17 + #prideflag svg * { 18 + pointer-events: all; 19 + } 20 + 21 + @media screen and (max-width: 950px) { 22 + #prideflag { 23 + display: none; 24 + } 25 + }
+6 -20
views/base.html
··· 7 7 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 8 8 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9 9 10 - {{block "head" .}} 11 - <!-- <title>ari melody 💫</title> --> 12 - <!-- <link rel="shortcut icon" href="img/favicon.png" type="image/x-icon"> --> 13 - <!----> 14 - <!-- <meta name="description" content="home to your local SPACEGIRL 💫"> --> 15 - <!----> 16 - <!-- <meta property="og:title" content="ari melody"> --> 17 - <!-- <meta property="og:type" content="website"> --> 18 - <!-- <meta property="og:url" content="www.arimelody.me"> --> 19 - <!-- <meta property="og:image" content="https://www.arimelody.me/img/favicon.png"> --> 20 - <!-- <meta property="og:site_name" content="ari melody"> --> 21 - <!-- <meta property="og:description" content="home to your local SPACEGIRL 💫"> --> 22 - <!----> 23 - <!-- <link rel="stylesheet" href="style/main.css"> --> 24 - <!----> 25 - <!-- <script type="module" src="/script/main.js" defer></script> --> 26 - {{end}} 10 + {{block "head" .}}{{end}} 27 11 28 - <!-- <script type="application/javascript" src="/script/lib/htmx.min.js"></script> --> 12 + <meta name="htmx-config" content='{"htmx.config.scrollIntoViewOnBoost":false}'> 13 + <script type="application/javascript" src="/script/lib/htmx.min.js"></script> 14 + <script type="application/javascript" src="/script/lib/htmx-head-support.js"></script> 29 15 </head> 30 16 31 - <body> 17 + <body hx-ext="head-support"> 32 18 {{template "header"}} 33 19 34 20 {{block "content" .}} ··· 43 29 {{end}} 44 30 45 31 {{template "footer"}} 46 - 47 32 <div id="overlay"></div> 33 + {{template "prideflag"}} 48 34 </body> 49 35 50 36 </html>
+1 -1
views/footer.html
··· 1 1 {{define "footer"}} 2 2 3 - <footer hx-preserve="true"> 3 + <footer> 4 4 <div id="footer"> 5 5 <small><em>*made with ♥ by ari, 2024*</em></small> 6 6 </div>
+3 -3
views/header.html
··· 1 1 {{define "header"}} 2 2 3 - <header hx-preserve="true"> 3 + <header> 4 4 <nav> 5 - <div id="header-home"> 5 + <div id="header-home" hx-get="/" hx-on="click" hx-target="main" hx-swap="outerHTML show:window:top" hx-push-url="true"> 6 6 <img src="/img/favicon.png" id="header-icon" width="100" height="100" alt=""> 7 7 <div id="header-text"> 8 8 <h1>ari melody</h1> ··· 16 16 <rect y="40" width="70" height="10" rx="5" fill="#eee" /> 17 17 </svg> 18 18 </a> 19 - <ul id="header-links"> 19 + <ul id="header-links" hx-boost="true" hx-target="main" hx-swap="outerHTML show:window:top"> 20 20 <li> 21 21 <a href="/">home</a> 22 22 </li>
+7 -26
views/htmx-base.html
··· 1 - <head> 2 - <meta http-equiv="content-type" content="text/html; charset=UTF-8"> 3 - <meta charset="UTF-8"> 4 - <meta http-equiv="X-UA-Compatible" content="IE=edge"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - 7 - {{block "head" .}} 8 - <title>ari melody 💫</title> 9 - <link rel="shortcut icon" href="img/favicon.png" type="image/x-icon"> 10 - 11 - <meta name="description" content="home to your local SPACEGIRL 💫"> 12 - 13 - <meta property="og:title" content="ari melody"> 14 - <meta property="og:type" content="website"> 15 - <meta property="og:url" content="www.arimelody.me"> 16 - <meta property="og:image" content="https://www.arimelody.me/img/favicon.png"> 17 - <meta property="og:site_name" content="ari melody"> 18 - <meta property="og:description" content="home to your local SPACEGIRL 💫"> 19 - 20 - <link rel="stylesheet" href="style/main.css"> 21 - 22 - <script type="module" src="/script/main.js" defer></script> 23 - {{end}} 24 - 25 - <script type="application/javascript" src="/script/lib/htmx.min.js"></script> 26 - </head> 1 + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> 2 + <meta charset="UTF-8"> 3 + <meta http-equiv="X-UA-Compatible" content="IE=edge"> 4 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 5 + {{block "head" .}}{{end}} 6 + <script type="application/javascript" src="/script/lib/htmx.min.js"></script> 7 + <script type="application/javascript" src="/script/lib/htmx-head-support.js"></script> 27 8 28 9 {{block "content" .}}{{end}}
-1
views/index.html
··· 12 12 <meta property="og:description" content="home to your local SPACEGIRL 💫"> 13 13 14 14 <link rel="stylesheet" href="/style/index.css"> 15 - 16 15 <script type="module" src="/script/main.js" defer></script> 17 16 <link rel="me" href="https://wetdry.world/@ari"> 18 17 {{end}}
+2 -2
views/music-gateway.html
··· 25 25 <meta name="twitter:image:alt" content="Cover art for &quot;{{.Title}}&quot;"> 26 26 27 27 <script type="module" src="/script/music-gateway.js" defer></script> 28 - <script type="application/javascript" src="/script/prideflag.js" defer></script> 29 28 <link rel="stylesheet" href="/style/music-gateway.css"> 30 29 {{end}} 31 30 ··· 33 32 <main> 34 33 <div id="background" data-url="{{.ResolveArtwork}}"></div> 35 34 36 - <a id="go-back" title="back to arimelody.me" href="/music">&lt;</a> 35 + <a href="/music" id="go-back" title="back to arimelody.me">back to arimelody.me</a> 36 + <br><br> 37 37 38 38 <div id="music-container"> 39 39 <div id="art-container">
+7 -7
views/music.html
··· 12 12 <meta property="og:description" content="music from your local SPACEGIRL 💫"> 13 13 14 14 <link rel="stylesheet" href="/style/music.css"> 15 - 16 - <script type="module" src="/script/main.js" defer></script> 17 - <script type="application/javascript" src="/script/music.js" defer></script> 15 + <script type="module" src="/script/music.js" defer></script> 18 16 {{end}} 19 17 20 18 {{define "content"}} ··· 25 23 26 24 <div id="music-container"> 27 25 {{range $Album := .}} 28 - <div class="music" id="{{$Album.Id}}"> 26 + <div class="music" id="{{$Album.Id}}" hx-get="/music/{{$Album.Id}}" hx-trigger="click" hx-target="main" hx-swap="outerHTML" hx-push-url="true"> 29 27 <div class="music-artwork"> 30 28 <img src="{{$Album.ResolveArtwork}}" alt="{{$Album.Title}} artwork" width="128"> 31 29 </div> 32 - <div class="music-details"> 30 + <div class="music-details" hx-boost="true" hx-target="main" hx-swap="outerHTML"> 33 31 <a href="/music/{{$Album.Id}}"><h1 class="music-title">{{$Album.Title}}</h1></a> 34 32 <h2 class="music-artist">{{$Album.PrintPrimaryArtists}}</h2> 35 33 <h3 class="music-type-{{.ResolveType}}">{{$Album.ResolveType}}</h3> ··· 45 43 {{end}} 46 44 </div> 47 45 48 - <h2 id="usage" class="question"> 46 + <h2 id="usage" class="question" hx-get="/music#usage" hx-on="click" hx-swap="none" hx-push-url="true"> 49 47 <a href="#usage"> 50 48 &gt; "can i use your music in my content?" 51 49 </a> 52 50 </h2> 53 - <div class="collapse"> 51 + <div class="answer"> 54 52 <p> 55 53 <strong class="big">yes!</strong> well, in most cases... 56 54 </p> ··· 85 83 &gt; <a href="mailto:ari@arimelody.me">ari@arimelody.me</a> 86 84 </p> 87 85 </div> 86 + 87 + <a href="#" id="backtotop">back to top</a> 88 88 </main> 89 89 {{end}}
+21
views/prideflag.html
··· 1 + {{define "prideflag"}} 2 + <a href="https://github.com/mellodoot/prideflag" target="_blank" id="prideflag"> 3 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120" hx-preserve="true"> 4 + <path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/> 5 + <path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/> 6 + <path id="yellow" d="M120,40 V0 L60,60 L80,80 Z" style="fill:#e5fe02"/> 7 + <path id="green" d="M120,0 H80 L40,40 L60,60 Z" style="fill:#09be01"/> 8 + <path id="blue" d="M80,0 H40 L20,20 L40,40 Z" style="fill:#081a9a"/> 9 + <path id="purple" d="M40,0 H0 L20,20 Z" style="fill:#76008a"/> 10 + 11 + <rect id="black" x="60" width="60" height="60" style="fill:#010101"/> 12 + <rect id="brown" x="70" width="50" height="50" style="fill:#603814"/> 13 + <rect id="lightblue" x="80" width="40" height="40" style="fill:#73d6ed"/> 14 + <rect id="pink" x="90" width="30" height="30" style="fill:#ffafc8"/> 15 + <rect id="white" x="100" width="20" height="20" style="fill:#fff"/> 16 + 17 + <rect id="intyellow" x="110" width="10" height="10" style="fill:#fed800"/> 18 + <circle id="intpurple" cx="120" cy="0" r="5" stroke="#7601ad" stroke-width="2" fill="none"/> 19 + </svg> 20 + </a> 21 + {{end}}