add release credits update UI
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
7914fba52a
commit
34cddcfdb2
27 changed files with 630 additions and 340 deletions
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;
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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" .}}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue