Add list styles: Podium, Spotlight, Grid, List
- Each list picks a style in List settings (new lists default to Podium); existing lists keep Grid or List - Podium: top 3 on medal-colored steps (#1 center and raised), the rest behind a native <details> "Show N more"; stacks #1 over #2/#3 on phones - Spotlight: #1 as a feature card (art, why-note or description, play button), the rest as a numbered list - Visitors can switch styles on a list page (?style=), preserved across pages; later pages fall back to a plain list since they don't start at #1 - Entries' detail data now lives on a wrapper per list, so featured and collapsed entries all open the detail view Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b09533cc00
commit
ddf2157e63
15 files changed
+286
-65
No files matched your search
@@ -44,6 +44,7 @@ func (s *Server) handleAdminLists(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, r, http.StatusOK, "admin_lists", "Your lists", adminPage{Boot: bootJSON(map[string]any{
|
||||
"lists": lists,
|
||||
"covers": covers,
|
||||
"styles": store.ListStyles,
|
||||
"categories": store.Categories,
|
||||
"limits": limits,
|
||||
})})
|
||||
@@ -72,6 +73,7 @@ func (s *Server) handleAdminList(w http.ResponseWriter, r *http.Request) {
|
||||
"limits": limits,
|
||||
"provider": provider,
|
||||
"productions": store.TheaterProductions,
|
||||
"styles": store.ListStyles,
|
||||
"pageSize": listPageSize,
|
||||
})})
|
||||
}
|
||||
|
||||
@@ -143,6 +143,9 @@ const listPageSize = 100
|
||||
type listPage struct {
|
||||
List store.List
|
||||
Category store.Category
|
||||
Style string // style being shown: the list's, or the visitor's pick
|
||||
Styles []store.ListStyle // for the style switcher
|
||||
PageQS string // extra query for pager links, e.g. "&style=grid"
|
||||
Items []ItemView
|
||||
Hero *ItemView
|
||||
Page int
|
||||
@@ -180,6 +183,10 @@ func (s *Server) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
site := s.site(ctx)
|
||||
p := listPage{List: l, Page: 1}
|
||||
p.Category = listCategory(l)
|
||||
p.Style, p.Styles = l.Layout, store.ListStyles
|
||||
if st, ok := store.StyleByKey(r.URL.Query().Get("style")); ok && st.Key != l.Layout {
|
||||
p.Style, p.PageQS = st.Key, "&style="+st.Key
|
||||
}
|
||||
p.Pages = max(1, (l.ItemCount+listPageSize-1)/listPageSize)
|
||||
if n, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && n > 1 {
|
||||
p.Page = min(n, p.Pages)
|
||||
|
||||
@@ -103,6 +103,17 @@ var funcs = template.FuncMap{
|
||||
},
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
// first and after split a ranked list: the podium/spotlight entries and
|
||||
// everything below them.
|
||||
"first": func(n int, items []ItemView) []ItemView { return items[:min(n, len(items))] },
|
||||
"after": func(n int, items []ItemView) []ItemView { return items[min(n, len(items)):] },
|
||||
// flatLayout is how a style shows entries that aren't featured.
|
||||
"flatLayout": func(style string) string {
|
||||
if style == "grid" {
|
||||
return "grid"
|
||||
}
|
||||
return "rows"
|
||||
},
|
||||
// dict builds a map for passing several values to a partial.
|
||||
"dict": func(kv ...any) map[string]any {
|
||||
m := map[string]any{}
|
||||
|
||||
+29
-3
@@ -24,7 +24,7 @@ type List struct {
|
||||
MaxItems int `json:"maxItems"`
|
||||
Position int `json:"position"`
|
||||
Visible bool `json:"visible"`
|
||||
Layout string `json:"layout"` // "grid" or "rows"
|
||||
Layout string `json:"layout"` // a ListStyles key, e.g. "podium"
|
||||
Art string `json:"art"` // "", "poster" or "square"; "" = category default
|
||||
ItemCount int `json:"itemCount"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
@@ -97,8 +97,8 @@ func (l *List) normalize() error {
|
||||
if l.MaxItems < 1 || l.MaxItems > MaxListSize {
|
||||
return invalid("List size must be between 1 and %d", MaxListSize)
|
||||
}
|
||||
if l.Layout != "rows" {
|
||||
l.Layout = "grid"
|
||||
if _, ok := StyleByKey(l.Layout); !ok {
|
||||
l.Layout = DefaultStyle
|
||||
}
|
||||
if l.Art != "poster" && l.Art != "square" {
|
||||
l.Art = ""
|
||||
@@ -245,3 +245,29 @@ func (l List) Aspect() string {
|
||||
c, _ := CategoryByKey(l.Category)
|
||||
return c.Aspect
|
||||
}
|
||||
|
||||
// ListStyle is a way of presenting a ranked list.
|
||||
type ListStyle struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
var ListStyles = []ListStyle{
|
||||
{"podium", "Podium", "podium", "Top 3 on a medal podium, the rest behind “Show more”."},
|
||||
{"spotlight", "Spotlight", "spotlight", "#1 as a big feature, the rest as a ranked list."},
|
||||
{"grid", "Grid", "grid", "Every entry as artwork in a grid."},
|
||||
{"rows", "List", "rows", "A compact numbered list with notes."},
|
||||
}
|
||||
|
||||
const DefaultStyle = "podium"
|
||||
|
||||
func StyleByKey(key string) (ListStyle, bool) {
|
||||
for _, s := range ListStyles {
|
||||
if s.Key == key {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return ListStyle{}, false
|
||||
}
|
||||
Reference in new issue
Block a user