A single Go binary with SQLite that serves a public page of the owner's ranked lists (movies, TV, albums, songs, video games, theater) and a login-gated editor. - Lists of 1–65,535 entries (100 recommended), several per category - Search-to-add via TMDB, TheTVDB, IGDB, Deezer/MusicBrainz, Wikipedia; manual entry as a fallback - Theater entries carry a production (Broadway, tour, …), venue and cast - Artwork cached locally; URL override and uploads - Drag, arrow and type-a-rank reordering; paginated for long lists - 12 theme presets plus custom CSS - First-run setup code or env-provided admin; bcrypt, rate-limited login, CSRF-guarded JSON API - Dockerfile and docker-compose.yml Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
2.0 KiB
Go
84 lines
2.0 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"html/template"
|
|
"net/http"
|
|
|
|
"github.com/cdnutter/topdeck/internal/store"
|
|
)
|
|
|
|
// adminPage carries a JSON blob the page script boots from.
|
|
type adminPage struct {
|
|
Boot template.JS
|
|
List *store.List
|
|
}
|
|
|
|
func bootJSON(v any) template.JS {
|
|
b, _ := json.Marshal(v)
|
|
return template.JS(b)
|
|
}
|
|
|
|
type sizeLimits struct {
|
|
Max int `json:"max"`
|
|
Recommended int `json:"recommended"`
|
|
Warn int `json:"warn"`
|
|
}
|
|
|
|
var limits = sizeLimits{store.MaxListSize, store.RecommendedListSize, store.SoftListSizeWarning}
|
|
|
|
func (s *Server) handleAdminLists(w http.ResponseWriter, r *http.Request) {
|
|
lists, err := s.store.Lists(r.Context(), true)
|
|
if err != nil {
|
|
s.serverError(w, r, err)
|
|
return
|
|
}
|
|
if lists == nil {
|
|
lists = []store.List{}
|
|
}
|
|
covers, err := s.store.ListCovers(r.Context(), 3)
|
|
if err != nil {
|
|
s.serverError(w, r, err)
|
|
return
|
|
}
|
|
s.render(w, r, http.StatusOK, "admin_lists", "Your lists", adminPage{Boot: bootJSON(map[string]any{
|
|
"lists": lists,
|
|
"covers": covers,
|
|
"categories": store.Categories,
|
|
"limits": limits,
|
|
})})
|
|
}
|
|
|
|
func (s *Server) handleAdminList(w http.ResponseWriter, r *http.Request) {
|
|
id, err := pathID(r)
|
|
if err != nil {
|
|
s.notFound(w, r)
|
|
return
|
|
}
|
|
l, err := s.store.ListByID(r.Context(), id)
|
|
if err != nil {
|
|
s.notFound(w, r)
|
|
return
|
|
}
|
|
cat, _ := store.CategoryByKey(l.Category)
|
|
provider := ""
|
|
if p, err := s.providers.For(r.Context(), l.Category); err == nil {
|
|
provider = p.Label()
|
|
}
|
|
s.render(w, r, http.StatusOK, "admin_list", l.Title, adminPage{List: &l, Boot: bootJSON(map[string]any{
|
|
"list": l,
|
|
"category": cat,
|
|
"categories": store.Categories,
|
|
"limits": limits,
|
|
"provider": provider,
|
|
"productions": store.TheaterProductions,
|
|
"pageSize": listPageSize,
|
|
})})
|
|
}
|
|
|
|
func (s *Server) handleAdminSettings(w http.ResponseWriter, r *http.Request) {
|
|
s.render(w, r, http.StatusOK, "admin_settings", "Settings", adminPage{Boot: bootJSON(map[string]any{
|
|
"categories": store.Categories,
|
|
})})
|
|
}
|