add release credits update UI

Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
ari melody 2024-08-23 23:08:28 +01:00
parent 7914fba52a
commit 34cddcfdb2
27 changed files with 630 additions and 340 deletions

View file

@ -0,0 +1,48 @@
<dialog id="addcredit">
<header>
<h2>Add artist credit</h2>
</header>
<ul>
{{range $Artist := .Artists}}
<li class="new-artist"
data-id="{{$Artist.ID}}"
hx-get="/admin/release/{{$.ReleaseID}}/newcredit/{{$Artist.ID}}"
hx-target="#editcredits ul"
hx-swap="beforeend"
>
<img src="{{$Artist.GetAvatar}}" alt="" width="16" loading="lazy" class="artist-avatar">
<span class="artist-name">{{$Artist.Name}}</span>
<span class="artist-id">({{$Artist.ID}})</span>
</li>
{{end}}
</ul>
{{if not .Artists}}
<p class="empty">There are no more artists to add.</p>
{{end}}
<div class="dialog-actions">
<button id="cancel" type="button">Cancel</button>
</div>
<script type="text/javascript">
(() => {
const newCreditModal = document.getElementById("addcredit")
const editCreditsModal = document.getElementById("editcredits")
const cancelBtn = newCreditModal.querySelector("#cancel");
editCreditsModal.addEventListener("htmx:afterSwap", () => {
newCreditModal.close();
newCreditModal.remove();
});
cancelBtn.addEventListener("click", () => {
newCreditModal.close();
newCreditModal.remove();
});
newCreditModal.showModal();
})();
</script>
</dialog>

View file

@ -0,0 +1,119 @@
<dialog id="editcredits">
<header>
<h2>Editing: Credits</h2>
<a id="add-credit"
class="button new"
href="/admin/release/{{.ID}}/addcredit"
hx-get="/admin/release/{{.ID}}/addcredit"
hx-target="body"
hx-swap="beforeend"
>Add</a>
</header>
<form action="/api/v1/music/{{.ID}}/credits">
<ul>
{{range .Credits}}
<li class="credit" data-artist="{{.Artist.ID}}">
<div>
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<div class="credit-info">
<p class="artist-name">{{.Artist.Name}}</p>
<div class="credit-attribute">
<label for="role">Role:</label>
<input type="text" name="role" value="{{.Role}}">
</div>
<div class="credit-attribute">
<label for="primary">Primary:</label>
<input type="checkbox" name="primary" {{if .Primary}}checked{{end}}>
</div>
</div>
<button type="button" class="delete">Delete</button>
</div>
</li>
{{end}}
</ul>
<div class="dialog-actions">
<button id="discard" type="button">Discard</button>
<button id="save" type="submit" class="save">Save</button>
</div>
</form>
<script type="module">
(() => {
const container = document.getElementById("editcredits");
const form = document.querySelector("#editcredits form");
const creditList = form.querySelector("ul");
const addCreditBtn = document.getElementById("add-credit");
const discardBtn = form.querySelector("button#discard");
function creditFromElement(el) {
const artistID = el.dataset.artist;
const roleInput = el.querySelector(`input[name="role"]`)
const primaryInput = el.querySelector(`input[name="primary"]`)
const deleteBtn = el.querySelector("button.delete");
let credit = {
"artist": artistID,
"role": roleInput.value,
"primary": primaryInput.checked,
};
roleInput.addEventListener("change", () => {
credit.role = roleInput.value;
});
primaryInput.addEventListener("change", () => {
credit.primary = primaryInput.checked;
});
deleteBtn.addEventListener("click", e => {
if (!confirm("Are you sure you want to delete " + artistID + "'s credit?")) return;
el.remove();
credits = credits.filter(credit => credit.artist != artistID);
});
return credit;
}
let credits = [...form.querySelectorAll(".credit")].map(el => creditFromElement(el));
creditList.addEventListener("htmx:afterSwap", e => {
const el = creditList.children[creditList.children.length - 1];
const credit = creditFromElement(el);
credits.push(credit);
});
container.showModal();
container.addEventListener("close", () => {
container.remove();
});
form.addEventListener("submit", e => {
e.preventDefault();
fetch(form.action, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credits)
}).then(res => {
if (res.ok) location = location;
else {
res.text().then(err => {
alert(err);
console.error(err);
});
}
}).catch(err => {
alert("Failed to update credits. Check the console for details");
console.error(err);
});
});
discardBtn.addEventListener("click", e => {
e.preventDefault();
container.close();
});
})();
</script>
</dialog>

