Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/streambinder/foedus/internal/models"
_ "modernc.org/sqlite"
)

Expand Down Expand Up @@ -195,7 +196,13 @@ func migrate() {
bank_account_iban TEXT NOT NULL DEFAULT '',
bank_account_holder TEXT NOT NULL DEFAULT '',
spotify_playlist TEXT NOT NULL DEFAULT '',
share_preview_media_id INTEGER REFERENCES media(id)
share_preview_media_id INTEGER REFERENCES media(id),
envelope_color TEXT NOT NULL DEFAULT '` + models.DefaultEnvelopeColor + `',
page_bg_color TEXT NOT NULL DEFAULT '` + models.DefaultPageBgColor + `',
home_bg_color TEXT NOT NULL DEFAULT '` + models.DefaultHomeBgColor + `',
stamp_media_id INTEGER REFERENCES media(id),
flora_bl_media_id INTEGER REFERENCES media(id),
flora_tr_media_id INTEGER REFERENCES media(id)
) STRICT`,
`CREATE TABLE IF NOT EXISTS registry_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
Expand Down
17 changes: 14 additions & 3 deletions internal/database/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
const settingsColumns = `groom_name, bride_name,
ceremony_datetime, ceremony_address, ceremony_location, ceremony_city, ceremony_lat, ceremony_lng, ceremony_media_id,
reception_datetime, reception_address, reception_location, reception_city, reception_lat, reception_lng, reception_media_id,
bank_account_iban, bank_account_holder, spotify_playlist, share_preview_media_id`
bank_account_iban, bank_account_holder, spotify_playlist, share_preview_media_id,
envelope_color, page_bg_color, home_bg_color, stamp_media_id, flora_bl_media_id, flora_tr_media_id`

// seedSettings materialises the single settings row. Every read assumes it
// exists, so this runs on every boot rather than only on a fresh database.
Expand All @@ -24,7 +25,7 @@ func seedSettings() {

func GetSettings() (models.Settings, error) {
var settings models.Settings
var ceremonyMediaID, receptionMediaID, sharePreviewMediaID sql.NullInt64
var ceremonyMediaID, receptionMediaID, sharePreviewMediaID, stampMediaID, floraBlMediaID, floraTrMediaID sql.NullInt64
err := DB.QueryRow(`SELECT `+settingsColumns+` FROM settings WHERE id = 1`).Scan(
&settings.GroomName, &settings.BrideName,
&settings.CeremonyDatetime, &settings.CeremonyAddress, &settings.CeremonyLocation,
Expand All @@ -33,13 +34,18 @@ func GetSettings() (models.Settings, error) {
&settings.ReceptionCity, &settings.ReceptionLat, &settings.ReceptionLng, &receptionMediaID,
&settings.BankAccountIBAN, &settings.BankAccountHolder,
&settings.SpotifyPlaylist, &sharePreviewMediaID,
&settings.EnvelopeColor, &settings.PageBgColor, &settings.HomeBgColor,
&stampMediaID, &floraBlMediaID, &floraTrMediaID,
)
if err != nil {
return models.Settings{}, err
}
settings.CeremonyMediaID = idOrZero(ceremonyMediaID)
settings.ReceptionMediaID = idOrZero(receptionMediaID)
settings.SharePreviewMediaID = idOrZero(sharePreviewMediaID)
settings.StampMediaID = idOrZero(stampMediaID)
settings.FloraBlMediaID = idOrZero(floraBlMediaID)
settings.FloraTrMediaID = idOrZero(floraTrMediaID)
return settings, nil
}

Expand All @@ -51,7 +57,9 @@ func UpdateSettings(q Querier, settings models.Settings) error {
ceremony_lat = ?, ceremony_lng = ?, ceremony_media_id = ?,
reception_datetime = ?, reception_address = ?, reception_location = ?, reception_city = ?,
reception_lat = ?, reception_lng = ?, reception_media_id = ?,
bank_account_iban = ?, bank_account_holder = ?, spotify_playlist = ?, share_preview_media_id = ?
bank_account_iban = ?, bank_account_holder = ?, spotify_playlist = ?, share_preview_media_id = ?,
envelope_color = ?, page_bg_color = ?, home_bg_color = ?,
stamp_media_id = ?, flora_bl_media_id = ?, flora_tr_media_id = ?
WHERE id = 1`,
settings.GroomName, settings.BrideName,
settings.CeremonyDatetime, settings.CeremonyAddress, settings.CeremonyLocation, settings.CeremonyCity,
Expand All @@ -60,6 +68,9 @@ func UpdateSettings(q Querier, settings models.Settings) error {
settings.ReceptionLat, settings.ReceptionLng, nullableID(settings.ReceptionMediaID),
settings.BankAccountIBAN, settings.BankAccountHolder,
settings.SpotifyPlaylist, nullableID(settings.SharePreviewMediaID),
settings.EnvelopeColor, settings.PageBgColor, settings.HomeBgColor,
nullableID(settings.StampMediaID),
nullableID(settings.FloraBlMediaID), nullableID(settings.FloraTrMediaID),
)
return err
}
51 changes: 51 additions & 0 deletions internal/database/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,54 @@ func TestHomepageLabelsRoundTripPerLanguage(t *testing.T) {
t.Errorf("got %v for an unconfigured language, want empty", missing)
}
}

