feat(sponsors): drop the sidebar slot and simplify the Sponsors page

The sidebar no longer carries a sponsor card. The backend stops serving
the "sidebar" slot, so a sponsors.json entry that still lists it keeps
only its other slots and one listing nothing else is dropped. Its
rotation, three-sponsor cap and collapsed-rail logo rendering go with it.

The Sponsors page now lists only real sponsors: the "Your brand here"
card shows only as the empty state instead of sitting in the grid and
repeating the header's Become a sponsor button, and the Available
placements section and its slot-occupancy helper are removed. Cards are
larger, two per row on wide screens. Unused i18n keys are dropped from
every locale.
This commit is contained in:
MHSanaei
2026-10-13 23:32:56 +02:00
parent 319e693276
commit 6f1dd0d757
25 changed files with 68 additions and 545 deletions
+3 -10
View File
@@ -45,7 +45,6 @@ const (
sponsorsErrTTL = 10 * time.Minute
maxSponsorsBytes = 256 << 10
maxLogoBytes = 256 << 10
maxSidebarSlots = 3
localSponsorsDir = "../sponsors/3X"
sponsorLogoPath = "/sponsors/logo/"
)
@@ -67,7 +66,7 @@ var (
// The panel proxies logos from sponsorLogoBase: CSP stays 'self' and no third party sees admin IPs.
sponsorLogoRe = regexp.MustCompile(`^[A-Za-z0-9_-][A-Za-z0-9._-]*\.(png|webp|jpg)$`)
sponsorSlots = map[string]bool{"dashboard": true, "sidebar": true, "page": true, "login": true}
sponsorSlots = map[string]bool{"dashboard": true, "page": true, "login": true}
sponsorsMu sync.Mutex
sponsorsRaw *SponsorList
@@ -217,7 +216,6 @@ func activeSponsors(raw *SponsorList, now time.Time) *SponsorList {
if strings.HasPrefix(raw.Contact, "https://") {
out.Contact = raw.Contact
}
sidebarTaken := 0
for _, sp := range raw.Sponsors {
// A missing enable counts as on, so a forgotten field never hides a paid slot.
disabled := sp.Enable != nil && !*sp.Enable
@@ -233,18 +231,13 @@ func activeSponsors(raw *SponsorList, now time.Time) *SponsorList {
}
slots := make([]string, 0, len(sp.Slots))
for _, slot := range sp.Slots {
// The sidebar rotates, so capping it keeps each paid card on screen long enough.
if !sponsorSlots[slot] || (slot == "sidebar" && sidebarTaken >= maxSidebarSlots) {
continue
if sponsorSlots[slot] {
slots = append(slots, slot)
}
slots = append(slots, slot)
}
if len(slots) == 0 {
continue
}
if slices.Contains(slots, "sidebar") {
sidebarTaken++
}
sp.Slots = slots
out.Sponsors = append(out.Sponsors, sp)
}
+2 -31
View File
@@ -6,7 +6,6 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"slices"
"strings"
"sync/atomic"
"testing"
@@ -22,7 +21,7 @@ func validSponsor() Sponsor {
return Sponsor{
ID: "acme",
Name: "Acme",
Slots: []string{"dashboard", "sidebar"},
Slots: []string{"dashboard"},
Until: sponsorTestNow.Add(24 * time.Hour),
Logo: "acme.png",
Link: "https://acme.example/",
@@ -59,34 +58,6 @@ func TestActiveSponsorsFilters(t *testing.T) {
}
}
func TestActiveSponsorsCapsSidebarAtThree(t *testing.T) {
raw := &SponsorList{}
for _, id := range []string{"expired", "a", "b", "c", "d", "e"} {
sp := validSponsor()
sp.ID = id
sp.Slots = []string{"sidebar", "page"}
if id == "expired" {
sp.Until = sponsorTestNow
}
if id == "e" {
sp.Slots = []string{"sidebar"}
}
raw.Sponsors = append(raw.Sponsors, sp)
}
got := activeSponsors(raw, sponsorTestNow)
want := map[string][]string{
"a": {"sidebar", "page"}, "b": {"sidebar", "page"}, "c": {"sidebar", "page"}, "d": {"page"},
}
if len(got.Sponsors) != len(want) {
t.Fatalf("got %d sponsors, want %d (e has only sidebar and must drop)", len(got.Sponsors), len(want))
}
for _, sp := range got.Sponsors {
if !slices.Equal(sp.Slots, want[sp.ID]) {
t.Errorf("%s slots = %v, want %v", sp.ID, sp.Slots, want[sp.ID])
}
}
}
func TestActiveSponsorsLogoName(t *testing.T) {
cases := []struct{ logo, want string }{
{"acme.png", "/sponsors/logo/acme.png"},
@@ -114,7 +85,7 @@ func TestActiveSponsorsLogoName(t *testing.T) {
func TestActiveSponsorsResolvesLogoAndSlots(t *testing.T) {
sp := validSponsor()
sp.Slots = []string{"subpage", "login"}
sp.Slots = []string{"subpage", "sidebar", "login"}
got := activeSponsors(&SponsorList{Contact: "javascript:x", Sponsors: []Sponsor{sp}}, sponsorTestNow)
if len(got.Sponsors) != 1 {
t.Fatalf("got %d sponsors, want 1", len(got.Sponsors))