add release credits update UI
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
7914fba52a
commit
34cddcfdb2
48
admin/components/credits/addcredit.html
Normal file
48
admin/components/credits/addcredit.html
Normal 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>
|
119
admin/components/credits/editcredits.html
Normal file
119
admin/components/credits/editcredits.html
Normal 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>
|
17
admin/components/credits/newcredit.html
Normal file
17
admin/components/credits/newcredit.html
Normal 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>
|
0
admin/components/links/addlink.html
Normal file
0
admin/components/links/addlink.html
Normal file
0
admin/components/links/editlinks.html
Normal file
0
admin/components/links/editlinks.html
Normal file
0
admin/components/links/newlink.html
Normal file
0
admin/components/links/newlink.html
Normal file
0
admin/components/tracks/addtrack.html
Normal file
0
admin/components/tracks/addtrack.html
Normal file
0
admin/components/tracks/edittracks.html
Normal file
0
admin/components/tracks/edittracks.html
Normal file
0
admin/components/tracks/newtrack.html
Normal file
0
admin/components/tracks/newtrack.html
Normal 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)))
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
<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" .}}
|
||||
|
|
9
public/img/external-link.svg
Normal file
9
public/img/external-link.svg
Normal 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 |
|
@ -4,11 +4,11 @@ import "./config.js";
|
|||
function type_out(e) {
|
||||
const text = e.innerText;
|
||||
const original = e.innerHTML;
|
||||
e.innerText = "";
|
||||
const delay = 25;
|
||||
let chars = 0;
|
||||
|
||||
function insert_char(character, parent) {
|
||||
if (chars == 0) parent.innerHTML = "";
|
||||
const c = document.createElement("span");
|
||||
c.innerText = character;
|
||||
parent.appendChild(c);
|
||||
|
@ -20,7 +20,7 @@ function type_out(e) {
|
|||
}
|
||||
|
||||
function increment_char() {
|
||||
const newchar = text.substring(chars - 1, chars);
|
||||
const newchar = text.substring(chars, chars + 1);
|
||||
insert_char(newchar, e);
|
||||
chars++;
|
||||
if (chars <= text.length) {
|
||||
|
@ -44,11 +44,12 @@ function fill_list(list) {
|
|||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
[...document.querySelectorAll("h1, h2, h3, h4, h5, h6")]
|
||||
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 != "")
|
||||
|
@ -56,15 +57,6 @@ function start() {
|
|||
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;
|
||||
});
|
||||
|
||||
const top_button = document.getElementById("backtotop");
|
||||
window.onscroll = () => {
|
||||
if (!top_button) return;
|
||||
|
@ -78,8 +70,4 @@ function start() {
|
|||
top_button.classList.remove("active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("swap", () => {
|
||||
start();
|
||||
});
|
||||
|
|
|
@ -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
1
public/script/vendor/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -55,6 +55,11 @@ a.link-button:hover {
|
|||
box-shadow: 0 0 1em var(--links);
|
||||
}
|
||||
|
||||
a img {
|
||||
height: .9em;
|
||||
transform: translateY(.1em);
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 1em;
|
||||
color: #aaa;
|
||||
|
|
|
@ -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
BIN
res/external-link.afdesign
Normal file
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"><</a>
|
||||
<a href="/music" id="go-back" title="back to arimelody.me"><</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}}
|
||||
|
|
|
@ -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">
|
||||
> "can i use your music in my content?"
|
||||
</a>
|
||||
|
|
Loading…
Reference in a new issue