diff --git a/.air.toml b/.air.toml index c6d499b..070166a 100644 --- a/.air.toml +++ b/.air.toml @@ -7,14 +7,14 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["uploads", "test", "db", "res"] + exclude_dir = ["admin/static", "admin\\static", "public", "uploads", "test", "db", "res"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false full_bin = "" include_dir = [] - include_ext = ["go", "tpl", "tmpl", "html", "css"] + include_ext = ["go", "tpl", "tmpl", "html"] include_file = [] kill_delay = "0s" log = "build-errors.log" diff --git a/.forgejo/workflows/push-prod.yaml b/.forgejo/workflows/push-prod.yaml deleted file mode 100644 index 7a9fd05..0000000 --- a/.forgejo/workflows/push-prod.yaml +++ /dev/null @@ -1,50 +0,0 @@ -on: - push: - branches: - - main - -env: - EXEC: arimelody-web - REMOTE: ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} - PORT: ${{ secrets.SSH_PORT }} - -jobs: - deploy: - runs-on: docker - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '^1.25.1' - - - name: Run tests - run: go test -v ./model - - - name: Build binary - run: make build - - - name: Bundle tarball - run: make bundle - - - name: Set up SSH keys - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - - name: Copy to production server - run: | - ssh-keyscan -p $PORT ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts - scp -P $PORT ./$EXEC.tar.gz $REMOTE:~/ - - - name: Restart production - run: | - ssh -o StrictHostKeyChecking=no $REMOTE -p $PORT << EOT - cd ${{ secrets.DEPLOY_DIR }} - tar xzf ~/$EXEC.tar.gz - /bin/bash ~/restart.sh - rm ~/$EXEC.tar.gz - EOT - diff --git a/Makefile b/Makefile index 98a4ea1..11e565a 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,11 @@ EXEC = arimelody-web .PHONY: $(EXEC) -build: +$(EXEC): GOOS=linux GOARCH=amd64 go build -o $(EXEC) -bundle: build - tar czf $(EXEC).tar.gz --exclude ".DS_Store" $(EXEC) admin/static/ public/ +bundle: $(EXEC) + tar czf $(EXEC).tar.gz $(EXEC) admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/ clean: rm $(EXEC) $(EXEC).tar.gz diff --git a/README.md b/README.md index f1fd392..464379e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ -# ari melody website +# arimelody.me home to your local SPACEGIRL! 💫 --- -a slightly-overcomplicated webserver built to show off everything i've worked -on, and then some! this server comes complete with twitch live status tracking, -a portfolio database, and a full-fledged admin CMS panel to manage it all! +built up from the initial [static](https://git.arimelody.me/ari/arimelody.me-static) +branch, this powerful, server-side rendered version comes complete with live +updates, powered by a new database and handy admin panel! + +the admin panel currently facilitates live updating of my music discography, +though i plan to expand it towards art portfolio and blog posts in the future. +if all goes well, i'd like to later separate these components into their own +library for others to use in their own sites. exciting stuff! ## build diff --git a/admin/accounthttp.go b/admin/accounthttp.go index 113a17a..945a507 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -1,29 +1,28 @@ package admin import ( - "database/sql" - "fmt" - "net/http" - "net/url" - "os" + "database/sql" + "fmt" + "net/http" + "net/url" + "os" - "arimelody-web/admin/templates" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) func accountHandler(app *model.AppState) http.Handler { mux := http.NewServeMux() - mux.Handle("/account/totp-setup", totpSetupHandler(app)) - mux.Handle("/account/totp-confirm", totpConfirmHandler(app)) - mux.Handle("/account/totp-delete", totpDeleteHandler(app)) + mux.Handle("/totp-setup", totpSetupHandler(app)) + mux.Handle("/totp-confirm", totpConfirmHandler(app)) + mux.Handle("/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app))) - mux.Handle("/account/password", changePasswordHandler(app)) - mux.Handle("/account/delete", deleteAccountHandler(app)) + mux.Handle("/password", changePasswordHandler(app)) + mux.Handle("/delete", deleteAccountHandler(app)) return mux } @@ -45,7 +44,7 @@ func accountIndexHandler(app *model.AppState) http.Handler { } accountResponse struct { - adminPageData + Session *model.Session TOTPs []TOTP } ) @@ -65,8 +64,8 @@ func accountIndexHandler(app *model.AppState) http.Handler { session.Message = sessionMessage session.Error = sessionError - err = templates.AccountTemplate.Execute(w, accountResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + err = accountTemplate.Execute(w, accountResponse{ + Session: session, TOTPs: totps, }) if err != nil { @@ -170,7 +169,7 @@ func deleteAccountHandler(app *model.AppState) http.Handler { } type totpConfirmData struct { - adminPageData + Session *model.Session TOTP *model.TOTP NameEscaped string QRBase64Image string @@ -179,9 +178,13 @@ type totpConfirmData struct { func totpSetupHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { + type totpSetupData struct { + Session *model.Session + } + session := r.Context().Value("session").(*model.Session) - err := templates.TOTPSetupTemplate.Execute(w, adminPageData{ Path: "/account", Session: session }) + err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -218,9 +221,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, - }) + err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -234,8 +235,8 @@ func totpSetupHandler(app *model.AppState) http.Handler { fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) } - err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + err = totpConfirmTemplate.Execute(w, totpConfirmData{ + Session: session, TOTP: &totp, NameEscaped: url.PathEscape(totp.Name), QRBase64Image: qrBase64Image, @@ -266,6 +267,11 @@ func totpConfirmHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + code := r.FormValue("totp") + if len(code) != controller.TOTP_CODE_LENGTH { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } totp, err := controller.GetTOTP(app.DB, session.Account.ID, name) if err != nil { @@ -285,22 +291,23 @@ func totpConfirmHandler(app *model.AppState) http.Handler { fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) } - code := r.FormValue("totp") confirmCode := controller.GenerateTOTP(totp.Secret, 0) - confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) - if len(code) != controller.TOTP_CODE_LENGTH || (code != confirmCode && code != confirmCodeOffset) { - session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } - err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, - TOTP: totp, - NameEscaped: url.PathEscape(totp.Name), - QRBase64Image: qrBase64Image, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + if code != confirmCode { + confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) + if code != confirmCodeOffset { + session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } + err = totpConfirmTemplate.Execute(w, totpConfirmData{ + Session: session, + TOTP: totp, + NameEscaped: url.PathEscape(totp.Name), + QRBase64Image: qrBase64Image, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return } - return } err = controller.ConfirmTOTP(app.DB, session.Account.ID, name) @@ -321,23 +328,18 @@ func totpConfirmHandler(app *model.AppState) http.Handler { func totpDeleteHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { + if r.Method != http.MethodGet { http.NotFound(w, r) return } - session := r.Context().Value("session").(*model.Session) + if len(r.URL.Path) < 2 { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + name := r.URL.Path[1:] - err := r.ParseForm() - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - name := r.FormValue("totp-name") - if len(name) == 0 { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } + session := r.Context().Value("session").(*model.Session) totp, err := controller.GetTOTP(app.DB, session.Account.ID, name) if err != nil { diff --git a/admin/artisthttp.go b/admin/artisthttp.go index f151ddd..9fa6bb2 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -1,86 +1,53 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/admin/templates" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/model" + "arimelody-web/controller" ) -func serveArtists(app *model.AppState) http.Handler { +func serveArtist(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/artists")[1:], "/") - artistID := slices[0] - - if len(artistID) > 0 { - serveArtist(app, artistID).ServeHTTP(w, r) - return - } - - artists, err := controller.GetAllArtists(app.DB) - if err != nil { - fmt.Printf("WARN: Failed to fetch artists: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - type ArtistsResponse struct { - adminPageData - Artists []*model.Artist - } - - err = templates.ArtistsTemplate.Execute(w, ArtistsResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, - Artists: artists, - }) - if err != nil { - fmt.Printf("WARN: Failed to serve admin artists page: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func serveArtist(app *model.AppState, artistID string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - artist, err := controller.GetArtist(app.DB, artistID) + slices := strings.Split(r.URL.Path[1:], "/") + id := slices[0] + artist, err := controller.GetArtist(app.DB, id) if err != nil { if artist == nil { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err) + fmt.Printf("Error rendering admin artist page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } credits, err := controller.GetArtistCredits(app.DB, artist.ID, true) if err != nil { - fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err) + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } type ArtistResponse struct { - adminPageData + Session *model.Session Artist *model.Artist Credits []*model.Credit } - err = templates.EditArtistTemplate.Execute(w, ArtistResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + session := r.Context().Value("session").(*model.Session) + + err = artistTemplate.Execute(w, ArtistResponse{ + Session: session, Artist: artist, Credits: credits, }) if err != nil { - fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err) + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } + diff --git a/admin/templates/html/components/credit/addcredit.html b/admin/components/credits/addcredit.html similarity index 94% rename from admin/templates/html/components/credit/addcredit.html rename to admin/components/credits/addcredit.html index 082ba17..c09a550 100644 --- a/admin/templates/html/components/credit/addcredit.html +++ b/admin/components/credits/addcredit.html @@ -7,7 +7,7 @@ {{range $Artist := .Artists}}
  • diff --git a/admin/templates/html/components/credit/editcredits.html b/admin/components/credits/editcredits.html similarity index 97% rename from admin/templates/html/components/credit/editcredits.html rename to admin/components/credits/editcredits.html index 38132e2..94dc268 100644 --- a/admin/templates/html/components/credit/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -3,8 +3,8 @@

    Editing: Credits

    Add diff --git a/admin/templates/html/components/credit/newcredit.html b/admin/components/credits/newcredit.html similarity index 100% rename from admin/templates/html/components/credit/newcredit.html rename to admin/components/credits/newcredit.html diff --git a/admin/templates/html/components/link/editlinks.html b/admin/components/links/editlinks.html similarity index 100% rename from admin/templates/html/components/link/editlinks.html rename to admin/components/links/editlinks.html diff --git a/admin/templates/html/components/release/release.html b/admin/components/release/release-list-item.html similarity index 75% rename from admin/templates/html/components/release/release.html rename to admin/components/release/release-list-item.html index 7a5059d..4b8f41e 100644 --- a/admin/templates/html/components/release/release.html +++ b/admin/components/release/release-list-item.html @@ -5,7 +5,7 @@

    - {{.Title}} + {{.Title}} {{.ReleaseDate.Year}} {{if not .Visible}}(hidden){{end}} @@ -13,9 +13,9 @@

    {{.PrintArtists true true}}

    {{.ReleaseType}} - ({{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}})

    + ({{len .Tracks}} track{{if not (eq (len .Tracks) 1)}}s{{end}})

    diff --git a/admin/templates/html/components/track/addtrack.html b/admin/components/tracks/addtrack.html similarity index 94% rename from admin/templates/html/components/track/addtrack.html rename to admin/components/tracks/addtrack.html index 6a2360b..a6dd433 100644 --- a/admin/templates/html/components/track/addtrack.html +++ b/admin/components/tracks/addtrack.html @@ -8,7 +8,7 @@
  • diff --git a/admin/templates/html/components/track/edittracks.html b/admin/components/tracks/edittracks.html similarity index 97% rename from admin/templates/html/components/track/edittracks.html rename to admin/components/tracks/edittracks.html index c06f0c3..d03f80a 100644 --- a/admin/templates/html/components/track/edittracks.html +++ b/admin/components/tracks/edittracks.html @@ -3,8 +3,8 @@

    Editing: Tracks

    Add diff --git a/admin/templates/html/components/track/newtrack.html b/admin/components/tracks/newtrack.html similarity index 100% rename from admin/templates/html/components/track/newtrack.html rename to admin/components/tracks/newtrack.html diff --git a/admin/http.go b/admin/http.go index 2a6b4ae..245a152 100644 --- a/admin/http.go +++ b/admin/http.go @@ -1,28 +1,22 @@ package admin import ( - "context" - "database/sql" - "fmt" - "net/http" - "os" - "strings" - "time" + "context" + "database/sql" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" - "arimelody-web/admin/templates" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" - "arimelody-web/view" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) -type adminPageData struct { - Path string - Session *model.Session -} - func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() @@ -44,30 +38,15 @@ func Handler(app *model.AppState) http.Handler { mux.Handle("/register", registerAccountHandler(app)) mux.Handle("/account", requireAccount(accountIndexHandler(app))) - mux.Handle("/account/", requireAccount(accountHandler(app))) + mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app)))) mux.Handle("/logs", requireAccount(logsHandler(app))) - mux.Handle("/releases", requireAccount(serveReleases(app))) - mux.Handle("/releases/", requireAccount(serveReleases(app))) - mux.Handle("/artists", requireAccount(serveArtists(app))) - mux.Handle("/artists/", requireAccount(serveArtists(app))) - mux.Handle("/tracks", requireAccount(serveTracks(app))) - mux.Handle("/tracks/", requireAccount(serveTracks(app))) + mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app)))) + mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app)))) + mux.Handle("/track/", requireAccount(http.StripPrefix("/track", serveTrack(app)))) - mux.Handle("/static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/static/admin.css" { - http.ServeFile(w, r, "./admin/static/admin.css") - return - } - if r.URL.Path == "/static/admin.js" { - http.ServeFile(w, r, "./admin/static/admin.js") - return - } - requireAccount( - http.StripPrefix("/static", - view.ServeFiles("./admin/static"))).ServeHTTP(w, r) - })) + mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/", requireAccount(AdminIndexHandler(app))) @@ -84,18 +63,12 @@ func AdminIndexHandler(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - releases, err := controller.GetAllReleases(app.DB, false, 3, true) + releases, err := controller.GetAllReleases(app.DB, false, 0, true) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - releaseCount, err := controller.GetReleaseCount(app.DB, false) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } artists, err := controller.GetAllArtists(app.DB) if err != nil { @@ -103,12 +76,6 @@ func AdminIndexHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - artistCount, err := controller.GetArtistCount(app.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } tracks, err := controller.GetOrphanTracks(app.DB) if err != nil { @@ -116,31 +83,19 @@ func AdminIndexHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - trackCount, err := controller.GetTrackCount(app.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } type IndexData struct { - adminPageData - Releases []*model.Release - ReleaseCount int - Artists []*model.Artist - ArtistCount int - Tracks []*model.Track - TrackCount int + Session *model.Session + Releases []*model.Release + Artists []*model.Artist + Tracks []*model.Track } - err = templates.IndexTemplate.Execute(w, IndexData{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + err = indexTemplate.Execute(w, IndexData{ + Session: session, Releases: releases, - ReleaseCount: releaseCount, Artists: artists, - ArtistCount: artistCount, Tracks: tracks, - TrackCount: trackCount, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) @@ -160,8 +115,12 @@ func registerAccountHandler(app *model.AppState) http.Handler { return } + type registerData struct { + Session *model.Session + } + render := func() { - err := templates.RegisterTemplate.Execute(w, adminPageData{ Path: r.URL.Path, Session: session }) + err := registerTemplate.Execute(w, registerData{ Session: session }) if err != nil { fmt.Printf("WARN: Error rendering create account page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -266,8 +225,12 @@ func loginHandler(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) + type loginData struct { + Session *model.Session + } + render := func() { - err := templates.LoginTemplate.Execute(w, adminPageData{ Path: r.URL.Path, Session: session }) + err := loginTemplate.Execute(w, loginData{ Session: session }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -378,8 +341,12 @@ func loginTOTPHandler(app *model.AppState) http.Handler { return } + type loginTOTPData struct { + Session *model.Session + } + render := func() { - err := templates.LoginTOTPTemplate.Execute(w, adminPageData{ Path: r.URL.Path, Session: session }) + err := loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -473,7 +440,7 @@ func logoutHandler(app *model.AppState) http.Handler { Path: "/", }) - err = templates.LogoutTemplate.Execute(w, nil) + err = logoutTemplate.Execute(w, nil) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -493,26 +460,26 @@ func requireAccount(next http.Handler) http.HandlerFunc { }) } -/* -//go:embed "static" -var staticFS embed.FS - func staticHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - uri := strings.TrimPrefix(r.URL.Path, "/static") - file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(uri))) + info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path))) + // does the file exist? if err != nil { + if os.IsNotExist(err) { + http.NotFound(w, r) + return + } + } + + // is thjs a directory? (forbidden) + if info.IsDir() { http.NotFound(w, r) return } - w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path))) - w.WriteHeader(http.StatusOK) - - w.Write(file) + http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r) }) } -*/ func enforceSession(app *model.AppState, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/admin/logshttp.go b/admin/logshttp.go index a6d8e40..7249b16 100644 --- a/admin/logshttp.go +++ b/admin/logshttp.go @@ -1,13 +1,12 @@ package admin import ( - "arimelody-web/admin/templates" - "arimelody-web/log" - "arimelody-web/model" - "fmt" - "net/http" - "os" - "strings" + "arimelody-web/log" + "arimelody-web/model" + "fmt" + "net/http" + "os" + "strings" ) func logsHandler(app *model.AppState) http.Handler { @@ -51,12 +50,12 @@ func logsHandler(app *model.AppState) http.Handler { } type LogsResponse struct { - adminPageData + Session *model.Session Logs []*log.Log } - err = templates.LogsTemplate.Execute(w, LogsResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + err = logsTemplate.Execute(w, LogsResponse{ + Session: session, Logs: logs, }) if err != nil { diff --git a/admin/releasehttp.go b/admin/releasehttp.go index 7cca841..c6b68ab 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -1,62 +1,19 @@ package admin import ( - "fmt" - "net/http" - "os" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/admin/templates" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/model" ) -func serveReleases(app *model.AppState) http.Handler { +func serveRelease(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/releases")[1:], "/") + slices := strings.Split(r.URL.Path[1:], "/") releaseID := slices[0] - var action string = "" - if len(slices) > 1 { - action = slices[1] - } - - if len(releaseID) > 0 { - serveRelease(app, releaseID, action).ServeHTTP(w, r) - return - } - - type ReleasesData struct { - adminPageData - Releases []*model.Release - } - - releases, err := controller.GetAllReleases(app.DB, false, 0, true) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = templates.ReleasesTemplate.Execute(w, ReleasesData{ - adminPageData: adminPageData{ - Path: r.URL.Path, - Session: session, - }, - Releases: releases, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to serve releases page: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) -} - -func serveRelease(app *model.AppState, releaseID string, action string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*model.Session) release, err := controller.GetRelease(app.DB, releaseID, true) @@ -65,13 +22,13 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to fetch full release data for %s: %s\n", releaseID, err) + fmt.Printf("WARN: Failed to pull full release data for %s: %s\n", releaseID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - if len(action) > 0 { - switch action { + if len(slices) > 1 { + switch slices[1] { case "editcredits": serveEditCredits(release).ServeHTTP(w, r) return @@ -99,20 +56,16 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han } type ReleaseResponse struct { - adminPageData + Session *model.Session Release *model.Release } - for i, track := range release.Tracks { - track.Number = i + 1 - } - - err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + err = releaseTemplate.Execute(w, ReleaseResponse{ + Session: session, Release: release, }) if err != nil { - fmt.Printf("WARN: Failed to serve admin release page for %s: %s\n", release.ID, err) + fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -121,9 +74,9 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han func serveEditCredits(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := templates.EditCreditsTemplate.Execute(w, release) + err := editCreditsTemplate.Execute(w, release) if err != nil { - fmt.Printf("WARN: Failed to serve edit credits component for %s: %s\n", release.ID, err) + fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -133,7 +86,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to pull artists not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -144,12 +97,12 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = templates.AddCreditTemplate.Execute(w, response{ + err = addCreditTemplate.Execute(w, response{ ReleaseID: release.ID, Artists: artists, }) if err != nil { - fmt.Printf("WARN: Failed to serve add credits component for %s: %s\n", release.ID, err) + fmt.Printf("Error rendering add credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -160,7 +113,7 @@ func serveNewCredit(app *model.AppState) http.Handler { artistID := strings.Split(r.URL.Path, "/")[3] artist, err := controller.GetArtist(app.DB, artistID) if err != nil { - fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err) + fmt.Printf("WARN: Failed to pull artists %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -170,9 +123,9 @@ func serveNewCredit(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = templates.NewCreditTemplate.Execute(w, artist) + err = newCreditTemplate.Execute(w, artist) if err != nil { - fmt.Printf("WARN: Failed to serve new credit component for %s: %s\n", artist.ID, err) + fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -181,9 +134,9 @@ func serveNewCredit(app *model.AppState) http.Handler { func serveEditLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := templates.EditLinksTemplate.Execute(w, release) + err := editLinksTemplate.Execute(w, release) if err != nil { - fmt.Printf("WARN: Failed to serve edit links component for %s: %s\n", release.ID, err) + fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -193,11 +146,17 @@ func serveEditTracks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - type editTracksData struct { Release *model.Release } + type editTracksData struct { + Release *model.Release + Add func(a int, b int) int + } - err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release }) + err := editTracksTemplate.Execute(w, editTracksData{ + Release: release, + Add: func(a, b int) int { return a + b }, + }) if err != nil { - fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err) + fmt.Printf("Error rendering edit tracks component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -207,7 +166,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to pull tracks not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -218,14 +177,15 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = templates.AddTrackTemplate.Execute(w, response{ + err = addTrackTemplate.Execute(w, response{ ReleaseID: release.ID, Tracks: tracks, }) if err != nil { - fmt.Printf("WARN: Failed to add tracks component for %s: %s\n", release.ID, err) + fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } + return }) } @@ -234,7 +194,7 @@ func serveNewTrack(app *model.AppState) http.Handler { trackID := strings.Split(r.URL.Path, "/")[3] track, err := controller.GetTrack(app.DB, trackID) if err != nil { - fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err) + fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -244,10 +204,11 @@ func serveNewTrack(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = templates.NewTrackTemplate.Execute(w, track) + err = newTrackTemplate.Execute(w, track) if err != nil { - fmt.Printf("WARN: Failed to serve new track component for %s: %s\n", track.ID, err) + fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } + return }) } diff --git a/admin/static/admin.css b/admin/static/admin.css index 8f983c7..877b5da 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -1,266 +1,88 @@ @import url("/style/prideflag.css"); @import url("/font/inter/inter.css"); -:root { - --bg-0: #101010; - --bg-1: #181818; - --bg-2: #282828; - --bg-3: #404040; - - --fg-0: #b0b0b0; - --fg-1: #c0c0c0; - --fg-2: #d0d0d0; - --fg-3: #e0e0e0; - - --col-shadow-0: #0002; - --col-shadow-1: #0004; - --col-shadow-2: #0006; - --col-highlight-0: #ffffff08; - --col-highlight-1: #fff1; - --col-highlight-2: #fff2; - - --col-new: #b3ee5b; - --col-on-new: #1b2013; - --col-save: #6fd7ff; - --col-on-save: #283f48; - --col-delete: #ff7171; - --col-on-delete: #371919; - - --col-warn: #ffe86a; - --col-on-warn: var(--bg-0); - --col-warn-hover: #ffec81; - - --shadow-sm: - 0 1px 2px var(--col-shadow-2), - inset 0 1px 1px var(--col-highlight-2); - --shadow-md: - 0 2px 4px var(--col-shadow-1), - inset 0 2px 2px var(--col-highlight-1); - --shadow-lg: - 0 4px 8px var(--col-shadow-0), - inset 0 4px 4px var(--col-highlight-0); -} - -@media (prefers-color-scheme: light) { - :root { - --bg-0: #e8e8e8; - --bg-1: #f0f0f0; - --bg-2: #f8f8f8; - --bg-3: #ffffff; - - --fg-0: #606060; - --fg-1: #404040; - --fg-2: #303030; - --fg-3: #202020; - - --col-shadow-0: #0002; - --col-shadow-1: #0004; - --col-shadow-2: #0008; - --col-highlight-0: #fff2; - --col-highlight-1: #fff4; - --col-highlight-2: #fff8; - - --col-warn: #ffe86a; - --col-on-warn: var(--fg-3); - --col-warn-hover: #ffec81; - } -} - -@media (prefers-color-scheme: green) { - :root { - --bg-0: #d0d9c7; - --bg-1: #e2e5de; - --bg-2: #f1f1f1; - --bg-3: #ffffff; - --fg-0: #626f54; - --fg-1: #505c43; - --fg-2: #49523e; - --fg-3: #2c3522; - } -} - -@media (prefers-color-scheme: purple) { - :root { - --bg-0: #15121c; - --bg-1: #1e1a27; - --bg-2: #302a3d; - --bg-3: #4a4358; - --fg-0: #9e8fbf; - --fg-1: #a29bb3; - --fg-2: #b9b0cd; - --fg-3: #dcd5ec; - } -} - -@media (prefers-color-scheme: dark) { - img.icon { - -webkit-filter: invert(.9); - filter: invert(.9); - } -} - body { - width: calc(100% - 180px); + width: 100%; height: calc(100vh - 1em); - margin: 0 0 0 180px; + margin: 0; padding: 0; - display: flex; - flex-direction: row; font-family: "Inter", sans-serif; font-size: 16px; - color: var(--fg-0); - background: var(--bg-0); - transition: background .1s ease-out, color .1s ease-out; + color: #303030; + background: #f0f0f0; } -h1, h2, h3, h4, h5, h6 { - color: var(--fg-3); -} - -header { - display: flex; - justify-content: space-between; - align-items: center; -} nav { - position: fixed; - top: 0; - left: 0; - width: 180px; - height: calc(100vh - 2em); - margin: 0; - padding: 1em 0; + width: min(720px, calc(100% - 2em)); + height: 2em; + margin: 1em auto; display: flex; - flex-direction: column; + flex-direction: row; justify-content: left; - background-color: var(--bg-1); - box-shadow: var(--shadow-md); - transition: background .1s ease-out, color .1s ease-out; - - user-select: none; + background: #f8f8f8; + border-radius: 4px; + border: 1px solid #808080; } nav .icon { - width: fit-content; - height: fit-content; - padding: 0; - margin: 0 auto 1em auto; + height: 100%; +} +nav .title { + width: auto; + height: 100%; + + margin: 0 1em 0 0; + display: flex; - border-radius: 100%; - box-shadow: var(--shadow-sm); - overflow: clip; -} -nav .icon img { - width: 3em; - height: 3em; + line-height: 2em; + text-decoration: none; + + color: inherit; } .nav-item { + width: auto; + height: 100%; + + margin: 0px; + padding: 0 1em; + display: flex; - color: var(--fg-2); + line-height: 2em; - font-weight: 500; - transition: color .1s ease-out, background-color .1s ease-out; } .nav-item:hover { - color: var(--bg-2); - background-color: var(--fg-2); + background: #00000010; text-decoration: none; } -.nav-item.active { - border-left: 4px solid var(--fg-2); -} -.nav-item.active a { - padding-left: calc(1em - 3.5px); -} nav a { - padding: .2em 1em; text-decoration: none; color: inherit; - width: 100%; } -nav a.active { - border-left: 5px solid var(--fg-0); - padding-left: calc(1em - 5px); -} -nav hr { - width: calc(100% - 2em); - margin: .5em auto; - border: none; - border-bottom: 1px solid var(--fg-0); -} -nav .section-label { - margin: .6em 0 .1em 1.6em; - font-size: .6em; - text-transform: uppercase; - font-weight: 600; -} -#toggle-nav { - position: fixed; - top: 16px; - left: 16px; - padding: 8px; - width: 48px; - height: 48px; - display: none; - justify-content: center; - align-items: center; - z-index: 1; -} -#toggle-nav img { - width: 100%; - height: 100%; - object-fit: cover; - transform: translate(1px, 1px); -} -#toggle-nav img:hover { - -webkit-filter: invert(.9); - filter: invert(.9); -} -@media (prefers-color-scheme: dark) { - #toggle-nav img { - -webkit-filter: invert(.9); - filter: invert(.9); - } - #toggle-nav img:hover { - -webkit-filter: none; - filter: none; - } +nav #logout { + /* margin-left: auto; */ } main { - width: 720px; - max-width: calc(100% - 2em); - height: fit-content; - min-height: calc(100vh - 2em); + width: min(720px, calc(100% - 2em)); margin: 0 auto; padding: 1em; } -main.dashboard { - width: 100%; -} a { color: inherit; text-decoration: none; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - transition: color .1s ease-out, background-color .1s ease-out; } -/* a:hover { text-decoration: underline; } -*/ -img.icon { +a img.icon { height: .8em; - transition: filter .1s ease-out; } code { @@ -272,24 +94,7 @@ code { -.cards { - width: 100%; - height: fit-content; - display: flex; - gap: 2em; - flex-wrap: wrap; -} - .card { - flex-basis: 40em; - padding: 1em; - background: var(--bg-1); - border-radius: 16px; - box-shadow: var(--shadow-lg); - - transition: background .1s ease-out, color .1s ease-out; -} -main:not(.dashboard) .card { margin-bottom: 1em; } @@ -297,7 +102,7 @@ main:not(.dashboard) .card { margin: 0 0 .5em 0; } -.card-header { +.card-title { margin-bottom: 1em; display: flex; gap: 1em; @@ -305,31 +110,21 @@ main:not(.dashboard) .card { align-items: center; justify-content: space-between; } -.card-header h1, -.card-header h2, -.card-header h3 { - margin: 0; -} -.card-header a:hover { - text-decoration: underline; -} -header :is(h1, h2, h3) small, -.card-header :is(h1, h2, h3) small { - display: inline-block; - font-size: .6em; - transform: translateY(-0.1em); - color: var(--fg-0); +.card-title h1, +.card-title h2, +.card-title h3 { + margin: 0; } .flex-fill { flex-grow: 1; } -.artists-group { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 1em; +@media screen and (max-width: 520px) { + body { + font-size: 12px; + } } @@ -338,15 +133,17 @@ header :is(h1, h2, h3) small, #error { margin: 0 0 1em 0; padding: 1em; - border-radius: 8px; - color: #101010; + border-radius: 4px; background: #ffffff; + border: 1px solid #888; } #message { background: #a9dfff; + border-color: #599fdc; } #error { background: #ffa9b8; + border-color: #dc5959; } @@ -355,54 +152,52 @@ a.delete:not(.button) { color: #d22828; } -.button, button { +button, .button { padding: .5em .8em; font-family: inherit; font-size: inherit; - + border-radius: 4px; + border: 1px solid #a0a0a0; + background: #f0f0f0; color: inherit; - background: var(--bg-2); - border: none; - border-radius: 10em; - box-shadow: var(--shadow-sm); - font-weight: 500; - transition: background .1s ease-out, color .1s ease-out; - - cursor: pointer; - user-select: none; } button:hover, .button:hover { background: #fff; + border-color: #d0d0d0; } button:active, .button:active { background: #d0d0d0; + border-color: #808080; } +.button, button { + color: inherit; +} .button.new, button.new { - color: var(--col-on-new); - background: var(--col-new); + background: #c4ff6a; + border-color: #84b141; } .button.save, button.save { - color: var(--col-on-save); - background: var(--col-save); + background: #6fd7ff; + border-color: #6f9eb0; } .button.delete, button.delete { - color: var(--col-on-delete); - background: var(--col-delete); + background: #ff7171; + border-color: #7d3535; } .button:hover, button:hover { - color: var(--bg-3); - background: var(--fg-3); + background: #fff; + border-color: #d0d0d0; } .button:active, button:active { - color: var(--bg-2); - background: var(--fg-0); + background: #d0d0d0; + border-color: #808080; } .button[disabled], button[disabled] { - color: var(--fg-0) !important; - background: var(--bg-3) !important; + background: #d0d0d0 !important; + border-color: #808080 !important; opacity: .5; - cursor: default !important; + cursor: not-allowed !important; } @@ -410,79 +205,24 @@ button:active, .button:active { form { width: 100%; display: block; - color: var(--fg-0); } form label { width: 100%; margin: 1rem 0 .5rem 0; display: block; + color: #10101080; } -form input[type="text"], -form input[type="password"] { - width: 16em; - max-width: 100%; - margin: .5em 0; - padding: .3em .5em; +form input { + margin: .5rem 0; + padding: .3rem .5rem; display: block; border-radius: 4px; border: 1px solid #808080; font-size: inherit; font-family: inherit; color: inherit; - background-color: var(--bg-0); } input[disabled] { opacity: .5; cursor: not-allowed; } - -@media screen and (max-width: 720px) { - main { - padding-top: 0; - } - - body { - width: 100%; - margin: 0; - font-size: 16px; - } - - nav { - width: 100%; - left: -100%; - font-size: 24px; - z-index: 1; - transition: transform .2s cubic-bezier(.2,0,.5,1); - } - nav.open { - transform: translateX(100%); - } - - #toggle-nav { - display: flex; - } - - main > h1:first-of-type { - margin-left: 68px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - main > header { - margin-left: 68px; - } - main > header h1 { - display: flex; - flex-direction: column; - font-size: 1.5em; - } - - .card { - flex-basis: 100%; - max-width: calc(100vw - 4em); - } - - .artists-group { - grid-template-columns: repeat(3, 1fr); - } -} diff --git a/admin/static/admin.js b/admin/static/admin.js index 8bf2480..0763ab7 100644 --- a/admin/static/admin.js +++ b/admin/static/admin.js @@ -69,28 +69,3 @@ export function makeMagicList(container, itemSelector, callback) { if (callback) callback(); }); } - -export function hijackClickEvent(container, link) { - container.addEventListener('click', event => { - if (event.target.tagName.toLowerCase() === 'a') return; - event.preventDefault(); - link.dispatchEvent(new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window, - ctrlKey: event.ctrlKey, - metaKey: event.metaKey, - shiftKey: event.shiftKey, - altKey: event.altKey, - button: event.button, - })); - }); -} - -document.addEventListener("readystatechange", () => { - const navbar = document.getElementById("navbar"); - document.getElementById("toggle-nav").addEventListener("click", event => { - event.preventDefault(); - navbar.classList.toggle("open"); - }) -}); diff --git a/admin/static/artists.css b/admin/static/artists.css deleted file mode 100644 index 516a998..0000000 --- a/admin/static/artists.css +++ /dev/null @@ -1,27 +0,0 @@ -.artist { - padding: .5em; - - color: var(--fg-3); - background: var(--bg-2); - box-shadow: var(--shadow-md); - border-radius: 16px; - text-align: center; - - cursor: pointer; - transition: background .1s ease-out, color .1s ease-out; -} - -.artist:hover { - background: var(--bg-1); - text-decoration: hover; -} - -.artist .artist-avatar { - width: 100%; - object-fit: cover; - border-radius: 8px; -} - -.artist .artist-name { - display: block; -} diff --git a/admin/static/artists.js b/admin/static/artists.js deleted file mode 100644 index 29eab22..0000000 --- a/admin/static/artists.js +++ /dev/null @@ -1,7 +0,0 @@ -import { hijackClickEvent } from "./admin.js"; - -document.addEventListener("readystatechange", () => { - document.querySelectorAll(".artists-group .artist").forEach(el => { - hijackClickEvent(el, el.querySelector("a.artist-name")) - }); -}); diff --git a/admin/static/edit-account.css b/admin/static/edit-account.css index c43d6e9..7a4d34a 100644 --- a/admin/static/edit-account.css +++ b/admin/static/edit-account.css @@ -11,11 +11,8 @@ label { align-items: center; color: inherit; } -form#change-password input, -form#delete-account input { - width: 20em; - min-width: auto; - max-width: calc(100% - 1em - 2px); +input { + width: min(20rem, calc(100% - 1rem)); margin: .5rem 0; padding: .3rem .5rem; display: block; @@ -28,14 +25,12 @@ form#delete-account input { .mfa-device { padding: .75em; + background: #f8f8f8f8; + border: 1px solid #808080; + border-radius: 8px; margin-bottom: .5em; display: flex; justify-content: space-between; - - color: var(--fg-3); - background: var(--bg-2); - box-shadow: var(--shadow-md); - border-radius: 16px; } .mfa-device div { @@ -51,14 +46,3 @@ form#delete-account input { .mfa-device .mfa-device-name { font-weight: bold; } - -.mfa-device form input { - display: none !important; -} - -.mfa-actions { - display: flex; - flex-direction: row; - gap: .5em; - flex-wrap: wrap; -} diff --git a/admin/static/edit-artist.css b/admin/static/edit-artist.css index 7bf146b..5627e64 100644 --- a/admin/static/edit-artist.css +++ b/admin/static/edit-artist.css @@ -1,3 +1,7 @@ +h1 { + margin: 0 0 1em 0; +} + #artist { margin-bottom: 1em; padding: 1.5em; @@ -5,9 +9,9 @@ flex-direction: row; gap: 1.2em; - border-radius: 16px; - background: var(--bg-2); - box-shadow: var(--shadow-md); + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; } .artist-avatar { @@ -23,8 +27,7 @@ cursor: pointer; } .artist-avatar #remove-avatar { - margin-top: .5em; - padding: .3em .6em; + padding: .3em .4em; } .artist-info { @@ -50,8 +53,8 @@ input[type="text"] { font-family: inherit; font-weight: inherit; color: inherit; - background: var(--bg-0); - border: none; + background: #ffffff; + border: 1px solid transparent; border-radius: 4px; outline: none; } @@ -71,7 +74,7 @@ input[type="text"]:focus { justify-content: right; } -.card-header a.button { +.card-title a.button { text-decoration: none; } @@ -82,17 +85,9 @@ input[type="text"]:focus { flex-direction: row; gap: 1em; align-items: center; - - border-radius: 16px; - background: var(--bg-2); - box-shadow: var(--shadow-md); - - cursor: pointer; - transition: background .1s; -} - -.credit:hover { - background: var(--bg-1); + background: #f8f8f8; + border-radius: 8px; + border: 1px solid #808080; } .release-artwork { @@ -101,14 +96,8 @@ input[type="text"]:focus { border-radius: 4px; } -.credit-info { - overflow: hidden; -} .credit-info h3, .credit-info p { margin: 0; font-size: .9em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } diff --git a/admin/static/edit-artist.js b/admin/static/edit-artist.js index 069c25d..8f1bb2b 100644 --- a/admin/static/edit-artist.js +++ b/admin/static/edit-artist.js @@ -1,5 +1,3 @@ -import { hijackClickEvent } from "./admin.js"; - const artistID = document.getElementById("artist").dataset.id; const nameInput = document.getElementById("name"); const avatarImg = document.getElementById("avatar"); @@ -79,9 +77,3 @@ removeAvatarBtn.addEventListener("click", () => { avatarImg.src = "/img/default-avatar.png" saveBtn.disabled = false; }); - -document.addEventListener('readystatechange', () => { - document.querySelectorAll('#releases .credit').forEach(el => { - hijackClickEvent(el, el.querySelector('.credit-name a')); - }); -}); diff --git a/admin/static/edit-release.css b/admin/static/edit-release.css index 434b487..aa70e34 100644 --- a/admin/static/edit-release.css +++ b/admin/static/edit-release.css @@ -12,10 +12,8 @@ input[type="text"] { gap: 1.2em; border-radius: 8px; - background: var(--bg-2); - box-shadow: var(--shadow-md); - - transition: background .1s ease-out, color .1s ease-out; + background: #f8f8f8f8; + border: 1px solid #808080; } .release-artwork { @@ -31,9 +29,7 @@ input[type="text"] { cursor: pointer; } .release-artwork #remove-artwork { - margin-top: .5em; - padding: .3em .6em; - background: var(--bg-3); + padding: .3em .4em; } .release-info { @@ -58,17 +54,17 @@ input[type="text"] { background: transparent; outline: none; cursor: pointer; - transition: background .1s ease-out, border-color .1s ease-out; } #title:hover { - background: var(--bg-3); - border-color: var(--fg-0); + background: #ffffff; + border-color: #80808080; } #title:active, #title:focus { - background: var(--bg-3); + background: #ffffff; + border-color: #808080; } .release-title small { @@ -79,21 +75,19 @@ input[type="text"] { width: 100%; margin: .5em 0; border-collapse: collapse; - color: var(--fg-2); } .release-info table td { padding: .2em; - border-bottom: 1px solid color-mix(in srgb, var(--fg-0) 25%, transparent); - transition: background .1s ease-out, border-color .1s ease-out; + border-bottom: 1px solid #d0d0d0; } .release-info table tr td:first-child { vertical-align: top; - opacity: .5; + opacity: .66; } .release-info table tr td:not(:first-child) select:hover, .release-info table tr td:not(:first-child) input:hover, .release-info table tr td:not(:first-child) textarea:hover { - background: var(--bg-3); + background: #e8e8e8; cursor: pointer; } .release-info table td select, @@ -121,25 +115,13 @@ input[type="text"] { gap: .5em; flex-direction: row; justify-content: right; - color: var(--fg-3); -} - -.release-actions button, -.release-actions .button { - color: var(--fg-2); - background: var(--bg-3); } dialog { width: min(720px, calc(100% - 2em)); padding: 2em; - border: none; - border-radius: 16px; - color: var(--fg-0); - background-color: var(--bg-0); - box-shadow: var(--shadow-lg); - - transition: color .1s ease-out, background-color .1s ease-out; + border: 1px solid #101010; + border-radius: 8px; } dialog header { @@ -162,7 +144,7 @@ dialog div.dialog-actions { gap: .5em; } -.card-header a.button { +.card-title a.button { text-decoration: none; } @@ -170,7 +152,7 @@ dialog div.dialog-actions { * RELEASE CREDITS */ -#credits .credit { +.card.credits .credit { margin-bottom: .5em; padding: .5em; display: flex; @@ -178,47 +160,28 @@ dialog div.dialog-actions { align-items: center; gap: 1em; - border-radius: 16px; - background-color: var(--bg-2); - box-shadow: var(--shadow-md); - - cursor: pointer; - transition: background .1s ease-out; -} -#credits .credit:hover { - background-color: var(--bg-1); + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; } -#credits .credit p { +.card.credits .credit p { margin: 0; } -#credits .credit .artist-avatar { - border-radius: 12px; +.card.credits .credit .artist-avatar { + border-radius: 8px; } -#credits .credit .artist-name { - color: var(--fg-3); +.card.credits .credit .artist-name { font-weight: bold; } -#credits .credit .artist-role small { +.card.credits .credit .artist-role small { font-size: inherit; opacity: .66; } -#credits .credit .credit-info { - overflow: hidden; -} - -#credits .credit .credit-info :is(h3, p) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - - - #editcredits ul { margin: 0; padding: 0; @@ -234,8 +197,8 @@ dialog div.dialog-actions { gap: 1em; border-radius: 8px; - background: var(--bg-2); - box-shadow: var(--shadow-md); + background: #f8f8f8f8; + border: 1px solid #808080; } #editcredits .credit { @@ -269,7 +232,6 @@ dialog div.dialog-actions { margin: 0; display: flex; align-items: center; - color: inherit; } #editcredits .credit .credit-info .credit-attribute input[type="text"] { @@ -277,17 +239,15 @@ dialog div.dialog-actions { padding: .2em .4em; flex-grow: 1; font-family: inherit; - border: none; + border: 1px solid #8888; border-radius: 4px; - color: var(--fg-2); - background: var(--bg-0); + color: inherit; } #editcredits .credit .credit-info .credit-attribute input[type="checkbox"] { margin: 0 .3em; } #editcredits .credit .artist-name { - color: var(--fg-2); font-weight: bold; } @@ -296,12 +256,8 @@ dialog div.dialog-actions { opacity: .66; } -#editcredits .credit .delete { - margin-right: .5em; - cursor: pointer; -} -#editcredits .credit .delete:hover { - text-decoration: underline; +#editcredits .credit button.delete { + margin-left: auto; } #addcredit ul { @@ -333,39 +289,24 @@ dialog div.dialog-actions { * RELEASE LINKS */ -#links ul { - padding: 0; +.card.links { display: flex; gap: .2em; } -#links a img.icon { - -webkit-filter: none; - filter: none; -} - -#links a.button:hover { - color: var(--bg-3) !important; - background-color: var(--fg-3) !important; -} - -#links a.button[data-name="spotify"] { - color: #101010; +.card.links a.button[data-name="spotify"] { background-color: #8cff83 } -#links a.button[data-name="apple music"] { - color: #101010; +.card.links a.button[data-name="apple music"] { background-color: #8cd9ff } -#links a.button[data-name="soundcloud"] { - color: #101010; +.card.links a.button[data-name="soundcloud"] { background-color: #fdaa6d } -#links a.button[data-name="youtube"] { - color: #101010; +.card.links a.button[data-name="youtube"] { background-color: #ff6e6e } @@ -448,6 +389,62 @@ dialog div.dialog-actions { outline: 1px solid #808080; } +/* + * RELEASE TRACKS + */ + +.card.tracks .track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.card.tracks .track h3, +.card.tracks .track p { + margin: 0; +} + +.card.tracks h2.track-title { + margin: 0; + display: flex; + gap: .5em; +} + +.card.tracks h2.track-title .track-number { + opacity: .5; +} + +.card.tracks .track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.card.tracks .track-album.empty { + color: #ff2020; + opacity: 1; +} + +.card.tracks .track-description { + font-style: italic; +} + +.card.tracks .track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.card.tracks .track .empty { + opacity: 0.75; +} + #edittracks ul { padding: 0; list-style: none; @@ -499,17 +496,15 @@ dialog div.dialog-actions { padding: .5em; display: flex; gap: .5em; - background-color: var(--bg-0); cursor: pointer; - transition: background-color .1s ease-out, color .1s ease-out; } #addtrack ul li.new-track:nth-child(even) { - background: color-mix(in srgb, var(--bg-0) 95%, #fff); + background: #f0f0f0; } #addtrack ul li.new-track:hover { - background: color-mix(in srgb, var(--bg-0) 90%, #fff); + background: #e0e0e0; } @media only screen and (max-width: 1105px) { diff --git a/admin/static/edit-release.js b/admin/static/edit-release.js index ca2754f..11d21f0 100644 --- a/admin/static/edit-release.js +++ b/admin/static/edit-release.js @@ -1,5 +1,3 @@ -import { hijackClickEvent } from "./admin.js"; - const releaseID = document.getElementById("release").dataset.id; const titleInput = document.getElementById("title"); const artworkImg = document.getElementById("artwork"); @@ -98,9 +96,3 @@ removeArtworkBtn.addEventListener("click", () => { artworkData = ""; saveBtn.disabled = false; }); - -document.addEventListener("readystatechange", () => { - document.querySelectorAll("#credits .credit").forEach(el => { - hijackClickEvent(el, el.querySelector(".artist-name a")); - }); -}); diff --git a/admin/static/edit-track.css b/admin/static/edit-track.css index f292ca5..600b680 100644 --- a/admin/static/edit-track.css +++ b/admin/static/edit-track.css @@ -1,5 +1,9 @@ @import url("/admin/static/release-list-item.css"); +h1 { + margin: 0 0 .5em 0; +} + #track { margin-bottom: 1em; padding: .5em 1.5em 1.5em 1.5em; @@ -7,9 +11,9 @@ flex-direction: row; gap: 1.2em; - border-radius: 16px; - background: var(--bg-2); - box-shadow: var(--shadow-md); + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; } .track-info { @@ -45,8 +49,7 @@ font-weight: inherit; font-family: inherit; font-size: inherit; - background: var(--bg-0); - border: none; + border: 1px solid transparent; border-radius: 4px; outline: none; color: inherit; diff --git a/admin/static/index.css b/admin/static/index.css new file mode 100644 index 0000000..9fcd731 --- /dev/null +++ b/admin/static/index.css @@ -0,0 +1,82 @@ +@import url("/admin/static/release-list-item.css"); + +.artist { + margin-bottom: .5em; + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: .5em; + + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.artist:hover { + text-decoration: hover; +} + +.artist-avatar { + width: 32px; + height: 32px; + object-fit: cover; + border-radius: 100%; +} + +.track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.track p { + margin: 0; +} + +.card h2.track-title { + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.track-id { + width: fit-content; + font-family: "Monaspace Argon", monospace; + font-size: .8em; + font-style: italic; + line-height: 1em; + user-select: all; +} + +.track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.track-album.empty { + color: #ff2020; + opacity: 1; +} + +.track-description { + font-style: italic; +} + +.track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.track .empty { + opacity: 0.75; +} diff --git a/admin/static/index.js b/admin/static/index.js index 60bdfd0..e251802 100644 --- a/admin/static/index.js +++ b/admin/static/index.js @@ -12,7 +12,7 @@ newReleaseBtn.addEventListener("click", event => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({id}) }).then(res => { - if (res.ok) location = "/admin/releases/" + id; + if (res.ok) location = "/admin/release/" + id; else { res.text().then(err => { alert("Request failed: " + err); @@ -37,7 +37,7 @@ newArtistBtn.addEventListener("click", event => { }).then(res => { res.text().then(text => { if (res.ok) { - location = "/admin/artists/" + id; + location = "/admin/artist/" + id; } else { alert("Request failed: " + text); console.error(text); @@ -61,7 +61,7 @@ newTrackBtn.addEventListener("click", event => { }).then(res => { res.text().then(text => { if (res.ok) { - location = "/admin/tracks/" + text; + location = "/admin/track/" + text; } else { alert("Request failed: " + text); console.error(text); diff --git a/admin/static/logs.css b/admin/static/logs.css index 8da60d0..f0df299 100644 --- a/admin/static/logs.css +++ b/admin/static/logs.css @@ -2,14 +2,8 @@ main { width: min(1080px, calc(100% - 2em))!important } -form#search-form { - width: calc(100% - 2em); +form { margin: 1em 0; - padding: 1em; - border-radius: 16px; - color: var(--fg-0); - background: var(--bg-2); - box-shadow: var(--shadow-md); } div#search { @@ -18,25 +12,24 @@ div#search { #search input { margin: 0; - padding: .3em .8em; flex-grow: 1; - border: none; - border-radius: 16px; - color: var(--fg-1); - background: var(--bg-0); - box-shadow: var(--shadow-sm); + + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } #search button { - margin-left: .5em; - padding: 0 .8em; + padding: 0 .5em; + + border-top-left-radius: 0; + border-bottom-left-radius: 0; } form #filters p { margin: .5em 0 0 0; } form #filters label { - color: inherit; display: inline; } form #filters input { @@ -46,15 +39,6 @@ form #filters input { #logs { width: 100%; - overflow: scroll; -} -@media screen and (max-width: 720px) { - #logs { - font-size: 12px; - } -} - -#logs table{ border-collapse: collapse; } @@ -73,10 +57,6 @@ form #filters input { padding: .4em .8em; } -#logs .log { - color: var(--fg-2); -} - td, th { width: 1%; text-align: left; @@ -94,14 +74,13 @@ td.log-content { white-space: collapse; } -#logs .log:hover { - background: color-mix(in srgb, var(--fg-3) 10%, transparent); +.log:hover { + background: #fff8; } -#logs .log.warn { - color: var(--col-on-warn); - background: var(--col-warn); +.log.warn { + background: #ffe86a; } -#logs .log.warn:hover { - background: var(--col-warn-hover); +.log.warn:hover { + background: #ffec81; } diff --git a/admin/static/release-list-item.css b/admin/static/release-list-item.css new file mode 100644 index 0000000..638eac0 --- /dev/null +++ b/admin/static/release-list-item.css @@ -0,0 +1,87 @@ +.release { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: row; + gap: 1em; + + border-radius: 8px; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.release h3, +.release p { + margin: 0; +} + +.release-artwork { + width: 96px; + + display: flex; + justify-content: center; + align-items: center; +} + +.release-artwork img { + width: 100%; + aspect-ratio: 1; +} + +.release-title small { + opacity: .75; +} + +.release-links { + margin: .5em 0; + padding: 0; + display: flex; + flex-direction: row; + list-style: none; + flex-wrap: wrap; + gap: .5em; +} + +.release-links li { + flex-grow: 1; +} + +.release-links a { + padding: .5em; + display: block; + + border-radius: 8px; + text-decoration: none; + color: #f0f0f0; + background: #303030; + text-align: center; + + transition: color .1s, background .1s; +} + +.release-links a:hover { + color: #303030; + background: #f0f0f0; +} + +.release-actions { + margin-top: .5em; +} + +.release-actions a { + margin-right: .3em; + padding: .3em .5em; + display: inline-block; + + border-radius: 4px; + background: #e0e0e0; + + transition: color .1s, background .1s; +} + +.release-actions a:hover { + color: #303030; + background: #f0f0f0; + + text-decoration: none; +} diff --git a/admin/static/releases.css b/admin/static/releases.css deleted file mode 100644 index 0694875..0000000 --- a/admin/static/releases.css +++ /dev/null @@ -1,80 +0,0 @@ -.release { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: row; - gap: 1em; - - border-radius: 16px; - background: var(--bg-2); - box-shadow: var(--shadow-md); - - transition: background .1s ease-out, color .1s ease-out; -} - -.release h3, -.release p { - margin: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.release .release-artwork { - margin: auto 0; - width: 96px; - - display: flex; - justify-content: center; - align-items: center; - border-radius: 4px; - overflow: hidden; - box-shadow: var(--shadow-sm); -} - -.release .release-artwork img { - width: 100%; - aspect-ratio: 1; -} - -.release .release-info { - max-width: calc(100% - 96px - 1em); -} - -.release .release-title small { - opacity: .75; -} - -.release .release-links { - margin: .5em 0; - padding: 0; - display: flex; - flex-direction: row; - list-style: none; - flex-wrap: wrap; - gap: .5em; -} - -.release .release-actions { - margin-top: .5em; - user-select: none; - color: var(--fg-3); -} - -.release .release-actions a { - margin-right: .3em; - padding: .3em .5em; - display: inline-block; - - border-radius: 4px; - background: var(--bg-3); - box-shadow: var(--shadow-sm); - - transition: color .1s ease-out, background .1s ease-out; -} - -.release .release-actions a:hover { - background: var(--bg-0); - color: var(--fg-3); - text-decoration: none; -} diff --git a/admin/static/tracks.css b/admin/static/tracks.css deleted file mode 100644 index c36c1b1..0000000 --- a/admin/static/tracks.css +++ /dev/null @@ -1,127 +0,0 @@ -#tracks h2.track-title { - margin: 0; - display: flex; - gap: .5em; -} - -#tracks .track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: 16px; - background: var(--bg-2); - box-shadow: var(--shadow-md); - - transition: background .1s ease-out, color .1s ease-out; -} - -#tracks .track h3, -#tracks .track p { - margin: 0; -} - -#tracks h2.track-title { - margin: 0; - display: flex; - gap: .5em; -} - -#tracks h2.track-title .track-number { - opacity: .5; -} - -#tracks a:hover { - text-decoration: underline; -} - -#tracks .track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -#tracks .track-album.empty { - color: #ff2020; - opacity: 1; -} - -#tracks .track-description { - font-style: italic; -} - -#tracks .track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -#tracks .track .empty { - opacity: 0.75; -} - - -.card h2.track-title { - margin: 0; - display: flex; - flex-direction: row; - /* - justify-content: space-between; - */ -} - -/* -.track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: 8px; - background-color: var(--bg-2); - box-shadow: var(--shadow-md); - - transition: color .1s ease-out, background-color .1s ease-out; -} - -.track p { - margin: 0; -} - -.track-id { - width: fit-content; - font-family: "Monaspace Argon", monospace; - font-size: .8em; - font-style: italic; - line-height: 1em; - user-select: all; -} - -.track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -.track-album.empty { - color: #ff2020; - opacity: 1; -} - -.track-description { - font-style: italic; -} - -.track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -.track .empty { - opacity: 0.75; -} -*/ diff --git a/admin/templates.go b/admin/templates.go new file mode 100644 index 0000000..606d569 --- /dev/null +++ b/admin/templates.go @@ -0,0 +1,125 @@ +package admin + +import ( + "arimelody-web/log" + "fmt" + "html/template" + "path/filepath" + "strings" + "time" +) + +var indexTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "index.html"), +)) + +var loginTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "login.html"), +)) +var loginTOTPTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "login-totp.html"), +)) +var registerTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "register.html"), +)) +var logoutTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "logout.html"), +)) +var accountTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-account.html"), +)) +var totpSetupTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "totp-setup.html"), +)) +var totpConfirmTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "totp-confirm.html"), +)) + +var logsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{ + "parseLevel": func(level log.LogLevel) string { + switch level { + case log.LEVEL_INFO: + return "INFO" + case log.LEVEL_WARN: + return "WARN" + } + return fmt.Sprintf("%d?", level) + }, + "titleCase": func(logType string) string { + runes := []rune(logType) + for i, r := range runes { + if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { + runes[i] = r + ('A' - 'a') + } + } + return string(runes) + }, + "lower": func(str string) string { return strings.ToLower(str) }, + "prettyTime": func(t time.Time) string { + // return t.Format("2006-01-02 15:04:05") + // return t.Format("15:04:05, 2 Jan 2006") + return t.Format("02 Jan 2006, 15:04:05") + }, +}).ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "logs.html"), +)) + +var releaseTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-release.html"), +)) +var artistTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-artist.html"), +)) +var trackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "edit-track.html"), +)) + +var editCreditsTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "editcredits.html"), +)) +var addCreditTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "addcredit.html"), +)) +var newCreditTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "newcredit.html"), +)) + +var editLinksTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "links", "editlinks.html"), +)) + +var editTracksTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "edittracks.html"), +)) +var addTrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "addtrack.html"), +)) +var newTrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "newtrack.html"), +)) diff --git a/admin/templates/html/artists.html b/admin/templates/html/artists.html deleted file mode 100644 index f4a6432..0000000 --- a/admin/templates/html/artists.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "head"}} -Artists - ari melody 💫 - - -{{end}} - -{{define "content"}} -
    -
    -

    Artists ({{len .Artists}} total)

    - Create New -
    - - {{if .Artists}} -
    - {{range .Artists}} - {{block "artist" .}}{{end}} - {{end}} -
    - {{else}} -

    There are no artists.

    - {{end}} -
    - - -{{end}} diff --git a/admin/templates/html/components/artist/artist.html b/admin/templates/html/components/artist/artist.html deleted file mode 100644 index 86ac4cc..0000000 --- a/admin/templates/html/components/artist/artist.html +++ /dev/null @@ -1,6 +0,0 @@ -{{define "artist"}} -
    - - {{.Name}} -
    -{{end}} diff --git a/admin/templates/html/components/track/track.html b/admin/templates/html/components/track/track.html deleted file mode 100644 index 4db20a3..0000000 --- a/admin/templates/html/components/track/track.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "track"}} -
    -

    - {{if .Number}} - {{.Number}} - {{end}} - {{.Title}} -

    - -

    Description

    - {{if .Description}} -

    {{.GetDescriptionHTML}}

    - {{else}} -

    No description provided.

    - {{end}} - -

    Lyrics

    - {{if .Lyrics}} -

    {{.GetLyricsHTML}}

    - {{else}} -

    There are no lyrics.

    - {{end}} -
    -{{end}} diff --git a/admin/templates/html/index.html b/admin/templates/html/index.html deleted file mode 100644 index afb9471..0000000 --- a/admin/templates/html/index.html +++ /dev/null @@ -1,61 +0,0 @@ -{{define "head"}} -Admin - ari melody 💫 - - - - -{{end}} - -{{define "content"}} -
    -

    Dashboard

    - -
    -
    -
    -

    Releases ({{.ReleaseCount}} total)

    - Create New -
    - {{if .Artists}} - {{range .Releases}} - {{block "release" .}}{{end}} - {{end}} - {{else}} -

    There are no releases.

    - {{end}} -
    - -
    -
    -

    Artists ({{.ArtistCount}} total)

    - Create New -
    - {{if .Artists}} -
    - {{range .Artists}} - {{block "artist" .}}{{end}} - {{end}} -
    - {{else}} -

    There are no artists.

    - {{end}} -
    - -
    -
    -

    Tracks ({{.TrackCount}} total)

    - Create New -
    -

    "Orphaned" tracks that have not yet been bound to a release.

    -
    - {{range .Tracks}} - {{block "track" .}}{{end}} - {{end}} -
    -
    - -
    - - - -{{end}} diff --git a/admin/templates/html/layout.html b/admin/templates/html/layout.html deleted file mode 100644 index bf1b542..0000000 --- a/admin/templates/html/layout.html +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - {{block "head" .}}{{end}} - - - - - - - -
    - - -
    - - {{block "content" .}}{{end}} - - {{template "prideflag"}} - - - diff --git a/admin/templates/html/releases.html b/admin/templates/html/releases.html deleted file mode 100644 index ed5aec6..0000000 --- a/admin/templates/html/releases.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "head"}} -Releases - ari melody 💫 - - -{{end}} - -{{define "content"}} -
    -
    -

    Releases ({{len .Releases}} total)

    - Create New -
    - - {{if .Releases}} -
    - {{range .Releases}} - {{block "release" .}}{{end}} - {{end}} -
    - {{else}} -

    There are no releases.

    - {{end}} -
    -{{end}} diff --git a/admin/templates/html/tracks.html b/admin/templates/html/tracks.html deleted file mode 100644 index 6bb6f18..0000000 --- a/admin/templates/html/tracks.html +++ /dev/null @@ -1,34 +0,0 @@ -{{define "head"}} -Releases - ari melody 💫 - - -{{end}} - -{{define "content"}} -
    -
    -

    Tracks ({{len .Tracks}} total)

    - Create New -
    - -
    - {{range $Track := .Tracks}} -
    -

    - {{$Track.Title}} -

    - {{if $Track.Description}} -

    {{$Track.GetDescriptionHTML}}

    - {{else}} -

    No description provided.

    - {{end}} - {{if $Track.Lyrics}} -

    {{$Track.GetLyricsHTML}}

    - {{else}} -

    There are no lyrics.

    - {{end}} -
    - {{end}} -
    -
    -{{end}} diff --git a/admin/templates/templates.go b/admin/templates/templates.go deleted file mode 100644 index 51b1376..0000000 --- a/admin/templates/templates.go +++ /dev/null @@ -1,191 +0,0 @@ -package templates - -import ( - "arimelody-web/log" - _ "embed" - "fmt" - "html/template" - "strings" - "time" -) - -//go:embed "html/layout.html" -var layoutHTML string -//go:embed "html/prideflag.html" -var prideflagHTML string - -//go:embed "html/index.html" -var indexHTML string - -//go:embed "html/register.html" -var registerHTML string -//go:embed "html/login.html" -var loginHTML string -//go:embed "html/login-totp.html" -var loginTotpHTML string -//go:embed "html/totp-confirm.html" -var totpConfirmHTML string -//go:embed "html/totp-setup.html" -var totpSetupHTML string -//go:embed "html/logout.html" -var logoutHTML string - -//go:embed "html/logs.html" -var logsHTML string - -//go:embed "html/edit-account.html" -var editAccountHTML string - -//go:embed "html/releases.html" -var releasesHTML string -//go:embed "html/artists.html" -var artistsHTML string -//go:embed "html/tracks.html" -var tracksHTML string - -//go:embed "html/edit-release.html" -var editReleaseHTML string -//go:embed "html/edit-artist.html" -var editArtistHTML string -//go:embed "html/edit-track.html" -var editTrackHTML string - -//go:embed "html/components/credit/newcredit.html" -var componentNewCreditHTML string -//go:embed "html/components/credit/addcredit.html" -var componentAddCreditHTML string -//go:embed "html/components/credit/editcredits.html" -var componentEditCreditsHTML string - -//go:embed "html/components/link/editlinks.html" -var componentEditLinksHTML string - -//go:embed "html/components/release/release.html" -var componentReleaseHTML string -//go:embed "html/components/artist/artist.html" -var componentArtistHTML string -//go:embed "html/components/track/track.html" -var componentTrackHTML string - -//go:embed "html/components/track/newtrack.html" -var componentNewTrackHTML string -//go:embed "html/components/track/addtrack.html" -var componentAddTrackHTML string -//go:embed "html/components/track/edittracks.html" -var componentEditTracksHTML string - -var BaseTemplate = template.Must( - template.New("base").Funcs( - template.FuncMap{ - "hasPrefix": strings.HasPrefix, - }, - ).Parse(strings.Join([]string{ - layoutHTML, - prideflagHTML, - }, "\n"))) - -var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( - strings.Join([]string{ - indexHTML, - componentReleaseHTML, - componentArtistHTML, - componentTrackHTML, - }, "\n"), -)) - - - -var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML)) -var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML)) -var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML)) -var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML)) -var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML)) -var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML)) -var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML)) - - - -var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{ - "parseLevel": parseLevel, - "titleCase": titleCase, - "toLower": toLower, - "prettyTime": prettyTime, -}).Parse(logsHTML)) - - - -var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( - strings.Join([]string{ - releasesHTML, - componentReleaseHTML, - }, "\n"), -)) -var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( - strings.Join([]string{ - artistsHTML, - componentArtistHTML, - }, "\n"), -)) -var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( - strings.Join([]string{ - tracksHTML, - componentTrackHTML, - }, "\n"), -)) - - - -var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( - strings.Join([]string{ - editReleaseHTML, - componentTrackHTML, - }, "\n"), -)) -var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML)) -var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( - strings.Join([]string{ - editTrackHTML, - componentReleaseHTML, - }, "\n"), -)) - - - -var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML)) -var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML)) -var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML)) - -var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML)) - -var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML)) -var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML)) -var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML)) - - - -func parseLevel(level log.LogLevel) string { - switch level { - case log.LEVEL_INFO: - return "INFO" - case log.LEVEL_WARN: - return "WARN" - } - return fmt.Sprintf("%d?", level) -} -func titleCase(logType string) string { - runes := []rune(logType) - for i, r := range runes { - if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { - runes[i] = r + ('A' - 'a') - } - } - return string(runes) -} -func toLower(str string) string { - return strings.ToLower(str) -} -func prettyTime(t time.Time) string { - // return t.Format("2006-01-02 15:04:05") - // return t.Format("15:04:05, 2 Jan 2006") - return t.Format("02 Jan 2006, 15:04:05") -} diff --git a/admin/trackhttp.go b/admin/trackhttp.go index bcb5220..93eacdb 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -1,57 +1,21 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/admin/templates" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/model" + "arimelody-web/controller" ) -func serveTracks(app *model.AppState) http.Handler { +func serveTrack(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/tracks")[1:], "/") - trackID := slices[0] - - if len(trackID) > 0 { - serveTrack(app, trackID).ServeHTTP(w, r) - return - } - - tracks, err := controller.GetAllTracks(app.DB) + slices := strings.Split(r.URL.Path[1:], "/") + id := slices[0] + track, err := controller.GetTrack(app.DB, id) if err != nil { - fmt.Printf("WARN: Failed to fetch tracks: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - type TracksResponse struct { - adminPageData - Tracks []*model.Track - } - - err = templates.TracksTemplate.Execute(w, TracksResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, - Tracks: tracks, - }) - if err != nil { - fmt.Printf("WARN: Failed to serve admin tracks page: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func serveTrack(app *model.AppState, trackID string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*model.Session) - - track, err := controller.GetTrack(app.DB, trackID) - if err != nil { - fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err) + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -62,25 +26,28 @@ func serveTrack(app *model.AppState, trackID string) http.Handler { releases, err := controller.GetTrackReleases(app.DB, track.ID, true) if err != nil { - fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err) + fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } type TrackResponse struct { - adminPageData + Session *model.Session Track *model.Track Releases []*model.Release } - err = templates.EditTrackTemplate.Execute(w, TrackResponse{ - adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + session := r.Context().Value("session").(*model.Session) + + err = trackTemplate.Execute(w, TrackResponse{ + Session: session, Track: track, Releases: releases, }) if err != nil { - fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err) + fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } + diff --git a/admin/templates/html/edit-account.html b/admin/views/edit-account.html similarity index 80% rename from admin/templates/html/edit-account.html rename to admin/views/edit-account.html index a63d37a..6c17088 100644 --- a/admin/templates/html/edit-account.html +++ b/admin/views/edit-account.html @@ -14,10 +14,10 @@ {{end}}

    Account Settings ({{.Session.Account.Username}})

    +
    +

    Change Password

    +
    -
    -

    Change Password

    -
    @@ -28,16 +28,14 @@ -
    -
    +
    +

    MFA Devices

    +
    -
    -

    MFA Devices

    -
    {{if .TOTPs}} {{range .TOTPs}}
    @@ -46,10 +44,7 @@

    Added: {{.CreatedAtString}}

    -
    - - -
    + Delete
    {{end}} @@ -57,30 +52,28 @@

    You have no MFA devices.

    {{end}} -
    +
    Add TOTP Device
    +
    +

    Danger Zone

    +
    -
    -

    Danger Zone

    -

    Clicking the button below will delete your account. This action is irreversible. You will need to enter your password and TOTP below.

    -
    + -
    -
    diff --git a/admin/templates/html/edit-artist.html b/admin/views/edit-artist.html similarity index 84% rename from admin/templates/html/edit-artist.html rename to admin/views/edit-artist.html index e9b829a..b0cfb41 100644 --- a/admin/templates/html/edit-artist.html +++ b/admin/views/edit-artist.html @@ -2,7 +2,6 @@ Editing {{.Artist.Name}} - ari melody 💫 - {{end}} {{define "content"}} @@ -30,16 +29,16 @@ -
    -
    -

    Featured in

    -
    +
    +

    Featured in

    +
    +
    {{if .Credits}} {{range .Credits}}
    -

    {{.Release.Title}}

    +

    {{.Release.Title}}

    {{.Release.PrintArtists true true}}

    Role: {{.Role}} @@ -55,10 +54,10 @@ {{end}}

    -
    -
    -

    Danger Zone

    -
    +
    +

    Danger Zone

    +
    +

    Clicking the button below will delete this artist. This action is irreversible. diff --git a/admin/templates/html/edit-release.html b/admin/views/edit-release.html similarity index 70% rename from admin/templates/html/edit-release.html rename to admin/views/edit-release.html index 6b47f75..02447e1 100644 --- a/admin/templates/html/edit-release.html +++ b/admin/views/edit-release.html @@ -2,13 +2,10 @@ Editing {{.Release.Title}} - ari melody 💫 - - {{end}} {{define "content"}}

    -

    Editing {{.Release.Title}}

    @@ -100,21 +97,21 @@
    -
    -
    -

    Credits ({{len .Release.Credits}} total)

    - Edit -
    +
    +

    Credits ({{len .Release.Credits}})

    + Edit +
    +
    {{range .Release.Credits}}
    -

    {{.Artist.Name}}

    +

    {{.Artist.Name}}

    {{.Role}} {{if .Primary}} @@ -129,42 +126,59 @@ {{end}}

    -
  • - +
    - catdance + catdance

    watch the cat dance 🐱

  • - +
    - pride flag + pride flag

    progressive pride flag widget for websites

  • @@ -172,16 +195,16 @@
  • - +
    - impact meme + impact meme

    impact meme generator

  • - OpenTerminal + OpenTerminal

    communal online text buffer

  • @@ -201,7 +224,7 @@
    - + ari melody web button @@ -213,9 +236,6 @@ thermia web button - - WangleLine button - elke web button @@ -243,9 +263,6 @@ isabel roses web button - - sweet like bubblegum web button -
    diff --git a/templates/html/layout.html b/views/layout.html similarity index 100% rename from templates/html/layout.html rename to views/layout.html diff --git a/templates/html/music-gateway.html b/views/music-gateway.html similarity index 90% rename from templates/html/music-gateway.html rename to views/music-gateway.html index febef4d..0f4441c 100644 --- a/templates/html/music-gateway.html +++ b/views/music-gateway.html @@ -6,22 +6,22 @@ - + - - + + - + - - - + + + - + @@ -34,7 +34,7 @@
    - < + <

    diff --git a/templates/html/music.html b/views/music.html similarity index 90% rename from templates/html/music.html rename to views/music.html index e7b4bd9..6f84672 100644 --- a/templates/html/music.html +++ b/views/music.html @@ -6,9 +6,9 @@ - - - + + + @@ -72,7 +72,7 @@

    music used: ari melody - free2play
    - https://arimelody.space/music/free2play
    + https://arimelody.me/music/free2play
    licensed under CC BY-SA 4.0.

    @@ -84,7 +84,7 @@ if you do happen to use my work in something you're particularly proud of, feel free to send it my way!

    - > ari@arimelody.space + > ari@arimelody.me

    diff --git a/admin/templates/html/prideflag.html b/views/prideflag.html similarity index 100% rename from admin/templates/html/prideflag.html rename to views/prideflag.html