From 0b3301f92152d104140754aac3212cf1d2d31b41 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Mon, 9 Feb 2026 19:30:47 +1100 Subject: [PATCH] refactored view package --- internal/db/getbyfield.go | 22 +- internal/db/getlist.go | 12 + internal/embedfs/web/css/output.css | 151 ++++++++++++ internal/handlers/admin_dashboard.go | 4 +- internal/handlers/admin_users.go | 4 +- internal/handlers/callback.go | 19 +- internal/handlers/errorpage.go | 6 +- internal/handlers/errors.go | 9 + internal/handlers/index.go | 7 +- internal/handlers/newseason.go | 4 +- internal/handlers/notifswebsocket.go | 28 +-- internal/handlers/notifytest.go | 4 +- internal/handlers/register.go | 4 +- internal/handlers/season.go | 4 +- internal/handlers/seasons.go | 6 +- internal/validation/validation.go | 4 +- .../view/adminview/dashboard_layout.templ | 11 + internal/view/adminview/dashboard_page.templ | 9 + .../admin => adminview}/user_list.templ | 2 +- .../register_form.templ} | 4 +- .../register_page.templ} | 11 +- .../error.templ => baseview/error_page.templ} | 11 +- internal/view/baseview/footer.templ | 128 ++++++++++ .../global.templ => baseview/layout.templ} | 15 +- internal/view/baseview/navbar.templ | 233 ++++++++++++++++++ internal/view/component/footer/footer.templ | 116 --------- internal/view/component/nav/navbar.templ | 41 --- internal/view/component/nav/navbarleft.templ | 19 -- internal/view/component/nav/navbarright.templ | 131 ---------- internal/view/component/nav/sidenav.templ | 45 ---- .../view/{component => }/datepicker/README.md | 0 .../datepicker/datepicker.templ | 0 .../index.templ => homeview/index_page.templ} | 8 +- internal/view/layout/admin_dashboard.templ | 8 - internal/view/page/admin_dashboard.templ | 10 - .../pagination/pagination.templ | 0 .../popup/errorModalContainer.templ | 0 .../{component => }/popup/errorModalWS.templ | 0 .../view/{component => }/popup/toast.templ | 0 .../popup/toastContainer.templ | 0 .../detail_page.templ} | 11 +- .../list_page.templ} | 15 +- .../new_form.templ} | 6 +- .../new_page.templ} | 11 +- .../status_badge.templ} | 2 +- .../view/{component => }/sort/dropdown.templ | 0 .../notification_test_page.templ} | 8 +- 47 files changed, 653 insertions(+), 490 deletions(-) create mode 100644 internal/view/adminview/dashboard_layout.templ create mode 100644 internal/view/adminview/dashboard_page.templ rename internal/view/{component/admin => adminview}/user_list.templ (84%) rename internal/view/{component/form/register.templ => authview/register_form.templ} (98%) rename internal/view/{page/register.templ => authview/register_page.templ} (68%) rename internal/view/{page/error.templ => baseview/error_page.templ} (86%) create mode 100644 internal/view/baseview/footer.templ rename internal/view/{layout/global.templ => baseview/layout.templ} (79%) create mode 100644 internal/view/baseview/navbar.templ delete mode 100644 internal/view/component/footer/footer.templ delete mode 100644 internal/view/component/nav/navbar.templ delete mode 100644 internal/view/component/nav/navbarleft.templ delete mode 100644 internal/view/component/nav/navbarright.templ delete mode 100644 internal/view/component/nav/sidenav.templ rename internal/view/{component => }/datepicker/README.md (100%) rename internal/view/{component => }/datepicker/datepicker.templ (100%) rename internal/view/{page/index.templ => homeview/index_page.templ} (56%) delete mode 100644 internal/view/layout/admin_dashboard.templ delete mode 100644 internal/view/page/admin_dashboard.templ rename internal/view/{component => }/pagination/pagination.templ (100%) rename internal/view/{component => }/popup/errorModalContainer.templ (100%) rename internal/view/{component => }/popup/errorModalWS.templ (100%) rename internal/view/{component => }/popup/toast.templ (100%) rename internal/view/{component => }/popup/toastContainer.templ (100%) rename internal/view/{page/season.templ => seasonsview/detail_page.templ} (94%) rename internal/view/{page/seasons_list.templ => seasonsview/list_page.templ} (87%) rename internal/view/{component/form/new_season.templ => seasonsview/new_form.templ} (98%) rename internal/view/{page/new_season.templ => seasonsview/new_page.templ} (68%) rename internal/view/{component/season/status.templ => seasonsview/status_badge.templ} (99%) rename internal/view/{component => }/sort/dropdown.templ (100%) rename internal/view/{page/test.templ => testview/notification_test_page.templ} (94%) diff --git a/internal/db/getbyfield.go b/internal/db/getbyfield.go index 70767e2..74cdba7 100644 --- a/internal/db/getbyfield.go +++ b/internal/db/getbyfield.go @@ -8,39 +8,27 @@ import ( "github.com/uptrace/bun" ) -type ListFilter struct { - filters []Filter -} - -func NewListFilter() *ListFilter { - return &ListFilter{[]Filter{}} -} - -func (f *ListFilter) Add(field string, value any) { - f.filters = append(f.filters, Filter{field, value}) -} - type fieldgetter[T any] struct { q *bun.SelectQuery field string value any + model *T } func (g *fieldgetter[T]) get(ctx context.Context) (*T, error) { if g.field == "id" && (g.value).(int) < 1 { return nil, errors.New("invalid id") } - model := new(T) err := g.q. Where("? = ?", bun.Ident(g.field), g.value). - Scan(ctx, model) + Scan(ctx) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, errors.Wrap(err, "bun.SelectQuery.Scan") } - return model, nil + return g.model, nil } func (g *fieldgetter[T]) GetFirst(ctx context.Context) (*T, error) { @@ -68,10 +56,12 @@ func GetByField[T any]( field string, value any, ) *fieldgetter[T] { + model := new(T) return &fieldgetter[T]{ - tx.NewSelect().Model((*T)(nil)), + tx.NewSelect().Model(model), field, value, + model, } } diff --git a/internal/db/getlist.go b/internal/db/getlist.go index e309c34..8e4719c 100644 --- a/internal/db/getlist.go +++ b/internal/db/getlist.go @@ -26,6 +26,18 @@ type Filter struct { Value any } +type ListFilter struct { + filters []Filter +} + +func NewListFilter() *ListFilter { + return &ListFilter{[]Filter{}} +} + +func (f *ListFilter) Add(field string, value any) { + f.filters = append(f.filters, Filter{field, value}) +} + func GetList[T any](tx bun.Tx, pageOpts, defaults *PageOpts) *listgetter[T] { l := &listgetter[T]{ items: new([]*T), diff --git a/internal/embedfs/web/css/output.css b/internal/embedfs/web/css/output.css index cce1f74..5141f60 100644 --- a/internal/embedfs/web/css/output.css +++ b/internal/embedfs/web/css/output.css @@ -45,6 +45,36 @@ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); + --color-rosewater: var(--rosewater); + --color-flamingo: var(--flamingo); + --color-pink: var(--pink); + --color-mauve: var(--mauve); + --color-red: var(--red); + --color-dark-red: var(--dark-red); + --color-maroon: var(--maroon); + --color-peach: var(--peach); + --color-yellow: var(--yellow); + --color-dark-yellow: var(--dark-yellow); + --color-green: var(--green); + --color-dark-green: var(--dark-green); + --color-teal: var(--teal); + --color-sky: var(--sky); + --color-sapphire: var(--sapphire); + --color-blue: var(--blue); + --color-dark-blue: var(--dark-blue); + --color-lavender: var(--lavender); + --color-text: var(--text); + --color-subtext1: var(--subtext1); + --color-subtext0: var(--subtext0); + --color-overlay2: var(--overlay2); + --color-overlay1: var(--overlay1); + --color-overlay0: var(--overlay0); + --color-surface2: var(--surface2); + --color-surface1: var(--surface1); + --color-surface0: var(--surface0); + --color-base: var(--base); + --color-mantle: var(--mantle); + --color-crust: var(--crust); } } @layer base { @@ -243,6 +273,9 @@ .top-0 { top: calc(var(--spacing) * 0); } + .top-1 { + top: calc(var(--spacing) * 1); + } .top-1\/2 { top: calc(1/2 * 100%); } @@ -276,6 +309,24 @@ .z-50 { z-index: 50; } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } .mx-auto { margin-inline: auto; } @@ -442,9 +493,22 @@ .flex-1 { flex: 1; } + .flex-shrink { + flex-shrink: 1; + } .shrink-0 { flex-shrink: 0; } + .grow { + flex-grow: 1; + } + .border-collapse { + border-collapse: collapse; + } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -546,6 +610,11 @@ border-color: var(--surface2); } } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .overflow-hidden { overflow: hidden; } @@ -838,6 +907,9 @@ .italic { font-style: italic; } + .underline { + text-decoration-line: underline; + } .opacity-50 { opacity: 50%; } @@ -853,6 +925,13 @@ --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } .transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -1434,6 +1513,64 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { @@ -1465,6 +1602,20 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; } } } diff --git a/internal/handlers/admin_dashboard.go b/internal/handlers/admin_dashboard.go index cf49316..25404e7 100644 --- a/internal/handlers/admin_dashboard.go +++ b/internal/handlers/admin_dashboard.go @@ -6,7 +6,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" - "git.haelnorr.com/h/oslstats/internal/view/page" + adminview "git.haelnorr.com/h/oslstats/internal/view/adminview" "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -24,6 +24,6 @@ func AdminDashboard(s *hws.Server, conn *bun.DB) http.Handler { }); !ok { return } - renderSafely(page.AdminDashboard(users), s, r, w) + renderSafely(adminview.DashboardPage(users), s, r, w) }) } diff --git a/internal/handlers/admin_users.go b/internal/handlers/admin_users.go index 6bab1e2..53bc156 100644 --- a/internal/handlers/admin_users.go +++ b/internal/handlers/admin_users.go @@ -6,7 +6,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" - "git.haelnorr.com/h/oslstats/internal/view/component/admin" + adminview "git.haelnorr.com/h/oslstats/internal/view/adminview" "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -29,6 +29,6 @@ func AdminUsersList(s *hws.Server, conn *bun.DB) http.Handler { }); !ok { return } - renderSafely(admin.UserList(users), s, r, w) + renderSafely(adminview.UserList(users), s, r, w) }) } diff --git a/internal/handlers/callback.go b/internal/handlers/callback.go index 4607557..ff310ee 100644 --- a/internal/handlers/callback.go +++ b/internal/handlers/callback.go @@ -15,6 +15,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/discord" "git.haelnorr.com/h/oslstats/internal/store" "git.haelnorr.com/h/oslstats/internal/throw" + "git.haelnorr.com/h/oslstats/internal/validation" "git.haelnorr.com/h/oslstats/pkg/oauth" ) @@ -36,15 +37,23 @@ func Callback( throw.BadRequest(s, w, r, "Too many redirects. Please try logging in again.", err) return } - - state := r.URL.Query().Get("state") - code := r.URL.Query().Get("code") - if state == "" && code == "" { - http.Redirect(w, r, "/", http.StatusBadRequest) + getter := validation.NewQueryGetter(r) + state := getter.String("state").Required().Value + code := getter.String("code").Required().Value + if !getter.Validate() { + store.ClearRedirectTrack(r, "/callback") + apiErr := getter.String("error").Value + errDesc := getter.String("error_description").Value + if apiErr == "access_denied" { + throw.Unauthorized(s, w, r, "OAuth login failed or cancelled", errors.New(errDesc)) + return + } + throw.BadRequest(s, w, r, "OAuth login failed", errors.New("state or code parameters missing")) return } data, err := verifyState(cfg.OAuth, w, r, state) if err != nil { + store.ClearRedirectTrack(r, "/callback") if vsErr, ok := err.(*verifyStateError); ok { if vsErr.IsCookieError() { throw.Unauthorized(s, w, r, "OAuth session not found or expired", err) diff --git a/internal/handlers/errorpage.go b/internal/handlers/errorpage.go index 917e930..082dfeb 100644 --- a/internal/handlers/errorpage.go +++ b/internal/handlers/errorpage.go @@ -5,7 +5,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/notify" - "git.haelnorr.com/h/oslstats/internal/view/page" + baseview "git.haelnorr.com/h/oslstats/internal/view/baseview" ) func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) { @@ -27,7 +27,7 @@ func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) { // Render appropriate template if details != "" { - return page.ErrorWithDetails( + return baseview.ErrorPageWithDetails( hwsError.StatusCode, http.StatusText(hwsError.StatusCode), message, @@ -35,7 +35,7 @@ func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) { ), nil } - return page.Error( + return baseview.ErrorPage( hwsError.StatusCode, http.StatusText(hwsError.StatusCode), message, diff --git a/internal/handlers/errors.go b/internal/handlers/errors.go index 7c602a2..de4a577 100644 --- a/internal/handlers/errors.go +++ b/internal/handlers/errors.go @@ -34,3 +34,12 @@ func renderSafely(page templ.Component, s *hws.Server, r *http.Request, w http.R throw.InternalServiceError(s, w, r, "Failed to render page", errors.Wrap(err, "page.")) } } + +func logError(s *hws.Server, msg string, err error) { + s.LogError(hws.HWSError{ + Message: msg, + Error: err, + Level: hws.ErrorERROR, + StatusCode: http.StatusInternalServerError, + }) +} diff --git a/internal/handlers/index.go b/internal/handlers/index.go index 19e668d..6eaa23a 100644 --- a/internal/handlers/index.go +++ b/internal/handlers/index.go @@ -3,10 +3,9 @@ package handlers import ( "net/http" - "git.haelnorr.com/h/oslstats/internal/throw" - "git.haelnorr.com/h/oslstats/internal/view/page" - "git.haelnorr.com/h/golib/hws" + "git.haelnorr.com/h/oslstats/internal/throw" + homeview "git.haelnorr.com/h/oslstats/internal/view/homeview" ) // Index handles responses to the / path. Also serves a 404 Page for paths that @@ -17,7 +16,7 @@ func Index(s *hws.Server) http.Handler { if r.URL.Path != "/" { throw.NotFound(s, w, r, r.URL.Path) } - renderSafely(page.Index(), s, r, w) + renderSafely(homeview.IndexPage(), s, r, w) }, ) } diff --git a/internal/handlers/newseason.go b/internal/handlers/newseason.go index 615564e..778b169 100644 --- a/internal/handlers/newseason.go +++ b/internal/handlers/newseason.go @@ -9,7 +9,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/notify" "git.haelnorr.com/h/oslstats/internal/validation" - "git.haelnorr.com/h/oslstats/internal/view/page" + seasonsview "git.haelnorr.com/h/oslstats/internal/view/seasonsview" "git.haelnorr.com/h/timefmt" "github.com/pkg/errors" "github.com/uptrace/bun" @@ -21,7 +21,7 @@ func NewSeason( ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { - renderSafely(page.NewSeason(), s, r, w) + renderSafely(seasonsview.NewPage(), s, r, w) return } }) diff --git a/internal/handlers/notifswebsocket.go b/internal/handlers/notifswebsocket.go index 072c901..0a174ee 100644 --- a/internal/handlers/notifswebsocket.go +++ b/internal/handlers/notifswebsocket.go @@ -11,7 +11,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/config" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/throw" - "git.haelnorr.com/h/oslstats/internal/view/component/popup" + "git.haelnorr.com/h/oslstats/internal/view/popup" "github.com/coder/websocket" "github.com/pkg/errors" @@ -28,36 +28,24 @@ func NotificationWS( } nc, err := setupClient(s, w, r) if err != nil { - s.LogError(hws.HWSError{ - Message: "Failed to get notification client", - Error: err, - Level: hws.ErrorERROR, - StatusCode: http.StatusInternalServerError, - }) + logError(s, "Failed to get notification client", errors.Wrap(err, "setupClient")) return } ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{ OriginPatterns: []string{cfg.HWSAuth.TrustedHost}, }) if err != nil { - s.LogError(hws.HWSError{ - Message: "Failed to open websocket", - Error: err, - Level: hws.ErrorERROR, - StatusCode: http.StatusInternalServerError, - }) + logError(s, "Failed to open websocket", errors.Wrap(err, "websocket.Accept")) return } - defer ws.CloseNow() ctx := ws.CloseRead(r.Context()) err = notifyLoop(ctx, nc, ws) if err != nil { - s.LogError(hws.HWSError{ - Message: "Notification error", - Error: err, - Level: hws.ErrorERROR, - StatusCode: http.StatusInternalServerError, - }) + logError(s, "Notification error", errors.Wrap(err, "notifyLoop")) + } + err = ws.CloseNow() + if err != nil { + logError(s, "Error closing websocket", errors.Wrap(err, "ws.CloseNow")) } }, ) diff --git a/internal/handlers/notifytest.go b/internal/handlers/notifytest.go index 0aefe8a..659a1d5 100644 --- a/internal/handlers/notifytest.go +++ b/internal/handlers/notifytest.go @@ -7,7 +7,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/notify" - "git.haelnorr.com/h/oslstats/internal/view/page" + testview "git.haelnorr.com/h/oslstats/internal/view/testview" ) // NotifyTester handles responses to the / path. Also serves a 404 Page for paths that @@ -17,7 +17,7 @@ func NotifyTester(s *hws.Server) http.Handler { func(w http.ResponseWriter, r *http.Request) { testErr := errors.New("This is a stack trace. No really i swear. Just pretend ok? Thanks") if r.Method == "GET" { - renderSafely(page.Test(), s, r, w) + renderSafely(testview.NotificationTestPage(), s, r, w) } else { _ = r.ParseForm() // target := r.Form.Get("target") diff --git a/internal/handlers/register.go b/internal/handlers/register.go index 80f973f..74e4030 100644 --- a/internal/handlers/register.go +++ b/internal/handlers/register.go @@ -14,7 +14,7 @@ import ( "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/store" "git.haelnorr.com/h/oslstats/internal/throw" - "git.haelnorr.com/h/oslstats/internal/view/page" + authview "git.haelnorr.com/h/oslstats/internal/view/authview" ) func Register( @@ -49,7 +49,7 @@ func Register( store.ClearRedirectTrack(r, "/register") if r.Method == "GET" { - renderSafely(page.Register(details.DiscordUser.Username), s, r, w) + renderSafely(authview.RegisterPage(details.DiscordUser.Username), s, r, w) return } username := r.FormValue("username") diff --git a/internal/handlers/season.go b/internal/handlers/season.go index 4a6b22b..52dfc26 100644 --- a/internal/handlers/season.go +++ b/internal/handlers/season.go @@ -7,7 +7,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" "git.haelnorr.com/h/oslstats/internal/throw" - "git.haelnorr.com/h/oslstats/internal/view/page" + seasonsview "git.haelnorr.com/h/oslstats/internal/view/seasonsview" "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -33,6 +33,6 @@ func SeasonPage( throw.NotFound(s, w, r, r.URL.Path) return } - renderSafely(page.SeasonPage(season), s, r, w) + renderSafely(seasonsview.DetailPage(season), s, r, w) }) } diff --git a/internal/handlers/seasons.go b/internal/handlers/seasons.go index 7402f1e..bedb0b4 100644 --- a/internal/handlers/seasons.go +++ b/internal/handlers/seasons.go @@ -6,7 +6,7 @@ import ( "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/oslstats/internal/db" - "git.haelnorr.com/h/oslstats/internal/view/page" + seasonsview "git.haelnorr.com/h/oslstats/internal/view/seasonsview" "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -31,7 +31,7 @@ func SeasonsPage( }); !ok { return } - renderSafely(page.SeasonsPage(seasons), s, r, w) + renderSafely(seasonsview.ListPage(seasons), s, r, w) }) } @@ -55,6 +55,6 @@ func SeasonsList( }); !ok { return } - renderSafely(page.SeasonsList(seasons), s, r, w) + renderSafely(seasonsview.SeasonsList(seasons), s, r, w) }) } diff --git a/internal/validation/validation.go b/internal/validation/validation.go index 3206ecc..a08f00d 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -88,6 +88,8 @@ func validateAndError( err_ := fmt.Errorf("%s: %s", check.Title, check.Message) err = errors.Join(err, err_) } - throw.BadRequest(s, w, r, "Invalid form data", err) + if len(failedChecks) > 0 { + throw.BadRequest(s, w, r, "Invalid form data", err) + } return len(failedChecks) == 0 } diff --git a/internal/view/adminview/dashboard_layout.templ b/internal/view/adminview/dashboard_layout.templ new file mode 100644 index 0000000..3ee2d00 --- /dev/null +++ b/internal/view/adminview/dashboard_layout.templ @@ -0,0 +1,11 @@ +package adminview + +import "git.haelnorr.com/h/oslstats/internal/view/baseview" + +templ DashboardLayout() { + @baseview.Layout("Admin") { +
+ { children... } +
+ } +} diff --git a/internal/view/adminview/dashboard_page.templ b/internal/view/adminview/dashboard_page.templ new file mode 100644 index 0000000..0c78287 --- /dev/null +++ b/internal/view/adminview/dashboard_page.templ @@ -0,0 +1,9 @@ +package adminview + +import "git.haelnorr.com/h/oslstats/internal/db" + +templ DashboardPage(users *db.List[db.User]) { + @DashboardLayout() { + @UserList(users) + } +} diff --git a/internal/view/component/admin/user_list.templ b/internal/view/adminview/user_list.templ similarity index 84% rename from internal/view/component/admin/user_list.templ rename to internal/view/adminview/user_list.templ index 55d914e..854f0a0 100644 --- a/internal/view/component/admin/user_list.templ +++ b/internal/view/adminview/user_list.templ @@ -1,4 +1,4 @@ -package admin +package adminview import "git.haelnorr.com/h/oslstats/internal/db" diff --git a/internal/view/component/form/register.templ b/internal/view/authview/register_form.templ similarity index 98% rename from internal/view/component/form/register.templ rename to internal/view/authview/register_form.templ index 00af173..b8aa722 100644 --- a/internal/view/component/form/register.templ +++ b/internal/view/authview/register_form.templ @@ -1,6 +1,6 @@ -package form +package authview -templ RegisterForm(username string) { +templ RegisterFormForm(username string) {
@@ -20,7 +19,7 @@ templ Register(username string) {

- @form.RegisterForm(username) + @RegisterFormForm(username)
diff --git a/internal/view/page/error.templ b/internal/view/baseview/error_page.templ similarity index 86% rename from internal/view/page/error.templ rename to internal/view/baseview/error_page.templ index 6dbad67..48adacc 100644 --- a/internal/view/page/error.templ +++ b/internal/view/baseview/error_page.templ @@ -1,16 +1,15 @@ -package page +package baseview -import "git.haelnorr.com/h/oslstats/internal/view/layout" import "strconv" // Original Error template (keep for backwards compatibility where needed) -templ Error(code int, err string, message string) { - @ErrorWithDetails(code, err, message, "") +templ ErrorPage(code int, err string, message string) { + @ErrorPageWithDetails(code, err, message, "") } // Enhanced Error template with optional details section -templ ErrorWithDetails(code int, err string, message string, details string) { - @layout.Global(err) { +templ ErrorPageWithDetails(code int, err string, message string, details string) { + @Layout(err) {

{ strconv.Itoa(code) }

diff --git a/internal/view/baseview/footer.templ b/internal/view/baseview/footer.templ new file mode 100644 index 0000000..29b2503 --- /dev/null +++ b/internal/view/baseview/footer.templ @@ -0,0 +1,128 @@ +package baseview + +type FooterItem struct { + Name string + Href string +} + +// Specify the links to show in the footer +func getFooterItems() []FooterItem { + return []FooterItem{ + {Name: "About", Href: "/about"}, + } +} + +// Returns the template fragment for the Footer +templ Footer() { +
+
+ @backToTopButton() +
+ @footerBranding() + @footerLinks(getFooterItems()) +
+
+ @footerCopyright() + @themeSelector() +
+
+
+} + +templ backToTopButton() { + +} + +templ footerBranding() { +
+
+ OSL Stats +
+

+ placeholder text +

+
+} + +templ footerLinks(items []FooterItem) { + +} + +templ footerCopyright() { +
+

+ by Haelnorr | placeholder text +

+
+} + +templ themeSelector() { +
+
+ + + +
+
+} diff --git a/internal/view/layout/global.templ b/internal/view/baseview/layout.templ similarity index 79% rename from internal/view/layout/global.templ rename to internal/view/baseview/layout.templ index 8e829be..40fdcf9 100644 --- a/internal/view/layout/global.templ +++ b/internal/view/baseview/layout.templ @@ -1,13 +1,10 @@ -package layout +package baseview -import "git.haelnorr.com/h/oslstats/internal/view/component/popup" -import "git.haelnorr.com/h/oslstats/internal/view/component/nav" -import "git.haelnorr.com/h/oslstats/internal/view/component/footer" +import "git.haelnorr.com/h/oslstats/internal/view/popup" import "git.haelnorr.com/h/oslstats/internal/contexts" -// Global page layout. Includes HTML document settings, header tags -// navbar and footer -templ Global(title string) { +// Global base layout for all pages +templ Layout(title string) { {{ devInfo := contexts.DevMode(ctx) }} - @nav.Navbar() + @Navbar()
{ children... }
- @footer.Footer() + @Footer()
diff --git a/internal/view/baseview/navbar.templ b/internal/view/baseview/navbar.templ new file mode 100644 index 0000000..69cf0a1 --- /dev/null +++ b/internal/view/baseview/navbar.templ @@ -0,0 +1,233 @@ +package baseview + +import ( + "context" + "git.haelnorr.com/h/oslstats/internal/contexts" + "git.haelnorr.com/h/oslstats/internal/db" +) + +type NavItem struct { + Name string + Href string +} + +type ProfileItem struct { + Name string + Href string +} + +// Main navigation items (centralized) +func getNavItems() []NavItem { + return []NavItem{ + {Name: "Seasons", Href: "/seasons"}, + } +} + +// Profile dropdown items (context-aware for admin) +func getProfileItems(ctx context.Context) []ProfileItem { + items := []ProfileItem{ + {Name: "Profile", Href: "/profile"}, + {Name: "Account", Href: "/account"}, + } + + cache := contexts.Permissions(ctx) + if cache != nil && cache.Roles["admin"] { + items = append(items, ProfileItem{ + Name: "Admin Panel", + Href: "/admin", + }) + } + + return items +} + +// Main navbar component +templ Navbar() { + {{ navItems := getNavItems() }} + {{ user := db.CurrentUser(ctx) }} + {{ profileItems := getProfileItems(ctx) }} +
+
+
+ + + + OSL Stats + + +
+ + @desktopNav(navItems) + + @userMenu(user, profileItems) +
+
+
+ + @mobileNav(navItems, user) +
+} + +// Desktop navigation (private helper) +templ desktopNav(navItems []NavItem) { + +} + +// User menu section (private helper) +templ userMenu(user *db.User, profileItems []ProfileItem) { +
+
+ if user != nil { + @profileDropdown(user, profileItems) + } else { + @loginButton() + } +
+ @mobileMenuButton() +
+} + +// Profile dropdown (private helper) +templ profileDropdown(user *db.User, items []ProfileItem) { +
+
+ +
+ +
+} + +// Login button (private helper) +templ loginButton() { + +} + +// Mobile menu toggle (private helper) +templ mobileMenuButton() { + +} + +// Mobile navigation drawer (private helper) +templ mobileNav(navItems []NavItem, user *db.User) { +
+
+ +
+ if user == nil { +
+ +
+ } +
+} diff --git a/internal/view/component/footer/footer.templ b/internal/view/component/footer/footer.templ deleted file mode 100644 index 59a19a1..0000000 --- a/internal/view/component/footer/footer.templ +++ /dev/null @@ -1,116 +0,0 @@ -package footer - -type FooterItem struct { - name string - href string -} - -// Specify the links to show in the footer -func getFooterItems() []FooterItem { - return []FooterItem{ - { - name: "About", - href: "/about", - }, - } -} - -// Returns the template fragment for the Footer -templ Footer() { -
-
- -
-
-
- // TODO: logo/branding here - OSL Stats -
-