View file

@ -0,0 +1,17 @@
<li class="credit" data-artist="{{.ID}}">
<div>
<img src="{{.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<div class="credit-info">
<p class="artist-name">{{.Name}}</p>
<div class="credit-attribute">
<label for="role">Role:</label>
<input type="text" name="role" value="">
</div>
<div class="credit-attribute">
<label for="primary">Primary:</label>
<input type="checkbox" name="primary">
</div>
</div>
<button type="button" class="delete">Delete</button>
</div>
</li>

View file

View file

View file

View file

View file

View file

View file

@ -85,9 +85,9 @@ func MustAuthorise(next http.Handler) http.Handler {
}
func GetSession(r *http.Request) *Session {
// if ADMIN_BYPASS {
// return &Session{}
// }
if ADMIN_BYPASS {
return &Session{}
}
var token = ""
// is the session token in context?
@ -177,22 +177,13 @@ func LoginHandler() http.Handler {
cookie.Name = "token"
cookie.Value = session.Token
cookie.Expires = time.Now().Add(24 * time.Hour)
// TODO: uncomment this probably that might be nice i think
// cookie.Secure = true
cookie.HttpOnly = true
cookie.Path = "/"
http.SetCookie(w, &cookie)
serveTemplate("login.html", loginData{Token: session.Token}).ServeHTTP(w, r)
// w.WriteHeader(http.StatusOK)
// w.Header().Add("Content-Type", "text/html")
// w.Write([]byte(
// "<!DOCTYPE html><html><head>"+
// "<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+
// "</head><body>"+
// "Logged in successfully. "+
// "You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+
// "</body></html>"),
// )
})
}
@ -255,6 +246,39 @@ func serveTemplate(page string, data any) http.Handler {
})
}
func serveComponent(page string, data any) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fp := filepath.Join("admin", "components", filepath.Clean(page))
info, err := os.Stat(fp)
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
}
if info.IsDir() {
http.NotFound(w, r)
return
}
template, err := template.ParseFiles(fp)
if err != nil {
fmt.Printf("Error parsing template files: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = template.Execute(w, data);
if err != nil {
fmt.Printf("Error executing template: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
})
}
func staticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path)))

View file

@ -4,6 +4,7 @@ import (
"fmt"
"html/template"
"net/http"
"path"
"strings"
"arimelody.me/arimelody.me/global"
@ -25,14 +26,26 @@ type (
func serveRelease() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
slices := strings.Split(r.URL.Path[1:], "/")
id := slices[0]
release := global.GetRelease(id)
if release == nil {
http.NotFound(w, r)
return
}
id := r.URL.Path[1:]
release := global.GetRelease(id)
if release == nil {
if len(slices) > 1 {
switch slices[1] {
case "editcredits":
serveEditCredits(release).ServeHTTP(w, r)
return
case "addcredit":
serveAddCredit(release).ServeHTTP(w, r)
return
case "newcredit":
serveNewCredit().ServeHTTP(w, r)
return
}
http.NotFound(w, r)
return
}
@ -56,3 +69,55 @@ func serveRelease() http.Handler {
}
})
}
func serveEditCredits(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
serveComponent(path.Join("credits", "editcredits.html"), release).ServeHTTP(w, r)
return
})
}
func serveAddCredit(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var artists = []*model.Artist{}
for _, artist := range global.Artists {
var exists = false
for _, credit := range release.Credits {
if credit.Artist == artist {
exists = true
break
}
}
if !exists {
artists = append(artists, artist)
}
}
type response struct {
ReleaseID string;
Artists []*model.Artist
}
w.Header().Set("Content-Type", "text/html")
serveComponent(path.Join("credits", "addcredit.html"), response{
ReleaseID: release.ID,
Artists: artists,
}).ServeHTTP(w, r)
return
})
}
func serveNewCredit() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
artist := global.GetArtist(strings.Split(r.URL.Path, "/")[3])
if artist == nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
serveComponent(path.Join("credits", "newcredit.html"), artist).ServeHTTP(w, r)
return
})
}

