home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

start huge dashboard rework; improve dark theme

+357 -227
+9 -11
admin/accounthttp.go
··· 45 45 } 46 46 47 47 accountResponse struct { 48 - Session *model.Session 48 + adminPageData 49 49 TOTPs []TOTP 50 50 } 51 51 ) ··· 66 66 session.Error = sessionError 67 67 68 68 err = templates.AccountTemplate.Execute(w, accountResponse{ 69 - Session: session, 69 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 70 70 TOTPs: totps, 71 71 }) 72 72 if err != nil { ··· 170 170 } 171 171 172 172 type totpConfirmData struct { 173 - Session *model.Session 173 + adminPageData 174 174 TOTP *model.TOTP 175 175 NameEscaped string 176 176 QRBase64Image string ··· 179 179 func totpSetupHandler(app *model.AppState) http.Handler { 180 180 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 181 181 if r.Method == http.MethodGet { 182 - type totpSetupData struct { 183 - Session *model.Session 184 - } 185 - 186 182 session := r.Context().Value("session").(*model.Session) 187 183 188 - err := templates.TOTPSetupTemplate.Execute(w, totpSetupData{ Session: session }) 184 + err := templates.TOTPSetupTemplate.Execute(w, adminPageData{ Path: "/account", Session: session }) 189 185 if err != nil { 190 186 fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) 191 187 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 222 218 if err != nil { 223 219 fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) 224 220 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 225 - err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{ Session: session }) 221 + err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{ 222 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 223 + }) 226 224 if err != nil { 227 225 fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) 228 226 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 237 235 } 238 236 239 237 err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ 240 - Session: session, 238 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 241 239 TOTP: &totp, 242 240 NameEscaped: url.PathEscape(totp.Name), 243 241 QRBase64Image: qrBase64Image, ··· 298 296 if code != confirmCodeOffset { 299 297 session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } 300 298 err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ 301 - Session: session, 299 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 302 300 TOTP: totp, 303 301 NameEscaped: url.PathEscape(totp.Name), 304 302 QRBase64Image: qrBase64Image,
+2 -2
admin/artisthttp.go
··· 33 33 } 34 34 35 35 type ArtistResponse struct { 36 - Session *model.Session 36 + adminPageData 37 37 Artist *model.Artist 38 38 Credits []*model.Credit 39 39 } ··· 41 41 session := r.Context().Value("session").(*model.Session) 42 42 43 43 err = templates.EditArtistTemplate.Execute(w, ArtistResponse{ 44 - Session: session, 44 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 45 45 Artist: artist, 46 46 Credits: credits, 47 47 })
+22 -21
admin/http.go
··· 21 21 "golang.org/x/crypto/bcrypt" 22 22 ) 23 23 24 + type adminPageData struct { 25 + Path string 26 + Session *model.Session 27 + } 28 + 24 29 func Handler(app *model.AppState) http.Handler { 25 30 mux := http.NewServeMux() 26 31 ··· 67 72 68 73 session := r.Context().Value("session").(*model.Session) 69 74 70 - releases, err := controller.GetAllReleases(app.DB, false, 0, true) 75 + releases, err := controller.GetAllReleases(app.DB, false, 3, true) 71 76 if err != nil { 72 77 fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) 78 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 79 + return 80 + } 81 + releaseCount, err := controller.GetReleasesCount(app.DB, false) 82 + if err != nil { 83 + fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err) 73 84 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 74 85 return 75 86 } ··· 89 100 } 90 101 91 102 type IndexData struct { 92 - Session *model.Session 93 - Releases []*model.Release 94 - Artists []*model.Artist 95 - Tracks []*model.Track 103 + adminPageData 104 + Releases []*model.Release 105 + ReleaseCount int 106 + Artists []*model.Artist 107 + Tracks []*model.Track 96 108 } 97 109 98 110 err = templates.IndexTemplate.Execute(w, IndexData{ 99 - Session: session, 111 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 100 112 Releases: releases, 113 + ReleaseCount: releaseCount, 101 114 Artists: artists, 102 115 Tracks: tracks, 103 116 }) ··· 119 132 return 120 133 } 121 134 122 - type registerData struct { 123 - Session *model.Session 124 - } 125 - 126 135 render := func() { 127 - err := templates.RegisterTemplate.Execute(w, registerData{ Session: session }) 136 + err := templates.RegisterTemplate.Execute(w, adminPageData{ Path: r.URL.Path, Session: session }) 128 137 if err != nil { 129 138 fmt.Printf("WARN: Error rendering create account page: %s\n", err) 130 139 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 229 238 230 239 session := r.Context().Value("session").(*model.Session) 231 240 232 - type loginData struct { 233 - Session *model.Session 234 - } 235 - 236 241 render := func() { 237 - err := templates.LoginTemplate.Execute(w, loginData{ Session: session }) 242 + err := templates.LoginTemplate.Execute(w, adminPageData{ Path: r.URL.Path, Session: session }) 238 243 if err != nil { 239 244 fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) 240 245 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 345 350 return 346 351 } 347 352 348 - type loginTOTPData struct { 349 - Session *model.Session 350 - } 351 - 352 353 render := func() { 353 - err := templates.LoginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) 354 + err := templates.LoginTOTPTemplate.Execute(w, adminPageData{ Path: r.URL.Path, Session: session }) 354 355 if err != nil { 355 356 fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) 356 357 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+2 -2
admin/logshttp.go
··· 51 51 } 52 52 53 53 type LogsResponse struct { 54 - Session *model.Session 54 + adminPageData 55 55 Logs []*log.Log 56 56 } 57 57 58 58 err = templates.LogsTemplate.Execute(w, LogsResponse{ 59 - Session: session, 59 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 60 60 Logs: logs, 61 61 }) 62 62 if err != nil {
+2 -2
admin/releasehttp.go
··· 57 57 } 58 58 59 59 type ReleaseResponse struct { 60 - Session *model.Session 60 + adminPageData 61 61 Release *model.Release 62 62 } 63 63 64 64 err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{ 65 - Session: session, 65 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 66 66 Release: release, 67 67 }) 68 68 if err != nil {
+99 -34
admin/static/admin.css
··· 3 3 4 4 :root { 5 5 --bg-0: #101010; 6 - --bg-1: #141414; 7 - --bg-2: #181818; 8 - --bg-3: #202020; 6 + --bg-1: #181818; 7 + --bg-2: #282828; 8 + --bg-3: #404040; 9 9 10 10 --fg-0: #b0b0b0; 11 11 --fg-1: #c0c0c0; ··· 67 67 } 68 68 69 69 body { 70 - width: 100%; 70 + width: calc(100% - 180px); 71 71 height: calc(100vh - 1em); 72 72 73 - margin: 0; 73 + margin: 0 0 0 180px; 74 74 padding: 0; 75 + display: flex; 76 + flex-direction: row; 75 77 76 78 font-family: "Inter", sans-serif; 77 79 font-size: 16px; 78 - 79 80 color: var(--fg-0); 80 81 background: var(--bg-0); 82 + 83 + transition: background .1s ease-out, color .1s ease-out; 81 84 } 82 85 83 86 h1, h2, h3, h4, h5, h6 { 84 87 color: var(--fg-3); 85 88 } 86 89 90 + header { 91 + position: fixed; 92 + left: 0; 93 + height: 100vh; 94 + display: flex; 95 + flex-direction: column; 96 + width: 180px; 97 + background-color: var(--bg-1); 98 + box-shadow: var(--shadow-md); 99 + 100 + transition: background .1s ease-out, color .1s ease-out; 101 + } 87 102 nav { 88 - width: min(720px, calc(100% - 2em)); 89 - height: 2em; 90 - margin: 1em auto; 103 + height: 100%; 104 + margin: 1em 0; 91 105 display: flex; 92 - flex-direction: row; 106 + flex-direction: column; 93 107 justify-content: left; 94 - gap: .5em; 95 108 user-select: none; 96 109 } 97 110 nav .icon { 98 - height: 100%; 111 + width: fit-content; 112 + height: fit-content; 113 + padding: 0; 114 + margin: 0 auto 1em auto; 115 + display: flex; 116 + 99 117 border-radius: 100%; 100 118 box-shadow: var(--shadow-sm); 101 - overflow: hidden; 119 + overflow: clip; 102 120 } 103 121 nav .icon img { 104 - width: 100%; 105 - height: 100%; 122 + width: 3em; 123 + height: 3em; 106 124 } 107 125 .nav-item { 108 - width: auto; 109 - height: 100%; 110 126 display: flex; 111 - 112 127 color: var(--fg-2); 113 - background: var(--bg-2); 114 - border-radius: 10em; 115 - box-shadow: var(--shadow-sm); 116 - 117 128 line-height: 2em; 118 129 font-weight: 500; 130 + transition: color .1s, background-color .1s; 119 131 } 120 132 .nav-item:hover { 121 - background: var(--bg-1); 133 + background: var(--bg-2); 122 134 text-decoration: none; 123 135 } 136 + .nav-item.active { 137 + border-left: 4px solid var(--fg-2); 138 + } 139 + .nav-item.active a { 140 + padding-left: calc(1em - 4px); 141 + } 124 142 nav a { 125 - padding: 0 1em; 143 + padding: .2em 1em; 126 144 text-decoration: none; 127 145 color: inherit; 146 + width: 100%; 128 147 } 129 - nav a.icon { 130 - padding: 0; 148 + nav a.active { 149 + border-left: 5px solid var(--fg-0); 150 + padding-left: calc(1em - 5px); 151 + } 152 + nav hr { 153 + width: calc(100% - 2em); 154 + margin: .5em auto; 155 + border: none; 156 + border-bottom: 1px solid var(--fg-0); 131 157 } 132 - nav #logout { 133 - /* margin-left: auto; */ 158 + nav .section-label { 159 + margin: 8px 0 2px 15px; 160 + font-size: 10px; 161 + text-transform: uppercase; 162 + font-weight: 600; 134 163 } 135 164 136 165 main { 137 - width: min(720px, calc(100% - 2em)); 166 + width: min(calc(100% - 16px), 720px); 167 + height: fit-content; 168 + min-height: calc(100vh - 2em); 138 169 margin: 0 auto; 139 170 padding: 1em; 171 + } 172 + main.dashboard { 173 + width: 100%; 140 174 } 141 175 142 176 a { ··· 162 196 } 163 197 164 198 199 + 200 + .cards { 201 + width: 100%; 202 + height: fit-content; 203 + display: flex; 204 + gap: 2em; 205 + flex-wrap: wrap; 206 + } 165 207 166 208 .card { 209 + flex-basis: 40em; 210 + padding: 1em; 211 + background: var(--bg-1); 212 + border-radius: 16px; 213 + box-shadow: var(--shadow-lg); 214 + 215 + transition: background .1s ease-out, color .1s ease-out; 216 + } 217 + main:not(.dashboard) .card { 167 218 margin-bottom: 1em; 168 219 } 169 220 ··· 171 222 margin: 0 0 .5em 0; 172 223 } 173 224 174 - .card-title { 225 + .card-header { 175 226 margin-bottom: 1em; 176 227 display: flex; 177 228 gap: 1em; ··· 179 230 align-items: center; 180 231 justify-content: space-between; 181 232 } 182 - 183 - .card-title h1, 184 - .card-title h2, 185 - .card-title h3 { 233 + .card-header h1, 234 + .card-header h2, 235 + .card-header h3 { 186 236 margin: 0; 187 237 } 238 + .card-header a:hover { 239 + text-decoration: underline; 240 + } 241 + .card-header small { 242 + display: inline-block; 243 + font-size: 15px; 244 + transform: translateY(-2px); 245 + color: var(--fg-0); 246 + } 188 247 189 248 .flex-fill { 190 249 flex-grow: 1; 250 + } 251 + 252 + .artists-group { 253 + display: grid; 254 + grid-template-columns: repeat(5, 1fr); 255 + gap: 1em; 191 256 } 192 257 193 258 @media screen and (max-width: 520px) {
+9 -2
admin/static/edit-artist.css
··· 28 28 } 29 29 .artist-avatar #remove-avatar { 30 30 margin-top: .5em; 31 - padding: .3em .4em; 31 + padding: .3em .6em; 32 32 } 33 33 34 34 .artist-info { ··· 75 75 justify-content: right; 76 76 } 77 77 78 - .card-title a.button { 78 + .card-header a.button { 79 79 text-decoration: none; 80 80 } 81 81 ··· 90 90 border-radius: 16px; 91 91 background: var(--bg-2); 92 92 box-shadow: var(--shadow-md); 93 + 94 + cursor: pointer; 95 + transition: background .1s; 96 + } 97 + 98 + .credit:hover { 99 + background: var(--bg-1); 93 100 } 94 101 95 102 .release-artwork {
+8
admin/static/edit-artist.js
··· 1 + import { hijackClickEvent } from "./admin.js"; 2 + 1 3 const artistID = document.getElementById("artist").dataset.id; 2 4 const nameInput = document.getElementById("name"); 3 5 const avatarImg = document.getElementById("avatar"); ··· 77 79 avatarImg.src = "/img/default-avatar.png" 78 80 saveBtn.disabled = false; 79 81 }); 82 + 83 + document.addEventListener('readystatechange', () => { 84 + document.querySelectorAll('.card#releases .credit').forEach(el => { 85 + hijackClickEvent(el, el.querySelector('.credit-name a')); 86 + }); 87 + });
+34 -23
admin/static/edit-release.css
··· 155 155 gap: .5em; 156 156 } 157 157 158 - .card-title a.button { 158 + .card-header a.button { 159 159 text-decoration: none; 160 160 } 161 161 ··· 163 163 * RELEASE CREDITS 164 164 */ 165 165 166 - .card.credits .credit { 166 + .card#credits .credit { 167 167 margin-bottom: .5em; 168 168 padding: .5em; 169 169 display: flex; ··· 172 172 gap: 1em; 173 173 174 174 border-radius: 16px; 175 - background: var(--bg-2); 175 + background-color: var(--bg-2); 176 176 box-shadow: var(--shadow-md); 177 + 178 + cursor: pointer; 179 + transition: background .1s ease-out; 180 + } 181 + .card#credits .credit:hover { 182 + background-color: var(--bg-1); 177 183 } 178 184 179 - .card.credits .credit p { 185 + .card#credits .credit p { 180 186 margin: 0; 181 187 } 182 188 183 - .card.credits .credit .artist-avatar { 189 + .card#credits .credit .artist-avatar { 184 190 border-radius: 12px; 185 191 } 186 192 187 - .card.credits .credit .artist-name { 193 + .card#credits .credit .artist-name { 188 194 color: var(--fg-3); 189 195 font-weight: bold; 190 196 } 191 197 192 - .card.credits .credit .artist-role small { 198 + .card#credits .credit .artist-role small { 193 199 font-size: inherit; 194 200 opacity: .66; 195 201 } ··· 308 314 * RELEASE LINKS 309 315 */ 310 316 311 - .card.links { 317 + .card#links ul { 318 + padding: 0; 312 319 display: flex; 313 320 gap: .2em; 314 321 } 315 322 316 - .card.links a.button:hover { 323 + .card#links a.button:hover { 317 324 color: var(--bg-3) !important; 318 325 background-color: var(--fg-3) !important; 319 326 } 320 327 321 - .card.links a.button[data-name="spotify"] { 328 + .card#links a.button[data-name="spotify"] { 322 329 color: #101010; 323 330 background-color: #8cff83 324 331 } 325 332 326 - .card.links a.button[data-name="apple music"] { 333 + .card#links a.button[data-name="apple music"] { 327 334 color: #101010; 328 335 background-color: #8cd9ff 329 336 } 330 337 331 - .card.links a.button[data-name="soundcloud"] { 338 + .card#links a.button[data-name="soundcloud"] { 332 339 color: #101010; 333 340 background-color: #fdaa6d 334 341 } 335 342 336 - .card.links a.button[data-name="youtube"] { 343 + .card#links a.button[data-name="youtube"] { 337 344 color: #101010; 338 345 background-color: #ff6e6e 339 346 } ··· 421 428 * RELEASE TRACKS 422 429 */ 423 430 424 - .card.tracks .track { 431 + .card#tracks .track { 425 432 margin-bottom: 1em; 426 433 padding: 1em; 427 434 display: flex; ··· 433 440 box-shadow: var(--shadow-md); 434 441 } 435 442 436 - .card.tracks .track h3, 437 - .card.tracks .track p { 443 + .card#tracks .track h3, 444 + .card#tracks .track p { 438 445 margin: 0; 439 446 } 440 447 441 - .card.tracks h2.track-title { 448 + .card#tracks h2.track-title { 442 449 margin: 0; 443 450 display: flex; 444 451 gap: .5em; 445 452 } 446 453 447 - .card.tracks h2.track-title .track-number { 454 + .card#tracks h2.track-title .track-number { 448 455 opacity: .5; 449 456 } 450 457 451 - .card.tracks .track-album { 458 + .card#tracks a:hover { 459 + text-decoration: underline; 460 + } 461 + 462 + .card#tracks .track-album { 452 463 margin-left: auto; 453 464 font-style: italic; 454 465 font-size: .75em; 455 466 opacity: .5; 456 467 } 457 468 458 - .card.tracks .track-album.empty { 469 + .card#tracks .track-album.empty { 459 470 color: #ff2020; 460 471 opacity: 1; 461 472 } 462 473 463 - .card.tracks .track-description { 474 + .card#tracks .track-description { 464 475 font-style: italic; 465 476 } 466 477 467 - .card.tracks .track-lyrics { 478 + .card#tracks .track-lyrics { 468 479 max-height: 10em; 469 480 overflow-y: scroll; 470 481 } 471 482 472 - .card.tracks .track .empty { 483 + .card#tracks .track .empty { 473 484 opacity: 0.75; 474 485 } 475 486
+8
admin/static/edit-release.js
··· 1 + import { hijackClickEvent } from "./admin.js"; 2 + 1 3 const releaseID = document.getElementById("release").dataset.id; 2 4 const titleInput = document.getElementById("title"); 3 5 const artworkImg = document.getElementById("artwork"); ··· 96 98 artworkData = ""; 97 99 saveBtn.disabled = false; 98 100 }); 101 + 102 + document.addEventListener("readystatechange", () => { 103 + document.querySelectorAll(".card#credits .credit").forEach(el => { 104 + hijackClickEvent(el, el.querySelector(".artist-name a")); 105 + }); 106 + });
+6 -9
admin/static/index.css
··· 1 1 @import url("/admin/static/release-list-item.css"); 2 2 3 3 .artist { 4 - margin-bottom: .5em; 5 4 padding: .5em; 6 - display: flex; 7 - flex-direction: row; 8 - align-items: center; 9 - gap: .5em; 10 5 11 6 color: var(--fg-3); 12 7 background: var(--bg-2); 13 8 box-shadow: var(--shadow-md); 14 9 border-radius: 16px; 10 + text-align: center; 15 11 16 - transition: background .1s ease-out; 17 12 cursor: pointer; 13 + transition: background .1s ease-out, color .1s ease-out; 18 14 } 19 15 20 16 .artist:hover { ··· 23 19 } 24 20 25 21 .artist-avatar { 26 - width: 32px; 27 - height: 32px; 22 + width: 100%; 28 23 object-fit: cover; 29 - border-radius: 100%; 24 + border-radius: 8px; 30 25 } 31 26 32 27 .track { ··· 39 34 border-radius: 8px; 40 35 background: #f8f8f8f8; 41 36 border: 1px solid #808080; 37 + 38 + transition: background .1s ease-out, color .1s ease-out; 42 39 } 43 40 44 41 .track p {
+1 -1
admin/static/index.js
··· 76 76 }); 77 77 78 78 document.addEventListener("readystatechange", () => { 79 - document.querySelectorAll(".card.artists .artist").forEach(el => { 79 + document.querySelectorAll("#artists .artist").forEach(el => { 80 80 hijackClickEvent(el, el.querySelector("a.artist-name")) 81 81 }); 82 82 });
+2
admin/static/release-list-item.css
··· 8 8 border-radius: 16px; 9 9 background: var(--bg-2); 10 10 box-shadow: var(--shadow-md); 11 + 12 + transition: background .1s ease-out, color .1s ease-out; 11 13 } 12 14 13 15 .release h3,
+9 -9
admin/templates/html/edit-account.html
··· 14 14 {{end}} 15 15 <h1>Account Settings ({{.Session.Account.Username}})</h1> 16 16 17 - <div class="card-title"> 18 - <h2>Change Password</h2> 19 - </div> 20 17 <div class="card"> 18 + <div class="card-header"> 19 + <h2>Change Password</h2> 20 + </div> 21 21 <form action="/admin/account/password" method="POST" id="change-password"> 22 22 <label for="current-password">Current Password</label> 23 23 <input type="password" id="current-password" name="current-password" value="" autocomplete="current-password" required> ··· 32 32 </form> 33 33 </div> 34 34 35 - <div class="card-title"> 36 - <h2>MFA Devices</h2> 37 - </div> 38 35 <div class="card mfa-devices"> 36 + <div class="card-header"> 37 + <h2>MFA Devices</h2> 38 + </div> 39 39 {{if .TOTPs}} 40 40 {{range .TOTPs}} 41 41 <div class="mfa-device"> ··· 58 58 </div> 59 59 </div> 60 60 61 - <div class="card-title"> 62 - <h2>Danger Zone</h2> 63 - </div> 64 61 <div class="card danger"> 62 + <div class="card-header"> 63 + <h2>Danger Zone</h2> 64 + </div> 65 65 <p> 66 66 Clicking the button below will delete your account. 67 67 This action is <strong>irreversible</strong>.
+8 -8
admin/templates/html/edit-artist.html
··· 29 29 </div> 30 30 </div> 31 31 32 - <div class="card-title"> 33 - <h2>Featured in</h2> 34 - </div> 35 - <div class="card releases"> 32 + <div class="card" id="releases"> 33 + <div class="card-header"> 34 + <h2>Featured in</h2> 35 + </div> 36 36 {{if .Credits}} 37 37 {{range .Credits}} 38 38 <div class="credit"> ··· 54 54 {{end}} 55 55 </div> 56 56 57 - <div class="card-title"> 58 - <h2>Danger Zone</h2> 59 - </div> 60 - <div class="card danger"> 57 + <div class="card" id="danger"> 58 + <div class="card-header"> 59 + <h2>Danger Zone</h2> 60 + </div> 61 61 <p> 62 62 Clicking the button below will delete this artist. 63 63 This action is <strong>irreversible</strong>.
+39 -37
admin/templates/html/edit-release.html
··· 97 97 </div> 98 98 </div> 99 99 100 - <div class="card-title"> 101 - <h2>Credits ({{len .Release.Credits}})</h2> 102 - <a class="button edit" 103 - href="/admin/release/{{.Release.ID}}/editcredits" 104 - hx-get="/admin/release/{{.Release.ID}}/editcredits" 105 - hx-target="body" 106 - hx-swap="beforeend" 107 - >Edit</a> 108 - </div> 109 - <div class="card credits"> 100 + <div class="card" id="credits"> 101 + <div class="card-header"> 102 + <h2>Credits ({{len .Release.Credits}})</h2> 103 + <a class="button edit" 104 + href="/admin/release/{{.Release.ID}}/editcredits" 105 + hx-get="/admin/release/{{.Release.ID}}/editcredits" 106 + hx-target="body" 107 + hx-swap="beforeend" 108 + >Edit</a> 109 + </div> 110 110 {{range .Release.Credits}} 111 111 <div class="credit"> 112 112 <img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> ··· 126 126 {{end}} 127 127 </div> 128 128 129 - <div class="card-title"> 130 - <h2>Links ({{len .Release.Links}})</h2> 131 - <a class="button edit" 132 - href="/admin/release/{{.Release.ID}}/editlinks" 133 - hx-get="/admin/release/{{.Release.ID}}/editlinks" 134 - hx-target="body" 135 - hx-swap="beforeend" 136 - >Edit</a> 137 - </div> 138 - <div class="card links"> 139 - {{range .Release.Links}} 140 - <a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a> 141 - {{end}} 129 + <div class="card" id="links"> 130 + <div class="card-header"> 131 + <h2>Links ({{len .Release.Links}})</h2> 132 + <a class="button edit" 133 + href="/admin/release/{{.Release.ID}}/editlinks" 134 + hx-get="/admin/release/{{.Release.ID}}/editlinks" 135 + hx-target="body" 136 + hx-swap="beforeend" 137 + >Edit</a> 138 + </div> 139 + <ul> 140 + {{range .Release.Links}} 141 + <a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a> 142 + {{end}} 143 + </ul> 142 144 </div> 143 145 144 - <div class="card-title" id="tracks"> 145 - <h2>Tracklist ({{len .Release.Tracks}})</h2> 146 - <a class="button edit" 147 - href="/admin/release/{{.Release.ID}}/edittracks" 148 - hx-get="/admin/release/{{.Release.ID}}/edittracks" 149 - hx-target="body" 150 - hx-swap="beforeend" 151 - >Edit</a> 152 - </div> 153 - <div class="card tracks"> 146 + <div class="card" id="tracks"> 147 + <div class="card-header" id="tracks"> 148 + <h2>Tracklist ({{len .Release.Tracks}})</h2> 149 + <a class="button edit" 150 + href="/admin/release/{{.Release.ID}}/edittracks" 151 + hx-get="/admin/release/{{.Release.ID}}/edittracks" 152 + hx-target="body" 153 + hx-swap="beforeend" 154 + >Edit</a> 155 + </div> 154 156 {{range $i, $track := .Release.Tracks}} 155 157 <div class="track" data-id="{{$track.ID}}"> 156 158 <h2 class="track-title"> ··· 175 177 {{end}} 176 178 </div> 177 179 178 - <div class="card-title"> 179 - <h2>Danger Zone</h2> 180 - </div> 181 - <div class="card danger"> 180 + <div class="card" id="danger"> 181 + <div class="card-header"> 182 + <h2>Danger Zone</h2> 183 + </div> 182 184 <p> 183 185 Clicking the button below will delete this release. 184 186 This action is <strong>irreversible</strong>.
+6 -6
admin/templates/html/edit-track.html
··· 39 39 </div> 40 40 </div> 41 41 42 - <div class="card-title"> 43 - <h2>Featured in</h2> 44 - </div> 45 42 <div class="card releases"> 43 + <div class="card-header"> 44 + <h2>Featured in</h2> 45 + </div> 46 46 {{if .Releases}} 47 47 {{range .Releases}} 48 48 {{block "release" .}}{{end}} ··· 52 52 {{end}} 53 53 </div> 54 54 55 - <div class="card-title"> 56 - <h2>Danger Zone</h2> 57 - </div> 58 55 <div class="card danger"> 56 + <div class="card-header"> 57 + <h2>Danger Zone</h2> 58 + </div> 59 59 <p> 60 60 Clicking the button below will delete this track. 61 61 This action is <strong>irreversible</strong>.
+59 -53
admin/templates/html/index.html
··· 5 5 {{end}} 6 6 7 7 {{define "content"}} 8 - <main> 9 - 10 - <div class="card-title"> 11 - <h1>Releases</h1> 12 - <a class="button new" id="create-release">Create New</a> 13 - </div> 14 - <div class="card releases"> 15 - {{range .Releases}} 16 - {{block "release" .}}{{end}} 17 - {{end}} 18 - {{if not .Releases}} 19 - <p>There are no releases.</p> 20 - {{end}} 21 - </div> 8 + <main class="dashboard"> 9 + <h1>Dashboard</h1> 22 10 23 - <div class="card-title"> 24 - <h1>Artists</h1> 25 - <a class="button new" id="create-artist">Create New</a> 26 - </div> 27 - <div class="card artists"> 28 - {{range $Artist := .Artists}} 29 - <div class="artist"> 30 - <img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 31 - <a href="/admin/artist/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> 32 - </div> 33 - {{end}} 34 - {{if not .Artists}} 35 - <p>There are no artists.</p> 36 - {{end}} 37 - </div> 11 + <div class="cards"> 12 + <div class="card" id="releases"> 13 + <div class="card-header"> 14 + <h2><a href="/admin/releases/">Releases</a> <small>({{.ReleaseCount}} total)</small></h2> 15 + <a class="button new" id="create-release">Create New</a> 16 + </div> 17 + {{range .Releases}} 18 + {{block "release" .}}{{end}} 19 + {{end}} 20 + {{if not .Releases}} 21 + <p>There are no releases.</p> 22 + {{end}} 23 + </div> 38 24 39 - <div class="card-title"> 40 - <h1>Tracks</h1> 41 - <a class="button new" id="create-track">Create New</a> 42 - </div> 43 - <div class="card tracks"> 44 - <p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p> 45 - <br> 46 - {{range $Track := .Tracks}} 47 - <div class="track"> 48 - <h2 class="track-title"> 49 - <a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a> 50 - </h2> 51 - {{if $Track.Description}} 52 - <p class="track-description">{{$Track.GetDescriptionHTML}}</p> 25 + <div class="card" id="artists"> 26 + <div class="card-header"> 27 + <h2><a href="/admin/artists/">Artists</a></h2> 28 + <a class="button new" id="create-artist">Create New</a> 29 + </div> 30 + {{if .Artists}} 31 + <div class="artists-group"> 32 + {{range $Artist := .Artists}} 33 + <div class="artist"> 34 + <img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar"> 35 + <a href="/admin/artist/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a> 36 + </div> 37 + {{end}} 38 + </div> 53 39 {{else}} 54 - <p class="track-description empty">No description provided.</p> 40 + <p>There are no artists.</p> 55 41 {{end}} 56 - {{if $Track.Lyrics}} 57 - <p class="track-lyrics">{{$Track.GetLyricsHTML}}</p> 58 - {{else}} 59 - <p class="track-lyrics empty">There are no lyrics.</p> 42 + </div> 43 + 44 + <div class="card" id="tracks"> 45 + <div class="card-header"> 46 + <h2><a href="/admin/tracks/">Tracks</a></h2> 47 + <a class="button new" id="create-track">Create New</a> 48 + </div> 49 + <p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p> 50 + <br> 51 + {{range $Track := .Tracks}} 52 + <div class="track"> 53 + <h2 class="track-title"> 54 + <a href="/admin/track/{{$Track.ID}}">{{$Track.Title}}</a> 55 + </h2> 56 + {{if $Track.Description}} 57 + <p class="track-description">{{$Track.GetDescriptionHTML}}</p> 58 + {{else}} 59 + <p class="track-description empty">No description provided.</p> 60 + {{end}} 61 + {{if $Track.Lyrics}} 62 + <p class="track-lyrics">{{$Track.GetLyricsHTML}}</p> 63 + {{else}} 64 + <p class="track-lyrics empty">There are no lyrics.</p> 65 + {{end}} 66 + </div> 60 67 {{end}} 61 - </div> 62 - {{end}} 63 - {{if not .Artists}} 64 - <p>There are no artists.</p> 65 - {{end}} 68 + {{if not .Artists}} 69 + <p>There are no artists.</p> 70 + {{end}} 71 + </div> 66 72 </div> 67 73 68 74 </main>
+19 -5
admin/templates/html/layout.html
··· 17 17 <header> 18 18 <nav> 19 19 <a href="/" class="nav icon" aria-label="ari melody" title="Return to Home"> 20 - <img src="/img/favicon.png" alt=""> 20 + <img src="/img/favicon.png" alt="" width="64" height="64"> 21 21 </a> 22 - <div class="nav-item"> 22 + <div class="nav-item{{if eq .Path "/"}} active{{end}}"> 23 23 <a href="/admin">home</a> 24 24 </div> 25 25 {{if .Session.Account}} 26 - <div class="nav-item"> 26 + <div class="nav-item{{if eq .Path "/logs"}} active{{end}}"> 27 27 <a href="/admin/logs">logs</a> 28 28 </div> 29 + <hr> 30 + <p class="section-label">music</p> 31 + <div class="nav-item{{if eq .Path "/releases"}} active{{end}}"> 32 + <a href="/admin/releases">releases</a> 33 + </div> 34 + <div class="nav-item{{if eq .Path "/artists"}} active{{end}}"> 35 + <a href="/admin/artists">artists</a> 36 + </div> 37 + <div class="nav-item{{if eq .Path "/tracks"}} active{{end}}"> 38 + <a href="/admin/tracks">tracks</a> 39 + </div> 29 40 {{end}} 30 41 31 42 <div class="flex-fill"></div> 32 43 33 44 {{if .Session.Account}} 34 - <div class="nav-item"> 45 + <div class="nav-item{{if eq .Path "/account"}} active{{end}}"> 35 46 <a href="/admin/account">account ({{.Session.Account.Username}})</a> 36 47 </div> 37 48 <div class="nav-item"> 38 49 <a href="/admin/logout" id="logout">log out</a> 39 50 </div> 40 51 {{else}} 41 - <div class="nav-item"> 52 + <div class="nav-item{{if eq .Path "/login"}} active{{end}}"> 53 + <a href="/admin/login" id="login">log in</a> 54 + </div> 55 + <div class="nav-item{{if eq .Path "/register"}} active{{end}}"> 42 56 <a href="/admin/register" id="register">create account</a> 43 57 </div> 44 58 {{end}}
+2 -2
admin/trackhttp.go
··· 33 33 } 34 34 35 35 type TrackResponse struct { 36 - Session *model.Session 36 + adminPageData 37 37 Track *model.Track 38 38 Releases []*model.Release 39 39 } ··· 41 41 session := r.Context().Value("session").(*model.Session) 42 42 43 43 err = templates.EditTrackTemplate.Execute(w, TrackResponse{ 44 - Session: session, 44 + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, 45 45 Track: track, 46 46 Releases: releases, 47 47 })
+11
controller/release.go
··· 99 99 100 100 return releases, nil 101 101 } 102 + func GetReleasesCount(db *sqlx.DB, onlyVisible bool) (int, error) { 103 + query := "SELECT count(*) FROM musicrelease" 104 + if onlyVisible { 105 + query += " WHERE visible=true" 106 + } 107 + 108 + var count int 109 + err := db.Get(&count, query) 110 + 111 + return count, err 112 + } 102 113 103 114 func CreateRelease(db *sqlx.DB, release *model.Release) error { 104 115 _, err := db.Exec(