placeholder text

-
-
    - for _, item := range getFooterItems() { -
  • - { item.name } -
  • - } -
-
-
-
-

- by Haelnorr | placeholder text -

-
-
-
- - - -
-
-
-
-
-} diff --git a/internal/view/component/nav/navbar.templ b/internal/view/component/nav/navbar.templ deleted file mode 100644 index 09fb94d..0000000 --- a/internal/view/component/nav/navbar.templ +++ /dev/null @@ -1,41 +0,0 @@ -package nav - -type NavItem struct { - name string // Label to display - href string // Link reference -} - -// Return the list of navbar links -func getNavItems() []NavItem { - return []NavItem{ - { - name: "Seasons", - href: "/seasons", - }, - } -} - -// Returns the navbar template fragment -templ Navbar() { - {{ navItems := getNavItems() }} -
-
-
- - - - OSL Stats - - -
- @navLeft(navItems) - @navRight() -
-
-
- @sideNav(navItems) -
-} diff --git a/internal/view/component/nav/navbarleft.templ b/internal/view/component/nav/navbarleft.templ deleted file mode 100644 index fec9443..0000000 --- a/internal/view/component/nav/navbarleft.templ +++ /dev/null @@ -1,19 +0,0 @@ -package nav - -// Returns the left portion of the navbar -templ navLeft(navItems []NavItem) { - -} diff --git a/internal/view/component/nav/navbarright.templ b/internal/view/component/nav/navbarright.templ deleted file mode 100644 index 6a3d53e..0000000 --- a/internal/view/component/nav/navbarright.templ +++ /dev/null @@ -1,131 +0,0 @@ -package nav - -import ( - "context" - "git.haelnorr.com/h/oslstats/internal/contexts" - "git.haelnorr.com/h/oslstats/internal/db" -) - -type ProfileItem struct { - name string // Label to display - href string // Link reference -} - -// Return the list of profile links -func getProfileItems(ctx context.Context) []ProfileItem { - items := []ProfileItem{ - { - name: "Profile", - href: "/profile", - }, - { - name: "Account", - href: "/account", - }, - } - - // Add admin link if user has admin role - cache := contexts.Permissions(ctx) - if cache != nil && cache.Roles["admin"] { - items = append(items, ProfileItem{ - name: "Admin Panel", - href: "/admin", - }) - } - - return items -} - -// Returns the right portion of the navbar -templ navRight() { - {{ user := db.CurrentUser(ctx) }} - {{ items := getProfileItems(ctx) }} -
-
- if user != nil { -
-
- -
- -
- } else { - - } -
- -
-} diff --git a/internal/view/component/nav/sidenav.templ b/internal/view/component/nav/sidenav.templ deleted file mode 100644 index 5b50e70..0000000 --- a/internal/view/component/nav/sidenav.templ +++ /dev/null @@ -1,45 +0,0 @@ -package nav - -import "git.haelnorr.com/h/oslstats/internal/db" - -// Returns the mobile version of the navbar thats only visible when activated -templ sideNav(navItems []NavItem) { - {{ user := db.CurrentUser(ctx) }} -
-
- -
- if user == nil { -
- -
- } -
-} diff --git a/internal/view/component/datepicker/README.md b/internal/view/datepicker/README.md similarity index 100% rename from internal/view/component/datepicker/README.md rename to internal/view/datepicker/README.md diff --git a/internal/view/component/datepicker/datepicker.templ b/internal/view/datepicker/datepicker.templ similarity index 100% rename from internal/view/component/datepicker/datepicker.templ rename to internal/view/datepicker/datepicker.templ diff --git a/internal/view/page/index.templ b/internal/view/homeview/index_page.templ similarity index 56% rename from internal/view/page/index.templ rename to internal/view/homeview/index_page.templ index 8882c04..452aa5b 100644 --- a/internal/view/page/index.templ +++ b/internal/view/homeview/index_page.templ @@ -1,10 +1,10 @@ -package page +package homeview -import "git.haelnorr.com/h/oslstats/internal/view/layout" +import "git.haelnorr.com/h/oslstats/internal/view/baseview" // Page content for the index page -templ Index() { - @layout.Global("OSL Stats") { +templ IndexPage() { + @baseview.Layout("OSL Stats") {
OSL Stats
Placeholder text
diff --git a/internal/view/layout/admin_dashboard.templ b/internal/view/layout/admin_dashboard.templ deleted file mode 100644 index f0e54a8..0000000 --- a/internal/view/layout/admin_dashboard.templ +++ /dev/null @@ -1,8 +0,0 @@ -package layout - -templ AdminDashboard() { - @Global("Admin") -
- { children... } -
-} diff --git a/internal/view/page/admin_dashboard.templ b/internal/view/page/admin_dashboard.templ deleted file mode 100644 index d72899a..0000000 --- a/internal/view/page/admin_dashboard.templ +++ /dev/null @@ -1,10 +0,0 @@ -package page - -import "git.haelnorr.com/h/oslstats/internal/view/layout" -import "git.haelnorr.com/h/oslstats/internal/view/component/admin" -import "git.haelnorr.com/h/oslstats/internal/db" - -templ AdminDashboard(users *db.List[db.User]) { - @layout.AdminDashboard() - @admin.UserList(users) -} diff --git a/internal/view/component/pagination/pagination.templ b/internal/view/pagination/pagination.templ similarity index 100% rename from internal/view/component/pagination/pagination.templ rename to internal/view/pagination/pagination.templ diff --git a/internal/view/component/popup/errorModalContainer.templ b/internal/view/popup/errorModalContainer.templ similarity index 100% rename from internal/view/component/popup/errorModalContainer.templ rename to internal/view/popup/errorModalContainer.templ diff --git a/internal/view/component/popup/errorModalWS.templ b/internal/view/popup/errorModalWS.templ similarity index 100% rename from internal/view/component/popup/errorModalWS.templ rename to internal/view/popup/errorModalWS.templ diff --git a/internal/view/component/popup/toast.templ b/internal/view/popup/toast.templ similarity index 100% rename from internal/view/component/popup/toast.templ rename to internal/view/popup/toast.templ diff --git a/internal/view/component/popup/toastContainer.templ b/internal/view/popup/toastContainer.templ similarity index 100% rename from internal/view/component/popup/toastContainer.templ rename to internal/view/popup/toastContainer.templ diff --git a/internal/view/page/season.templ b/internal/view/seasonsview/detail_page.templ similarity index 94% rename from internal/view/page/season.templ rename to internal/view/seasonsview/detail_page.templ index 384cbdf..93c0da3 100644 --- a/internal/view/page/season.templ +++ b/internal/view/seasonsview/detail_page.templ @@ -1,13 +1,12 @@ -package page +package seasonsview import "git.haelnorr.com/h/oslstats/internal/db" -import "git.haelnorr.com/h/oslstats/internal/view/layout" -import seasoncomp "git.haelnorr.com/h/oslstats/internal/view/component/season" +import "git.haelnorr.com/h/oslstats/internal/view/baseview" import "time" import "strconv" -templ SeasonPage(season *db.Season) { - @layout.Global(season.Name) { +templ DetailPage(season *db.Season) { + @baseview.Layout(season.Name) {
@SeasonDetails(season)
@@ -125,7 +124,7 @@ templ SeasonDetails(season *db.Season) {

Status

- @seasoncomp.StatusBadge(season, false, false) + @StatusBadge(season, false, false)
diff --git a/internal/view/page/seasons_list.templ b/internal/view/seasonsview/list_page.templ similarity index 87% rename from internal/view/page/seasons_list.templ rename to internal/view/seasonsview/list_page.templ index a859dc7..b499f49 100644 --- a/internal/view/page/seasons_list.templ +++ b/internal/view/seasonsview/list_page.templ @@ -1,16 +1,15 @@ -package page +package seasonsview import "git.haelnorr.com/h/oslstats/internal/db" -import "git.haelnorr.com/h/oslstats/internal/view/layout" -import "git.haelnorr.com/h/oslstats/internal/view/component/pagination" -import "git.haelnorr.com/h/oslstats/internal/view/component/sort" -import "git.haelnorr.com/h/oslstats/internal/view/component/season" +import "git.haelnorr.com/h/oslstats/internal/view/baseview" +import "git.haelnorr.com/h/oslstats/internal/view/pagination" +import "git.haelnorr.com/h/oslstats/internal/view/sort" import "fmt" import "time" import "github.com/uptrace/bun" -templ SeasonsPage(seasons *db.List[db.Season]) { - @layout.Global("Seasons") { +templ ListPage(seasons *db.List[db.Season]) { + @baseview.Layout("Seasons") {
@SeasonsList(seasons)
@@ -84,7 +83,7 @@ templ SeasonsList(seasons *db.List[db.Season]) {

{ s.Name }

- @season.StatusBadge(s, true, true) + @StatusBadge(s, true, true)
diff --git a/internal/view/component/form/new_season.templ b/internal/view/seasonsview/new_form.templ similarity index 98% rename from internal/view/component/form/new_season.templ rename to internal/view/seasonsview/new_form.templ index 6ec0730..cd86807 100644 --- a/internal/view/component/form/new_season.templ +++ b/internal/view/seasonsview/new_form.templ @@ -1,8 +1,8 @@ -package form +package seasonsview -import "git.haelnorr.com/h/oslstats/internal/view/component/datepicker" +import "git.haelnorr.com/h/oslstats/internal/view/datepicker" -templ NewSeason() { +templ NewForm() {
@@ -15,7 +14,7 @@ templ NewSeason() {

- @form.NewSeason() + @NewForm()
diff --git a/internal/view/component/season/status.templ b/internal/view/seasonsview/status_badge.templ similarity index 99% rename from internal/view/component/season/status.templ rename to internal/view/seasonsview/status_badge.templ index c317cbb..7878dc8 100644 --- a/internal/view/component/season/status.templ +++ b/internal/view/seasonsview/status_badge.templ @@ -1,4 +1,4 @@ -package season +package seasonsview import "git.haelnorr.com/h/oslstats/internal/db" import "time" diff --git a/internal/view/component/sort/dropdown.templ b/internal/view/sort/dropdown.templ similarity index 100% rename from internal/view/component/sort/dropdown.templ rename to internal/view/sort/dropdown.templ diff --git a/internal/view/page/test.templ b/internal/view/testview/notification_test_page.templ similarity index 94% rename from internal/view/page/test.templ rename to internal/view/testview/notification_test_page.templ index 972e671..e3bb97e 100644 --- a/internal/view/page/test.templ +++ b/internal/view/testview/notification_test_page.templ @@ -1,10 +1,10 @@ -package page +package testview -import "git.haelnorr.com/h/oslstats/internal/view/layout" +import "git.haelnorr.com/h/oslstats/internal/view/baseview" // Page content for the test notification page -templ Test() { - @layout.Global("Notification Test") { +templ NotificationTestPage() { + @baseview.Layout("Notification Test") {