// Appearance columns ride the same typed settings row as everything else:
// defaults seeded on a fresh row, values round-trip, absent flora overrides
// read back as NULL/0.
func TestSettingsAppearanceRoundTrip(t *testing.T) {
newTestDB(t)

got, err := GetSettings()
if err != nil {
t.Fatal(err)
}
if got.EnvelopeColor != models.DefaultEnvelopeColor || got.PageBgColor != models.DefaultPageBgColor ||
got.HomeBgColor != models.DefaultHomeBgColor {
t.Errorf("seeded colours %q/%q/%q, want defaults %q/%q/%q",
got.EnvelopeColor, got.PageBgColor, got.HomeBgColor,
models.DefaultEnvelopeColor, models.DefaultPageBgColor, models.DefaultHomeBgColor)
}
if got.StampMediaID != 0 || got.FloraBlMediaID != 0 || got.FloraTrMediaID != 0 {
t.Errorf("seeded image ids %d/%d/%d, want 0/0/0", got.StampMediaID, got.FloraBlMediaID, got.FloraTrMediaID)
}

floraID, err := InsertMedia(DB, "image/webp", []byte("flora"))
if err != nil {
t.Fatal(err)
}
stampID, err := InsertMedia(DB, "image/png", []byte("stamp"))
if err != nil {
t.Fatal(err)
}
want := models.Settings{
GroomName: "Davide", BrideName: "Agnese",
EnvelopeColor: "#123abc", PageBgColor: "#f0e6d8", HomeBgColor: "#e8f0e0",
StampMediaID: stampID, FloraBlMediaID: floraID,
}
if err := UpdateSettings(DB, want); err != nil {
t.Fatal(err)
}

got, err = GetSettings()
if err != nil {
t.Fatal(err)
}
if got.EnvelopeColor != "#123abc" || got.PageBgColor != "#f0e6d8" || got.HomeBgColor != "#e8f0e0" {
t.Errorf("colours round-tripped as %q/%q/%q, want #123abc/#f0e6d8/#e8f0e0",
got.EnvelopeColor, got.PageBgColor, got.HomeBgColor)
}
if got.StampMediaID != stampID || got.FloraBlMediaID != floraID || got.FloraTrMediaID != 0 {
t.Errorf("image ids round-tripped as %d/%d/%d, want %d/%d/0",
got.StampMediaID, got.FloraBlMediaID, got.FloraTrMediaID, stampID, floraID)
}
}
32 changes: 31 additions & 1 deletion internal/handlers/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,9 @@ func collectExistingMediaIDs(settings models.Settings, content templates.Setting
add(settings.CeremonyMediaID)
add(settings.ReceptionMediaID)
add(settings.SharePreviewMediaID)
add(settings.StampMediaID)
add(settings.FloraBlMediaID)
add(settings.FloraTrMediaID)
for _, place := range slices.Concat(content.Places, content.Honeymoon) {
add(place.MediaID)
}
Expand Down Expand Up @@ -495,7 +498,31 @@ func SaveSettings(c *fiber.Ctx) error {
SpotifyPlaylist: strings.TrimSpace(c.FormValue("spotify_playlist")),
}

// the three single-image fields resolve inside the tx: each may insert new
// appearance colours: the native picker always posts a value, but an
// empty field keeps the saved colour and anything else must be a strict
// #rrggbb, lowercased for a canonical stored form.
for _, colour := range []struct {
field string
existing string
target *string
}{
{"envelope_color", settings.EnvelopeColor, &updated.EnvelopeColor},
{"page_bg_color", settings.PageBgColor, &updated.PageBgColor},
{"home_bg_color", settings.HomeBgColor, &updated.HomeBgColor},
} {
raw := strings.ToLower(strings.TrimSpace(c.FormValue(colour.field)))
switch {
case raw == "":
*colour.target = colour.existing
case !models.IsHexColor(raw):
logger.Warn("settings save rejected", "field", colour.field, "value", raw)
return fiber.NewError(400, "invalid colour")
default:
*colour.target = raw
}
}

// the five single-image fields resolve inside the tx: each may insert new
// bytes or drop the row it replaces.
for _, image := range []struct {
field string
Expand All @@ -505,6 +532,9 @@ func SaveSettings(c *fiber.Ctx) error {
{"ceremony", settings.CeremonyMediaID, &updated.CeremonyMediaID},
{"reception", settings.ReceptionMediaID, &updated.ReceptionMediaID},
{"share_preview", settings.SharePreviewMediaID, &updated.SharePreviewMediaID},
{"stamp", settings.StampMediaID, &updated.StampMediaID},
{"flora_bl", settings.FloraBlMediaID, &updated.FloraBlMediaID},
{"flora_tr", settings.FloraTrMediaID, &updated.FloraTrMediaID},
} {
mediaID, err := resolveImageMediaID(tx, c.FormValue(image.field+"_image"), c.FormValue(image.field+"_media_id"), image.existing, true)
if err != nil {
Expand Down
62 changes: 48 additions & 14 deletions internal/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,30 @@ var translations = map[string]map[string]string{
"title.setup": "Setup Required",

// dashboard - sections
"dashboard.title": "Wedding Dashboard",
"dashboard.details": "Settings",
"dashboard.guests": "Guests",
"dashboard.track_additions": "Track Additions",
"dashboard.title": "Wedding Dashboard",
"dashboard.details": "Settings",
"dashboard.guests": "Guests",
"dashboard.track_additions": "Track Additions",
"dashboard.appearance": "Appearance",
"dashboard.appearance_description": "Colours of the invitation envelope and page, plus the decorative corner graphics.",

// dashboard - labels
"label.spouses": "Couple's Names",
"label.ceremony": "Ceremony",
"label.reception": "Celebration",
"label.spouses": "Couple's Names",
"label.ceremony": "Ceremony",
"label.reception": "Celebration",
"label.envelope_color": "Envelope colour",
"label.page_bg_color": "Invitation page background",
"label.home_bg_color": "Homepage background",
"label.stamp": "Envelope stamp",
"label.flora_bl": "Corner decoration · bottom-left",
"label.flora_tr": "Corner decoration · top-right",

// appearance section - descriptions
"appearance.envelope_color_description": "Base colour of the animated envelope on the invitation page. Shadows and highlights derive from it automatically.",
"appearance.page_bg_color_description": "Background of the invitation page, behind the text and venues.",
"appearance.home_bg_color_description": "Background of the homepage, behind the scrolling sections.",
"appearance.stamp_description": "Replaces the wax seal on the invitation envelope. Remove restores the default.",
"appearance.flora_description": "Replaces the built-in eucalyptus corner graphic on the homepage and ceremony pages. Remove restores the default.",

// dashboard - counters
"counter.rsvp": "RSVP",
Expand Down Expand Up @@ -112,6 +127,8 @@ var translations = map[string]map[string]string{
"btn.update_poll": "Update Poll",
"btn.edit": "Edit",
"btn.delete": "Delete",
"btn.remove": "Remove",
"btn.reset_default": "Reset to default",
"btn.reset_viewed": "Reset viewed",
"btn.move_up": "Move Up",
"btn.move_down": "Move Down",
Expand Down Expand Up @@ -344,15 +361,30 @@ var translations = map[string]map[string]string{
"title.setup": "Configurazione necessaria",

// dashboard - sections
"dashboard.title": "Pannello",
"dashboard.details": "Impostazioni",
"dashboard.guests": "Invitati",
"dashboard.track_additions": "Brani aggiunti",
"dashboard.title": "Pannello",
"dashboard.details": "Impostazioni",
"dashboard.guests": "Invitati",
"dashboard.track_additions": "Brani aggiunti",
"dashboard.appearance": "Aspetto",
"dashboard.appearance_description": "Colori della busta e della pagina dell'invito, più le grafiche decorative angolari.",

// dashboard - labels
"label.spouses": "Nomi degli sposi",
"label.ceremony": "Rito",
"label.reception": "Festa",
"label.spouses": "Nomi degli sposi",
"label.ceremony": "Rito",
"label.reception": "Festa",
"label.envelope_color": "Colore della busta",
"label.page_bg_color": "Sfondo della pagina dell'invito",
"label.home_bg_color": "Sfondo della homepage",
"label.stamp": "Timbro della busta",
"label.flora_bl": "Decorazione angolare · in basso a sinistra",
"label.flora_tr": "Decorazione angolare · in alto a destra",

// appearance section - descriptions
"appearance.envelope_color_description": "Colore di base della busta animata nella pagina dell'invito. Ombre e riflessi derivano automaticamente.",
"appearance.page_bg_color_description": "Sfondo della pagina dell'invito, dietro testi e luoghi.",
"appearance.home_bg_color_description": "Sfondo della homepage, dietro le sezioni a scorrimento.",
"appearance.stamp_description": "Sostituisce il sigillo di ceralacca sulla busta dell'invito. Rimuovi per tornare al default.",
"appearance.flora_description": "Sostituisce la grafica d'eucalipto predefinita negli angoli di homepage e pagina del rito. Rimuovi per tornare al default.",

// dashboard - counters
"counter.rsvp": "RSVP",
Expand Down Expand Up @@ -417,6 +449,8 @@ var translations = map[string]map[string]string{
"btn.update_poll": "Aggiorna Sondaggio",
"btn.edit": "Modifica",
"btn.delete": "Elimina",
"btn.remove": "Rimuovi",
"btn.reset_default": "Ripristina default",
"btn.reset_viewed": "Reset visione",
"btn.move_up": "Sposta Su",
"btn.move_down": "Sposta Giù",
Expand Down
31 changes: 30 additions & 1 deletion internal/models/models.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
package models

import "time"
import (
"regexp"
"time"
)

// Default appearance colours: the sage envelope, white invitation page and
// ivory homepage the site shipped with before the dashboard pickers existed.
const (
DefaultEnvelopeColor = "#6f7665"
DefaultPageBgColor = "#ffffff"
DefaultHomeBgColor = "#fcf8f3"
)

var hexColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)

// IsHexColor reports whether s is a strict #rrggbb colour. Both the dashboard
// handler and the invitation template gate on this, so a hand-edited settings
// row can never inject into a style attribute.
func IsHexColor(s string) bool {
return hexColorPattern.MatchString(s)
}

// place kinds — same entity, two roles on the homepage: story places render on
// the timeline, honeymoon ones in their own section.
Expand Down Expand Up @@ -32,6 +52,15 @@ type Settings struct {
BankAccountHolder string
SpotifyPlaylist string
SharePreviewMediaID int
// Appearance section: envelope and page colours, the wax stamp override
// and overrides for the two decorative flora corners (media IDs of 0
// mean the built-in defaults).
EnvelopeColor string
PageBgColor string
HomeBgColor string
StampMediaID int
FloraBlMediaID int
FloraTrMediaID int
}

func (s Settings) IsConfigured() bool {
Expand Down
23 changes: 23 additions & 0 deletions internal/models/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,26 @@ func TestEventPassedAt(t *testing.T) {
})
}
}

func TestIsHexColor(t *testing.T) {
for _, tc := range []struct {
in string
want bool
}{
{"#6f7665", true},
{"#FFFFFF", true},
{"#123abc", true},
{"", false},
{"6f7665", false},
{"#fff", false},
{"#gggggg", false},
{"#1234567", false},
{"red", false},
{"#6f7665;", false},
{"\"#6f7665\"", false},
} {
if got := IsHexColor(tc.in); got != tc.want {
t.Errorf("IsHexColor(%q) = %v, want %v", tc.in, got, tc.want)
}
}
}
29 changes: 29 additions & 0 deletions static/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -1283,3 +1283,32 @@ code {
align-items: center;
}
}

/* Appearance section: native colour pickers with a hex readout. */
.appearance-color-field {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.25rem;
}

.appearance-color-field label {
margin-bottom: 0;
min-width: 12rem;
}

.appearance-color-field input[type="color"] {
width: 3rem;
height: 2.25rem;
padding: 0.15rem;
border: 1px solid var(--dashboard-border, #d8d4cc);
border-radius: 0.375rem;
background: none;
cursor: pointer;
}

.appearance-color-field .color-hex {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.85rem;
color: var(--dashboard-muted, #6b675f);
}
Loading
Loading