View file

@ -15,7 +15,7 @@ body {
background: #f0f0f0;
}
header {
nav {
width: min(720px, calc(100% - 2em));
height: 2em;
margin: 1em auto;
@ -27,10 +27,10 @@ header {
border-radius: .5em;
border: 1px solid #808080;
}
header .icon {
nav .icon {
height: 100%;
}
header .title {
nav .title {
width: auto;
height: 100%;
@ -43,7 +43,7 @@ header .title {
color: inherit;
}
header a {
nav a {
width: auto;
height: 100%;
@ -57,11 +57,11 @@ header a {
color: inherit;
}
header a:hover {
nav a:hover {
background: #00000010;
text-decoration: none;
}
header #logout {
nav #logout {
margin-left: auto;
}
@ -80,6 +80,11 @@ a:hover {
text-decoration: underline;
}
a img {
height: .9em;
transform: translateY(.1em);
}
.card {
margin-bottom: 2em;
}

View file

@ -1,6 +1,6 @@
#release {
margin-bottom: 1em;
padding: 1em;
padding: 1.5em;
display: flex;
flex-direction: row;
gap: 1.2em;
@ -28,7 +28,7 @@
}
.release-info {
margin: .5em 0;
margin: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
@ -96,29 +96,26 @@ button:active, .button:active {
border-color: #808080;
}
button.edit {
button {
color: inherit;
}
button.new {
background: #c4ff6a;
border-color: #84b141;
}
button.edit:hover {
background: #fff;
border-color: #d0d0d0;
}
button.edit:active {
background: #d0d0d0;
border-color: #808080;
}
button.save {
background: #6fd7ff;
border-color: #6f9eb0;
}
button.save:hover {
button.delete {
background: #ff7171;
border-color: #7d3535;
}
button:hover {
background: #fff;
border-color: #d0d0d0;
}
button.save:active {
button:active {
background: #d0d0d0;
border-color: #808080;
}
@ -137,7 +134,7 @@ button[disabled] {
justify-content: right;
}
.credit {
.card.credits .credit {
margin-bottom: .5em;
padding: .5em;
display: flex;
@ -150,15 +147,15 @@ button[disabled] {
border: 1px solid #808080;
}
.credit .artist-avatar {
.card.credits .credit .artist-avatar {
border-radius: .5em;
}
.credit .artist-name {
.card.credits .credit .artist-name {
font-weight: bold;
}
.credit .artist-role small {
.card.credits .credit .artist-role small {
font-size: inherit;
opacity: .66;
}
@ -182,6 +179,10 @@ button[disabled] {
justify-content: space-between;
}
.card-title a.button {
text-decoration: none;
}
.track-id {
width: fit-content;
font-family: "Monaspace Argon", monospace;
@ -217,3 +218,88 @@ button[disabled] {
opacity: 0.75;
}
dialog {
width: min(720px, calc(100% - 2em));
padding: 2em;
border: 1px solid #101010;
border-radius: 8px;
}
dialog header {
margin-bottom: 1em;
background: none;
display: flex;
flex-direction: row;
justify-content: space-between;
}
dialog header h2 {
margin: 0;
}
dialog div.dialog-actions {
margin-top: 1em;
display: flex;
flex-direction: row;
justify-content: end;
gap: .5em;
}
dialog#editcredits ul {
margin: 0;
padding: 0;
list-style: none;
}
dialog#editcredits .credit>div {
margin-bottom: .5em;
padding: .5em;
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
border-radius: .5em;
background: #f8f8f8f8;
border: 1px solid #808080;
}
dialog#editcredits .credit p {
margin: 0;
}
dialog#editcredits .credit .artist-avatar {
border-radius: .5em;
}
dialog#editcredits .credit .credit-info {
width: 100%;
}
dialog#editcredits .credit .credit-info .credit-attribute {
width: 100%;
display: flex;
}
dialog#editcredits .credit .credit-info .credit-attribute input[type="text"] {
margin-left: .25em;
padding: .2em .4em;
flex-grow: 1;
font-family: inherit;
border: 1px solid #8888;
border-radius: 4px;
color: inherit;
}
dialog#editcredits .credit .artist-name {
font-weight: bold;
}
dialog#editcredits .credit .artist-role small {
font-size: inherit;
opacity: .66;
}
dialog#editcredits .credit button.delete {
margin-left: auto;
}

View file

@ -52,7 +52,7 @@
placeholder="No description provided."
rows="3"
id="description"
>{{.Description}}</textarea>
>{{.Description}}</textarea>
</td>
</tr>
<tr>
@ -84,7 +84,7 @@
</tr>
</table>
<div class="release-actions">
<a href="/music/{{.ID}}" class="button">Gateway</a>
<a href="/music/{{.ID}}" class="button" target="_blank">Gateway <img src="/img/external-link.svg"/></a>
<button type="submit" class="save" id="save" disabled>Save</button>
</div>
</div>
@ -92,17 +92,22 @@
<div class="card-title">
<h2>Credits ({{len .Credits}})</h2>
<button id="update-credits" class="edit">Edit</button>
<a class="button edit"
href="/admin/release/{{.ID}}/editcredits"
hx-get="/admin/release/{{.ID}}/editcredits"
hx-target="body"
hx-swap="beforeend"
>Edit</a>
</div>
<div class="card credits">
{{range $Credit := .Credits}}
{{range .Credits}}
<div class="credit">
<img src="{{$Credit.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<div class="credit-info">
<p class="artist-name"><a href="/admin/artists/{{$Credit.Artist.ID}}">{{$Credit.Artist.Name}}</a></p>
<p class="artist-name"><a href="/admin/artists/{{.Artist.ID}}">{{.Artist.Name}}</a></p>
<p class="artist-role">
{{$Credit.Role}}
{{if $Credit.Primary}}
{{.Role}}
{{if .Primary}}
<small>(Primary)</small>
{{end}}
</p>
@ -114,22 +119,44 @@
{{end}}
</div>
<div class="card-title">
<h2>Links ({{len .Links}})</h2>
<a class="button edit"
href="/admin/release/{{.ID}}/editlinks"
hx-get="/admin/release/{{.ID}}/editlinks"
hx-target="body"
hx-swap="beforeend"
>Edit</a>
</div>
<div class="card links">
{{range .Links}}
<div class="release-link" data-id="{{.Name}}">
<a href="{{.URL}}" class="button">{{.Name}} <img src="/img/external-link.svg"/></a></p>
</div>
{{end}}
</div>
<div class="card-title">
<h2>Tracklist ({{len .Tracks}})</h2>
<button id="update-tracks" class="edit">Edit</button>
<a class="button edit"
href="/admin/release/{{.ID}}/edittracks"
hx-get="/admin/release/{{.ID}}/edittracks"
hx-target="body"
hx-swap="beforeend"
>Edit</a>
</div>
<div class="card tracks">
{{range $Track := .Tracks}}
<div class="track" data-id="{{$Track.ID}}">
<h2 class="track-title">{{$Track.Number}}. {{$Track.Title}}</h2>
<p class="track-id">{{$Track.ID}}</p>
{{if $Track.Description}}
<p class="track-description">{{$Track.Description}}</p>
{{range .Tracks}}
<div class="track" data-id="{{.ID}}">
<h2 class="track-title">{{.Number}}. {{.Title}}</h2>
<p class="track-id">{{.ID}}</p>
{{if .Description}}
<p class="track-description">{{.Description}}</p>
{{else}}
<p class="track-description empty">No description provided.</p>
{{end}}
{{if $Track.Lyrics}}
<p class="track-lyrics">{{$Track.Lyrics}}</p>
{{if .Lyrics}}
<p class="track-lyrics">{{.Lyrics}}</p>
{{else}}
<p class="track-lyrics empty">There are no lyrics.</p>
{{end}}

View file

@ -10,14 +10,17 @@
{{block "head" .}}{{end}}
<link rel="stylesheet" href="/admin/static/admin.css">
<script type="module" src="/script/vendor/htmx.min.js"></script>
</head>
<body>
<header>
<img src="/img/favicon.png" alt="" class="icon">
<a href="/">arimelody.me</a>
<a href="/admin">home</a>
<a href="/admin/logout" id="logout">log out</a>
<nav>
<img src="/img/favicon.png" alt="" class="icon">
<a href="/">arimelody.me</a>
<a href="/admin">home</a>
<a href="/admin/logout" id="logout">log out</a>
</nav>
</header>
{{block "content" .}}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-8,-8)">
<g transform="matrix(0.9375,0,0,0.9375,8,16)">
<path d="M110.222,70.621C110.222,68.866 111.298,67.289 112.932,66.649C114.567,66.008 116.427,66.434 117.62,67.723L126.864,77.707C127.594,78.495 128,79.53 128,80.605L128,96C128,113.661 113.661,128 96,128L32,128C14.339,128 0,113.661 0,96L0,32C0,14.339 14.339,0 32,0L47.395,0C48.47,-0 49.505,0.406 50.293,1.136L60.277,10.38C61.566,11.573 61.992,13.433 61.351,15.068C60.711,16.702 59.134,17.778 57.379,17.778L32,17.778C24.151,17.778 17.778,24.151 17.778,32L17.778,96C17.778,103.849 24.151,110.222 32,110.222L96,110.222C103.849,110.222 110.222,103.849 110.222,96L110.222,70.621ZM65.524,82.956C64.724,83.756 63.638,84.206 62.507,84.206C61.375,84.206 60.29,83.757 59.49,82.956L45.044,68.51C44.243,67.71 43.794,66.625 43.794,65.493C43.794,64.362 44.244,63.276 45.044,62.476L92.16,15.36L75.55,-1.25C74.33,-2.47 73.965,-4.305 74.625,-5.899C75.286,-7.494 76.842,-8.533 78.567,-8.533L132.267,-8.533C133.398,-8.533 134.484,-8.084 135.284,-7.284C136.084,-6.483 136.533,-5.398 136.533,-4.267L136.533,49.433C136.533,51.158 135.494,52.714 133.899,53.375C132.305,54.035 130.47,53.67 129.25,52.45L112.64,35.84L65.524,82.956Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -2,84 +2,72 @@ import "./header.js";
import "./config.js";
function type_out(e) {
const text = e.innerText;
const original = e.innerHTML;
e.innerText = "";
const delay = 25;
let chars = 0;
const text = e.innerText;
const original = e.innerHTML;
const delay = 25;
let chars = 0;
function insert_char(character, parent) {
const c = document.createElement("span");
c.innerText = character;
parent.appendChild(c);
c.classList.add("newchar");
function insert_char(character, parent) {
if (chars == 0) parent.innerHTML = "";
const c = document.createElement("span");
c.innerText = character;
parent.appendChild(c);
c.classList.add("newchar");
}
function normalize() {
e.innerHTML = original;
}
function increment_char() {
const newchar = text.substring(chars, chars + 1);
insert_char(newchar, e);
chars++;
if (chars <= text.length) {
setTimeout(increment_char, delay);
} else {
setTimeout(normalize, 250);
}
}
function normalize() {
e.innerHTML = original;
}
function increment_char() {
const newchar = text.substring(chars - 1, chars);
insert_char(newchar, e);
chars++;
if (chars <= text.length) {
setTimeout(increment_char, delay);
} else {
setTimeout(normalize, 250);
}
}
increment_char();
increment_char();
}
function fill_list(list) {
const items = list.querySelectorAll("li a, li span");
items.innerText = "";
const delay = 100;
const items = list.querySelectorAll("li a, li span");
items.innerText = "";
const delay = 100;
items.forEach((item, iter) => {
item.style.animationDelay = `${iter * delay}ms`;
item.style.animationPlayState = "playing";
});
items.forEach((item, iter) => {
item.style.animationDelay = `${iter * delay}ms`;
item.style.animationPlayState = "playing";
});
}
function start() {
[...document.querySelectorAll("h1, h2, h3, h4, h5, h6")]
.filter((e) => e.innerText != "")
.forEach((e) => {
type_out(e);
});
[...document.querySelectorAll("ol, ul")]
.filter((e) => e.innerText != "")
.forEach((e) => {
fill_list(e);
});
document.addEventListener("htmx:afterSwap", async event => {
const res = await event.detail.xhr.response;
var new_head = res.substring(res.indexOf("<head>")+1, res.indexOf("</head>"));
if (new_head) {
document.head.innerHTML = new_head;
}
window.scrollY = 0;
document.addEventListener("DOMContentLoaded", () => {
[...document.querySelectorAll(".typeout")]
.filter((e) => e.innerText != "")
.forEach((e) => {
type_out(e);
console.log(e);
});
[...document.querySelectorAll("ol, ul")]
.filter((e) => e.innerText != "")
.forEach((e) => {
fill_list(e);
});
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");
}
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");
}
}
document.addEventListener("swap", () => {
start();
}
});

View file

@ -1,117 +0,0 @@
const swap_event = new Event("swap");
let caches = {};
async function cached_fetch(url) {
let cached = caches[url];
const res = cached === undefined ? await fetch(url) : await fetch(url, {
headers: {
"If-Modified-Since": cached.last_modified
}
});
if (res.status === 304 && cached !== undefined) {
return cached.content;
}
if (res.status !== 200) return;
if (!res.headers.get("content-type").startsWith("text/html")) return;
const text = await res.text();
if (res.headers.get("last-modified")) {
caches[url] = {
content: text,
last_modified: res.headers.get("last-modified")
}
}
return text;
}
async function swap(url, stateful) {
if (typeof url !== 'string') return;
const segments = window.location.href.split("/");
if (url.startsWith(window.location.origin) && segments[segments.length - 1].includes("#")) {
window.location.href = url;
return;
}
if (stateful && window.location.href.endsWith(url)) return;
const text = await cached_fetch(url);
const content = new DOMParser().parseFromString(text, "text/html");
const stylesheets = [...content.querySelectorAll("link[rel='stylesheet']")];
// swap title
document.title = content.title;
// swap body html
document.body.innerHTML = content.body.innerHTML;
// swap stylesheets
const old_sheets = document.head.querySelectorAll("link[rel='stylesheet']");
stylesheets.forEach(stylesheet => {
let exists = false;
old_sheets.forEach(old_sheet => {
if (old_sheet.href === stylesheet.href) exists = true;
});
if (!exists) document.head.appendChild(stylesheet);
});
old_sheets.forEach(old_sheet => {
let exists = false;
stylesheets.forEach(stylesheet => {
if (stylesheet.href === old_sheet.href) exists = true;
});
if (!exists) old_sheet.remove();
});
// push history
if (stateful) history.pushState(url, "", url);
bind(document.body);
}
function bind(content) {
if (typeof content !== 'object' || content.nodeType !== Node.ELEMENT_NODE) return;
content.querySelectorAll("[swap-url]").forEach(element => {
const href = element.attributes.getNamedItem('swap-url').value;
element.addEventListener("click", event => {
event.preventDefault();
swap(href, true);
});
[...element.querySelectorAll("a[href], [swap-url]")].forEach(element => {
if (element.href) {
if (!element.href.endsWith(href)) return;
element.attributes.removeNamedItem("href");
}
const swap_url = element.attributes.getNamedItem("swap-url");
if (swap_url) {
if (!swap_url.endsWith(href)) return;
element.attributes.removeNamedItem("swap-url");
}
});
});
content.querySelectorAll("a[href]:not([swap-url])").forEach(element => {
if (element.href.includes("#")) return;
if (!element.href.startsWith(window.location.origin)) return;
const href = element.href.substring(window.location.origin.length);
if (href.includes(".")) return;
element.addEventListener("click", event => {
event.preventDefault();
swap(element.href, true);
});
});
document.dispatchEvent(swap_event);
}
window.addEventListener("popstate", event => {
swap(event.state, false);
});
document.addEventListener("DOMContentLoaded", () => {
bind(document.body);
});

1
public/script/vendor/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -4,131 +4,136 @@
@import url("/style/prideflag.css");
@font-face {
font-family: "Monaspace Argon";
src: url("/font/monaspace-argon/MonaspaceArgonVarVF[wght,wdth,slnt].woff2") format("woff2-variations");
font-weight: 125 950;
font-stretch: 75% 125%;
font-style: oblique 0deg 20deg;
font-family: "Monaspace Argon";
src: url("/font/monaspace-argon/MonaspaceArgonVarVF[wght,wdth,slnt].woff2") format("woff2-variations");
font-weight: 125 950;
font-stretch: 75% 125%;
font-style: oblique 0deg 20deg;
}
body {
margin: 0;
padding: 0;
background: #080808;
color: #eee;
font-family: "Monaspace Argon", monospace;
font-size: 18px;
text-shadow: 0 0 3em;
scroll-behavior: smooth;
margin: 0;
padding: 0;
background: #080808;
color: #eee;
font-family: "Monaspace Argon", monospace;
font-size: 18px;
text-shadow: 0 0 3em;
scroll-behavior: smooth;
}
main {
}
a {
color: var(--links);
text-decoration: none;
color: var(--links);
text-decoration: none;
}
a:hover {
text-decoration: underline;
text-decoration: underline;
}
a.link-button {
padding: .3em .5em;
border: 1px solid var(--links);
color: var(--links);
border-radius: 2px;
background-color: transparent;
transition-property: color, border-color, background-color;
transition-duration: .2s;
animation-delay: 0s;
animation: list-item-fadein .2s forwards;
opacity: 0;
padding: .3em .5em;
border: 1px solid var(--links);
color: var(--links);
border-radius: 2px;
background-color: transparent;
transition-property: color, border-color, background-color;
transition-duration: .2s;
animation-delay: 0s;
animation: list-item-fadein .2s forwards;
opacity: 0;
}
a.link-button:hover {
color: #eee;
border-color: #eee;
background-color: var(--links) !important;
text-decoration: none;
box-shadow: 0 0 1em var(--links);
color: #eee;
border-color: #eee;
background-color: var(--links) !important;
text-decoration: none;
box-shadow: 0 0 1em var(--links);
}
a img {
height: .9em;
transform: translateY(.1em);
}
small {
font-size: 1em;
color: #aaa;
font-size: 1em;
color: #aaa;
}
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;
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;
top: 4rem;
}
a#backtotop:hover {
color: #eee;
border-color: #eee;
background-color: var(--links);
box-shadow: 0 0 1em var(--links);
opacity: 1;
color: #eee;
border-color: #eee;
background-color: var(--links);
box-shadow: 0 0 1em var(--links);
opacity: 1;
}
@keyframes newchar {
from {
background: #fff8;
}
from {
background: #fff8;
}
}
@keyframes list-item-fadein {
from {
opacity: 1;
background: #fff8;
}
from {
opacity: 1;
background: #fff8;
}
to {
opacity: 1;
background: transparent;
}
to {
opacity: 1;
background: transparent;
}
}
#overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-image: linear-gradient(180deg, rgba(0,0,0,0) 15%, rgb(0, 0, 0) 40%, rgb(0, 0, 0) 60%, rgba(0,0,0,0) 85%);
background-size: 100vw .2em;
background-repeat: repeat;
opacity: .5;
pointer-events: none;
mix-blend-mode: overlay;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-image: linear-gradient(180deg, rgba(0,0,0,0) 15%, rgb(0, 0, 0) 40%, rgb(0, 0, 0) 60%, rgba(0,0,0,0) 85%);
background-size: 100vw .2em;
background-repeat: repeat;
opacity: .5;
pointer-events: none;
mix-blend-mode: overlay;
}
@media screen and (max-width: 780px) {
body {
font-size: 14px;
}
body {
font-size: 14px;
}
main {
margin-top: 4rem;
}
main {
margin-top: 4rem;
}
}

View file

@ -459,6 +459,12 @@ div#extras ul li a.active {
display: none;
}
.album-track-subheading {
width: fit-content;
padding: .3em 1em;
background: #101010;
}
footer {
position: fixed;
left: 0;

BIN
res/external-link.afdesign Normal file

Binary file not shown.

View file

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

View file

@ -20,7 +20,7 @@
{{define "content"}}
<main>
<h1>
<h1 class="typeout">
# hello, world!
</h1>
@ -58,7 +58,7 @@
<hr>
<h2>
<h2 class="typeout">
## metadata
</h2>
@ -133,7 +133,7 @@
<hr>
<h2>
<h2 class="typeout">
## cool people
</h2>

View file

@ -9,11 +9,7 @@
{{block "head" .}}{{end}}
<!-- <meta name="htmx-config" content='{"htmx.config.scrollIntoViewOnBoost":false}'> -->
<!-- <script type="application/javascript" src="/script/lib/htmx.js"></script> -->
<!-- <script type="application/javascript" src="/script/lib/htmx.min.js"></script> -->
<!-- <script type="application/javascript" src="/script/lib/htmx-preload.js"></script> -->
<!-- <script type="application/javascript" src="/script/swap.js"></script> -->
<script type="module", src="/script/main.js"></script>
</head>
<body>

View file

@ -34,7 +34,7 @@
<div id="background" style="background-image: url({{.GetArtwork}})"></div>
<a href="/music" swap-url="/music" id="go-back" title="back to arimelody.me">&lt;</a>
<a href="/music" id="go-back" title="back to arimelody.me">&lt;</a>
<br><br>
<div id="music-container">
@ -61,6 +61,7 @@
<p id="type" class="{{.ReleaseType}}">{{.ReleaseType}}</p>
{{else}}
<p id="type" class="upcoming">upcoming</p>
<p>Releases: {{.PrintReleaseDate}}</p>
{{end}}
<ul id="links">
@ -118,6 +119,13 @@
{{range $i, $track := .Tracks}}
<details>
<summary class="album-track-title">{{$track.Number}}. {{$track.Title}}</summary>
{{if $track.Description}}
<p class="album-track-subheading">DESCRIPTION</p>
{{$track.Description}}
{{end}}
<p class="album-track-subheading">LYRICS</p>
{{if $track.Lyrics}}
{{$track.Lyrics}}
{{else}}

View file

@ -20,13 +20,13 @@
<main>
<script type="module" src="/script/music.js"></script>
<h1>
<h1 class="typeout">
# my music
</h1>
<div id="music-container">
{{range $Release := .}}
<div class="music" id="{{$Release.ID}}" swap-url="/music/{{$Release.ID}}">
<div class="music" id="{{$Release.ID}}">
<div class="music-artwork">
<img src="{{$Release.GetArtwork}}" alt="{{$Release.Title}} artwork" width="128" loading="lazy">
</div>
@ -50,7 +50,7 @@
{{end}}
</div>
<h2 id="usage" class="question">
<h2 id="usage" class="question typeout">
<a href="#usage">
&gt; "can i use your music in my content?"
</a>