refactored view package
This commit is contained in:
@@ -8,39 +8,27 @@ import (
|
|||||||
"github.com/uptrace/bun"
|
"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 {
|
type fieldgetter[T any] struct {
|
||||||
q *bun.SelectQuery
|
q *bun.SelectQuery
|
||||||
field string
|
field string
|
||||||
value any
|
value any
|
||||||
|
model *T
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *fieldgetter[T]) get(ctx context.Context) (*T, error) {
|
func (g *fieldgetter[T]) get(ctx context.Context) (*T, error) {
|
||||||
if g.field == "id" && (g.value).(int) < 1 {
|
if g.field == "id" && (g.value).(int) < 1 {
|
||||||
return nil, errors.New("invalid id")
|
return nil, errors.New("invalid id")
|
||||||
}
|
}
|
||||||
model := new(T)
|
|
||||||
err := g.q.
|
err := g.q.
|
||||||
Where("? = ?", bun.Ident(g.field), g.value).
|
Where("? = ?", bun.Ident(g.field), g.value).
|
||||||
Scan(ctx, model)
|
Scan(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, errors.Wrap(err, "bun.SelectQuery.Scan")
|
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) {
|
func (g *fieldgetter[T]) GetFirst(ctx context.Context) (*T, error) {
|
||||||
@@ -68,10 +56,12 @@ func GetByField[T any](
|
|||||||
field string,
|
field string,
|
||||||
value any,
|
value any,
|
||||||
) *fieldgetter[T] {
|
) *fieldgetter[T] {
|
||||||
|
model := new(T)
|
||||||
return &fieldgetter[T]{
|
return &fieldgetter[T]{
|
||||||
tx.NewSelect().Model((*T)(nil)),
|
tx.NewSelect().Model(model),
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
|
model,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ type Filter struct {
|
|||||||
Value any
|
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] {
|
func GetList[T any](tx bun.Tx, pageOpts, defaults *PageOpts) *listgetter[T] {
|
||||||
l := &listgetter[T]{
|
l := &listgetter[T]{
|
||||||
items: new([]*T),
|
items: new([]*T),
|
||||||
|
|||||||
@@ -45,6 +45,36 @@
|
|||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
--default-mono-font-family: var(--font-mono);
|
--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 {
|
@layer base {
|
||||||
@@ -243,6 +273,9 @@
|
|||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.top-1 {
|
||||||
|
top: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1/2 * 100%);
|
top: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
@@ -276,6 +309,24 @@
|
|||||||
.z-50 {
|
.z-50 {
|
||||||
z-index: 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 {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
@@ -442,9 +493,22 @@
|
|||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.flex-shrink {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
.shrink-0 {
|
.shrink-0 {
|
||||||
flex-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 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -546,6 +610,11 @@
|
|||||||
border-color: var(--surface2);
|
border-color: var(--surface2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -838,6 +907,9 @@
|
|||||||
.italic {
|
.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
.opacity-50 {
|
.opacity-50 {
|
||||||
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));
|
--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);
|
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 {
|
||||||
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-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));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
@@ -1434,6 +1513,64 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0 0 #0000;
|
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: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-size {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
@layer properties {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@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 {
|
*, ::before, ::after, ::backdrop {
|
||||||
@@ -1465,6 +1602,20 @@
|
|||||||
--tw-ring-offset-width: 0px;
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
--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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"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/pkg/errors"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
@@ -24,6 +24,6 @@ func AdminDashboard(s *hws.Server, conn *bun.DB) http.Handler {
|
|||||||
}); !ok {
|
}); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderSafely(page.AdminDashboard(users), s, r, w)
|
renderSafely(adminview.DashboardPage(users), s, r, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"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/pkg/errors"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
@@ -29,6 +29,6 @@ func AdminUsersList(s *hws.Server, conn *bun.DB) http.Handler {
|
|||||||
}); !ok {
|
}); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderSafely(admin.UserList(users), s, r, w)
|
renderSafely(adminview.UserList(users), s, r, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"git.haelnorr.com/h/oslstats/internal/discord"
|
"git.haelnorr.com/h/oslstats/internal/discord"
|
||||||
"git.haelnorr.com/h/oslstats/internal/store"
|
"git.haelnorr.com/h/oslstats/internal/store"
|
||||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
"git.haelnorr.com/h/oslstats/internal/throw"
|
||||||
|
"git.haelnorr.com/h/oslstats/internal/validation"
|
||||||
"git.haelnorr.com/h/oslstats/pkg/oauth"
|
"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)
|
throw.BadRequest(s, w, r, "Too many redirects. Please try logging in again.", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
getter := validation.NewQueryGetter(r)
|
||||||
state := r.URL.Query().Get("state")
|
state := getter.String("state").Required().Value
|
||||||
code := r.URL.Query().Get("code")
|
code := getter.String("code").Required().Value
|
||||||
if state == "" && code == "" {
|
if !getter.Validate() {
|
||||||
http.Redirect(w, r, "/", http.StatusBadRequest)
|
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
|
return
|
||||||
}
|
}
|
||||||
data, err := verifyState(cfg.OAuth, w, r, state)
|
data, err := verifyState(cfg.OAuth, w, r, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
store.ClearRedirectTrack(r, "/callback")
|
||||||
if vsErr, ok := err.(*verifyStateError); ok {
|
if vsErr, ok := err.(*verifyStateError); ok {
|
||||||
if vsErr.IsCookieError() {
|
if vsErr.IsCookieError() {
|
||||||
throw.Unauthorized(s, w, r, "OAuth session not found or expired", err)
|
throw.Unauthorized(s, w, r, "OAuth session not found or expired", err)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/oslstats/internal/notify"
|
"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) {
|
func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) {
|
||||||
@@ -27,7 +27,7 @@ func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) {
|
|||||||
|
|
||||||
// Render appropriate template
|
// Render appropriate template
|
||||||
if details != "" {
|
if details != "" {
|
||||||
return page.ErrorWithDetails(
|
return baseview.ErrorPageWithDetails(
|
||||||
hwsError.StatusCode,
|
hwsError.StatusCode,
|
||||||
http.StatusText(hwsError.StatusCode),
|
http.StatusText(hwsError.StatusCode),
|
||||||
message,
|
message,
|
||||||
@@ -35,7 +35,7 @@ func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) {
|
|||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.Error(
|
return baseview.ErrorPage(
|
||||||
hwsError.StatusCode,
|
hwsError.StatusCode,
|
||||||
http.StatusText(hwsError.StatusCode),
|
http.StatusText(hwsError.StatusCode),
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -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."))
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"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/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
|
// 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 != "/" {
|
if r.URL.Path != "/" {
|
||||||
throw.NotFound(s, w, r, r.URL.Path)
|
throw.NotFound(s, w, r, r.URL.Path)
|
||||||
}
|
}
|
||||||
renderSafely(page.Index(), s, r, w)
|
renderSafely(homeview.IndexPage(), s, r, w)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"git.haelnorr.com/h/oslstats/internal/db"
|
||||||
"git.haelnorr.com/h/oslstats/internal/notify"
|
"git.haelnorr.com/h/oslstats/internal/notify"
|
||||||
"git.haelnorr.com/h/oslstats/internal/validation"
|
"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"
|
"git.haelnorr.com/h/timefmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
@@ -21,7 +21,7 @@ func NewSeason(
|
|||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
renderSafely(page.NewSeason(), s, r, w)
|
renderSafely(seasonsview.NewPage(), s, r, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"git.haelnorr.com/h/oslstats/internal/config"
|
"git.haelnorr.com/h/oslstats/internal/config"
|
||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"git.haelnorr.com/h/oslstats/internal/db"
|
||||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
"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/coder/websocket"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -28,36 +28,24 @@ func NotificationWS(
|
|||||||
}
|
}
|
||||||
nc, err := setupClient(s, w, r)
|
nc, err := setupClient(s, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.LogError(hws.HWSError{
|
logError(s, "Failed to get notification client", errors.Wrap(err, "setupClient"))
|
||||||
Message: "Failed to get notification client",
|
|
||||||
Error: err,
|
|
||||||
Level: hws.ErrorERROR,
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||||
OriginPatterns: []string{cfg.HWSAuth.TrustedHost},
|
OriginPatterns: []string{cfg.HWSAuth.TrustedHost},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.LogError(hws.HWSError{
|
logError(s, "Failed to open websocket", errors.Wrap(err, "websocket.Accept"))
|
||||||
Message: "Failed to open websocket",
|
|
||||||
Error: err,
|
|
||||||
Level: hws.ErrorERROR,
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer ws.CloseNow()
|
|
||||||
ctx := ws.CloseRead(r.Context())
|
ctx := ws.CloseRead(r.Context())
|
||||||
err = notifyLoop(ctx, nc, ws)
|
err = notifyLoop(ctx, nc, ws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.LogError(hws.HWSError{
|
logError(s, "Notification error", errors.Wrap(err, "notifyLoop"))
|
||||||
Message: "Notification error",
|
}
|
||||||
Error: err,
|
err = ws.CloseNow()
|
||||||
Level: hws.ErrorERROR,
|
if err != nil {
|
||||||
StatusCode: http.StatusInternalServerError,
|
logError(s, "Error closing websocket", errors.Wrap(err, "ws.CloseNow"))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/oslstats/internal/notify"
|
"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
|
// 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) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
testErr := errors.New("This is a stack trace. No really i swear. Just pretend ok? Thanks")
|
testErr := errors.New("This is a stack trace. No really i swear. Just pretend ok? Thanks")
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
renderSafely(page.Test(), s, r, w)
|
renderSafely(testview.NotificationTestPage(), s, r, w)
|
||||||
} else {
|
} else {
|
||||||
_ = r.ParseForm()
|
_ = r.ParseForm()
|
||||||
// target := r.Form.Get("target")
|
// target := r.Form.Get("target")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"git.haelnorr.com/h/oslstats/internal/db"
|
||||||
"git.haelnorr.com/h/oslstats/internal/store"
|
"git.haelnorr.com/h/oslstats/internal/store"
|
||||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
"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(
|
func Register(
|
||||||
@@ -49,7 +49,7 @@ func Register(
|
|||||||
store.ClearRedirectTrack(r, "/register")
|
store.ClearRedirectTrack(r, "/register")
|
||||||
|
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
renderSafely(page.Register(details.DiscordUser.Username), s, r, w)
|
renderSafely(authview.RegisterPage(details.DiscordUser.Username), s, r, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"git.haelnorr.com/h/oslstats/internal/db"
|
||||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
"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/pkg/errors"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
@@ -33,6 +33,6 @@ func SeasonPage(
|
|||||||
throw.NotFound(s, w, r, r.URL.Path)
|
throw.NotFound(s, w, r, r.URL.Path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderSafely(page.SeasonPage(season), s, r, w)
|
renderSafely(seasonsview.DetailPage(season), s, r, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
"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/pkg/errors"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,7 @@ func SeasonsPage(
|
|||||||
}); !ok {
|
}); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderSafely(page.SeasonsPage(seasons), s, r, w)
|
renderSafely(seasonsview.ListPage(seasons), s, r, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,6 @@ func SeasonsList(
|
|||||||
}); !ok {
|
}); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderSafely(page.SeasonsList(seasons), s, r, w)
|
renderSafely(seasonsview.SeasonsList(seasons), s, r, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ func validateAndError(
|
|||||||
err_ := fmt.Errorf("%s: %s", check.Title, check.Message)
|
err_ := fmt.Errorf("%s: %s", check.Title, check.Message)
|
||||||
err = errors.Join(err, err_)
|
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
|
return len(failedChecks) == 0
|
||||||
}
|
}
|
||||||
|
|||||||
11
internal/view/adminview/dashboard_layout.templ
Normal file
11
internal/view/adminview/dashboard_layout.templ
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package adminview
|
||||||
|
|
||||||
|
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||||
|
|
||||||
|
templ DashboardLayout() {
|
||||||
|
@baseview.Layout("Admin") {
|
||||||
|
<div>
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
9
internal/view/adminview/dashboard_page.templ
Normal file
9
internal/view/adminview/dashboard_page.templ
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package adminview
|
||||||
|
|
||||||
|
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||||
|
|
||||||
|
templ DashboardPage(users *db.List[db.User]) {
|
||||||
|
@DashboardLayout() {
|
||||||
|
@UserList(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package admin
|
package adminview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package form
|
package authview
|
||||||
|
|
||||||
templ RegisterForm(username string) {
|
templ RegisterFormForm(username string) {
|
||||||
<form
|
<form
|
||||||
hx-post="/register"
|
hx-post="/register"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package page
|
package authview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/layout"
|
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/component/form"
|
|
||||||
|
|
||||||
// Returns the login page
|
// Returns the login page
|
||||||
templ Register(username string) {
|
templ RegisterPage(username string) {
|
||||||
@layout.Global("Register") {
|
@baseview.Layout("Register") {
|
||||||
<div class="max-w-100 mx-auto px-2">
|
<div class="max-w-100 mx-auto px-2">
|
||||||
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
|
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
|
||||||
<div class="p-4 sm:p-7">
|
<div class="p-4 sm:p-7">
|
||||||
@@ -20,7 +19,7 @@ templ Register(username string) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
@form.RegisterForm(username)
|
@RegisterFormForm(username)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
package page
|
package baseview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/layout"
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
// Original Error template (keep for backwards compatibility where needed)
|
// Original Error template (keep for backwards compatibility where needed)
|
||||||
templ Error(code int, err string, message string) {
|
templ ErrorPage(code int, err string, message string) {
|
||||||
@ErrorWithDetails(code, err, message, "")
|
@ErrorPageWithDetails(code, err, message, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced Error template with optional details section
|
// Enhanced Error template with optional details section
|
||||||
templ ErrorWithDetails(code int, err string, message string, details string) {
|
templ ErrorPageWithDetails(code int, err string, message string, details string) {
|
||||||
@layout.Global(err) {
|
@Layout(err) {
|
||||||
<div class="grid mt-24 left-0 right-0 top-0 bottom-0 place-content-center bg-base px-4">
|
<div class="grid mt-24 left-0 right-0 top-0 bottom-0 place-content-center bg-base px-4">
|
||||||
<div class="text-center max-w-2xl mx-auto">
|
<div class="text-center max-w-2xl mx-auto">
|
||||||
<h1 class="text-9xl text-text">{ strconv.Itoa(code) }</h1>
|
<h1 class="text-9xl text-text">{ strconv.Itoa(code) }</h1>
|
||||||
128
internal/view/baseview/footer.templ
Normal file
128
internal/view/baseview/footer.templ
Normal file
@@ -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() {
|
||||||
|
<footer class="bg-mantle mt-10">
|
||||||
|
<div class="relative mx-auto max-w-screen-xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
@backToTopButton()
|
||||||
|
<div class="lg:flex lg:items-end lg:justify-between">
|
||||||
|
@footerBranding()
|
||||||
|
@footerLinks(getFooterItems())
|
||||||
|
</div>
|
||||||
|
<div class="lg:flex lg:items-end lg:justify-between">
|
||||||
|
@footerCopyright()
|
||||||
|
@themeSelector()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ backToTopButton() {
|
||||||
|
<div class="absolute end-4 top-4 sm:end-6 lg:end-8">
|
||||||
|
<a
|
||||||
|
class="inline-block rounded-full bg-teal p-2 text-crust
|
||||||
|
shadow-sm transition hover:bg-teal/75"
|
||||||
|
href="#main-content"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Back to top</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="size-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293
|
||||||
|
3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4
|
||||||
|
4a1 1 0 010 1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ footerBranding() {
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-center text-text lg:justify-start">
|
||||||
|
<span class="text-2xl">OSL Stats</span>
|
||||||
|
</div>
|
||||||
|
<p class="mx-auto max-w-md text-center leading-relaxed text-subtext0">
|
||||||
|
placeholder text
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ footerLinks(items []FooterItem) {
|
||||||
|
<ul
|
||||||
|
class="mt-12 flex flex-wrap justify-center gap-6 md:gap-8
|
||||||
|
lg:mt-0 lg:justify-end lg:gap-12"
|
||||||
|
>
|
||||||
|
for _, item := range items {
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="transition hover:text-subtext1"
|
||||||
|
href={ templ.SafeURL(item.Href) }
|
||||||
|
>
|
||||||
|
{ item.Name }
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ footerCopyright() {
|
||||||
|
<div>
|
||||||
|
<p class="mt-4 text-center text-sm text-overlay0">
|
||||||
|
by Haelnorr | placeholder text
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ themeSelector() {
|
||||||
|
<div>
|
||||||
|
<div class="mt-2 text-center">
|
||||||
|
<label for="theme-select" class="hidden lg:inline">Theme</label>
|
||||||
|
<select
|
||||||
|
name="ThemeSelect"
|
||||||
|
id="theme-select"
|
||||||
|
class="mt-1.5 inline rounded-lg bg-surface0 p-2 w-fit"
|
||||||
|
x-model="theme"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
x-for="themeopt in [
|
||||||
|
'dark',
|
||||||
|
'light',
|
||||||
|
'system',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
x-text="displayThemeName(themeopt)"
|
||||||
|
:value="themeopt"
|
||||||
|
:selected="theme === themeopt"
|
||||||
|
></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<script>
|
||||||
|
const displayThemeName = (value) => {
|
||||||
|
if (value === "dark") return "Dark (Mocha)";
|
||||||
|
if (value === "light") return "Light (Latte)";
|
||||||
|
if (value === "system") return "System";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -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/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/contexts"
|
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
||||||
|
|
||||||
// Global page layout. Includes HTML document settings, header tags
|
// Global base layout for all pages
|
||||||
// navbar and footer
|
templ Layout(title string) {
|
||||||
templ Global(title string) {
|
|
||||||
{{ devInfo := contexts.DevMode(ctx) }}
|
{{ devInfo := contexts.DevMode(ctx) }}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html
|
||||||
@@ -44,11 +41,11 @@ templ Global(title string) {
|
|||||||
id="main-content"
|
id="main-content"
|
||||||
class="flex flex-col h-screen justify-between"
|
class="flex flex-col h-screen justify-between"
|
||||||
>
|
>
|
||||||
@nav.Navbar()
|
@Navbar()
|
||||||
<div id="page-content" class="mb-auto md:px-5 md:pt-5">
|
<div id="page-content" class="mb-auto md:px-5 md:pt-5">
|
||||||
{ children... }
|
{ children... }
|
||||||
</div>
|
</div>
|
||||||
@footer.Footer()
|
@Footer()
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
233
internal/view/baseview/navbar.templ
Normal file
233
internal/view/baseview/navbar.templ
Normal file
@@ -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) }}
|
||||||
|
<div x-data="{ open: false }">
|
||||||
|
<header class="bg-crust">
|
||||||
|
<div class="mx-auto flex h-16 max-w-7xl items-center gap-8 px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- Logo -->
|
||||||
|
<a class="block" href="/">
|
||||||
|
<span class="text-3xl font-bold transition hover:text-green">
|
||||||
|
OSL Stats
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="flex flex-1 items-center justify-end sm:justify-between">
|
||||||
|
<!-- Desktop nav links -->
|
||||||
|
@desktopNav(navItems)
|
||||||
|
<!-- User menu / Login button -->
|
||||||
|
@userMenu(user, profileItems)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Mobile side nav -->
|
||||||
|
@mobileNav(navItems, user)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop navigation (private helper)
|
||||||
|
templ desktopNav(navItems []NavItem) {
|
||||||
|
<nav aria-label="Global" class="hidden sm:block">
|
||||||
|
<ul class="flex items-center gap-6 text-xl">
|
||||||
|
for _, item := range navItems {
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-subtext1 hover:text-green transition"
|
||||||
|
href={ templ.SafeURL(item.Href) }
|
||||||
|
>
|
||||||
|
{ item.Name }
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
|
||||||
|
// User menu section (private helper)
|
||||||
|
templ userMenu(user *db.User, profileItems []ProfileItem) {
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="sm:flex sm:gap-2">
|
||||||
|
if user != nil {
|
||||||
|
@profileDropdown(user, profileItems)
|
||||||
|
} else {
|
||||||
|
@loginButton()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@mobileMenuButton()
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile dropdown (private helper)
|
||||||
|
templ profileDropdown(user *db.User, items []ProfileItem) {
|
||||||
|
<div x-data="{ isActive: false }" class="relative">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center overflow-hidden rounded-lg
|
||||||
|
bg-sapphire hover:bg-sapphire/75 transition"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
x-on:click="isActive = !isActive"
|
||||||
|
class="h-full py-2 px-4 text-mantle hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Profile</span>
|
||||||
|
{ user.Username }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute end-0 z-10 mt-2 w-36 divide-y divide-surface2
|
||||||
|
rounded-lg border border-surface1 bg-surface0 shadow-lg"
|
||||||
|
role="menu"
|
||||||
|
x-cloak
|
||||||
|
x-transition
|
||||||
|
x-show="isActive"
|
||||||
|
x-on:click.away="isActive = false"
|
||||||
|
x-on:keydown.escape.window="isActive = false"
|
||||||
|
>
|
||||||
|
<!-- Profile links -->
|
||||||
|
<div class="p-2">
|
||||||
|
for _, item := range items {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(item.Href) }
|
||||||
|
class="block rounded-lg px-4 py-2 text-md hover:bg-crust"
|
||||||
|
role="menuitem"
|
||||||
|
>
|
||||||
|
{ item.Name }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<!-- Logout -->
|
||||||
|
<div class="p-2">
|
||||||
|
<form hx-post="/logout">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="flex w-full items-center gap-2 rounded-lg px-4 py-2
|
||||||
|
text-md text-red hover:bg-red/25 hover:cursor-pointer"
|
||||||
|
role="menuitem"
|
||||||
|
@click="isActive=false"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login button (private helper)
|
||||||
|
templ loginButton() {
|
||||||
|
<button
|
||||||
|
class="hidden rounded-lg px-4 py-2 sm:block hover:cursor-pointer
|
||||||
|
bg-green hover:bg-green/75 text-mantle transition"
|
||||||
|
hx-post="/login"
|
||||||
|
hx-swap="none"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile menu toggle (private helper)
|
||||||
|
templ mobileMenuButton() {
|
||||||
|
<button
|
||||||
|
@click="open = !open"
|
||||||
|
class="block rounded-lg p-2.5 sm:hidden transition
|
||||||
|
bg-surface0 text-subtext0 hover:text-overlay2/75"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Toggle menu</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="size-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile navigation drawer (private helper)
|
||||||
|
templ mobileNav(navItems []NavItem, user *db.User) {
|
||||||
|
<div
|
||||||
|
x-show="open"
|
||||||
|
x-transition
|
||||||
|
class="absolute w-full bg-mantle sm:hidden z-10"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-6">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
for _, item := range navItems {
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(item.Href) }
|
||||||
|
class="block rounded-lg px-4 py-2 text-lg
|
||||||
|
bg-surface0 text-text transition hover:bg-surface2"
|
||||||
|
>
|
||||||
|
{ item.Name }
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
if user == nil {
|
||||||
|
<div class="px-4 pb-6">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li class="flex justify-center items-center gap-2">
|
||||||
|
<a
|
||||||
|
class="w-26 px-4 py-2 rounded-lg bg-green text-mantle
|
||||||
|
transition hover:bg-green/75 text-center"
|
||||||
|
href="/login"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -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() {
|
|
||||||
<footer class="bg-mantle mt-10">
|
|
||||||
<div
|
|
||||||
class="relative mx-auto max-w-screen-xl px-4 py-8 sm:px-6 lg:px-8"
|
|
||||||
>
|
|
||||||
<div class="absolute end-4 top-4 sm:end-6 lg:end-8">
|
|
||||||
<a
|
|
||||||
class="inline-block rounded-full bg-teal p-2 text-crust
|
|
||||||
shadow-sm transition hover:bg-teal/75"
|
|
||||||
href="#main-content"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Back to top</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="size-5"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293
|
|
||||||
3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4
|
|
||||||
4a1 1 0 010 1.414z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="lg:flex lg:items-end lg:justify-between">
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-center text-text lg:justify-start">
|
|
||||||
// TODO: logo/branding here
|
|
||||||
<span class="text-2xl">OSL Stats</span>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="mx-auto max-w-md text-center leading-relaxed
|
|
||||||
text-subtext0"
|
|
||||||
>placeholder text</p>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="mt-12 flex flex-wrap justify-center gap-6 md:gap-8
|
|
||||||
lg:mt-0 lg:justify-end lg:gap-12"
|
|
||||||
>
|
|
||||||
for _, item := range getFooterItems() {
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="transition hover:text-subtext1"
|
|
||||||
href={ templ.SafeURL(item.href) }
|
|
||||||
>{ item.name }</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="lg:flex lg:items-end lg:justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="mt-4 text-center text-sm text-overlay0">
|
|
||||||
by Haelnorr | placeholder text
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="mt-2 text-center">
|
|
||||||
<label
|
|
||||||
for="theme-select"
|
|
||||||
class="hidden lg:inline"
|
|
||||||
>Theme</label>
|
|
||||||
<select
|
|
||||||
name="ThemeSelect"
|
|
||||||
id="theme-select"
|
|
||||||
class="mt-1.5 inline rounded-lg bg-surface0 p-2 w-fit"
|
|
||||||
x-model="theme"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
x-for="themeopt in [
|
|
||||||
'dark',
|
|
||||||
'light',
|
|
||||||
'system',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
x-text="displayThemeName(themeopt)"
|
|
||||||
:value="themeopt"
|
|
||||||
:selected="theme === themeopt"
|
|
||||||
></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
<script>
|
|
||||||
const displayThemeName = (value) => {
|
|
||||||
if (value === "dark") return "Dark (Mocha)";
|
|
||||||
if (value === "light") return "Light (Latte)";
|
|
||||||
if (value === "system") return "System";
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
}
|
|
||||||
@@ -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() }}
|
|
||||||
<div x-data="{ open: false }">
|
|
||||||
<header class="bg-crust">
|
|
||||||
<div
|
|
||||||
class="mx-auto flex h-16 max-w-7xl items-center gap-8
|
|
||||||
px-4 sm:px-6 lg:px-8"
|
|
||||||
>
|
|
||||||
<a class="block" href="/">
|
|
||||||
<!-- logo here -->
|
|
||||||
<span class="text-3xl font-bold transition hover:text-green">
|
|
||||||
OSL Stats
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<div class="flex flex-1 items-center justify-end sm:justify-between">
|
|
||||||
@navLeft(navItems)
|
|
||||||
@navRight()
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
@sideNav(navItems)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package nav
|
|
||||||
|
|
||||||
// Returns the left portion of the navbar
|
|
||||||
templ navLeft(navItems []NavItem) {
|
|
||||||
<nav aria-label="Global" class="hidden sm:block">
|
|
||||||
<ul class="flex items-center gap-6 text-xl">
|
|
||||||
for _, item := range navItems {
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="text-subtext1 hover:text-green transition"
|
|
||||||
href={ templ.SafeURL(item.href) }
|
|
||||||
>
|
|
||||||
{ item.name }
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
}
|
|
||||||
@@ -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) }}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="sm:flex sm:gap-2">
|
|
||||||
if user != nil {
|
|
||||||
<div x-data="{ isActive: false }" class="relative">
|
|
||||||
<div
|
|
||||||
class="inline-flex items-center overflow-hidden
|
|
||||||
rounded-lg bg-sapphire hover:bg-sapphire/75 transition"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
x-on:click="isActive = !isActive"
|
|
||||||
class="h-full py-2 px-4 text-mantle hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Profile</span>
|
|
||||||
{ user.Username }
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="absolute end-0 z-10 mt-2 w-36 divide-y
|
|
||||||
divide-surface2 rounded-lg border border-surface1
|
|
||||||
bg-surface0 shadow-lg"
|
|
||||||
role="menu"
|
|
||||||
x-cloak
|
|
||||||
x-transition
|
|
||||||
x-show="isActive"
|
|
||||||
x-on:click.away="isActive = false"
|
|
||||||
x-on:keydown.escape.window="isActive = false"
|
|
||||||
>
|
|
||||||
<div class="p-2">
|
|
||||||
for _, item := range items {
|
|
||||||
<a
|
|
||||||
href={ templ.SafeURL(item.href) }
|
|
||||||
class="block rounded-lg px-4 py-2 text-md
|
|
||||||
hover:bg-crust"
|
|
||||||
role="menuitem"
|
|
||||||
>
|
|
||||||
{ item.name }
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="p-2">
|
|
||||||
<form hx-post="/logout">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="flex w-full items-center gap-2
|
|
||||||
rounded-lg px-4 py-2 text-md text-red
|
|
||||||
hover:bg-red/25 hover:cursor-pointer"
|
|
||||||
role="menuitem"
|
|
||||||
@click="isActive=false"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<button
|
|
||||||
class="hidden rounded-lg px-4 py-2 sm:block hover:cursor-pointer
|
|
||||||
bg-green hover:bg-green/75 text-mantle transition"
|
|
||||||
hx-post="/login"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
@click="open = !open"
|
|
||||||
class="block rounded-lg p-2.5 sm:hidden transition
|
|
||||||
bg-surface0 text-subtext0 hover:text-overlay2/75"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Toggle menu</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="size-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -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) }}
|
|
||||||
<div
|
|
||||||
x-show="open"
|
|
||||||
x-transition
|
|
||||||
class="absolute w-full bg-mantle sm:hidden z-10"
|
|
||||||
>
|
|
||||||
<div class="px-4 py-6">
|
|
||||||
<ul class="space-y-1">
|
|
||||||
for _, item := range navItems {
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={ templ.SafeURL(item.href) }
|
|
||||||
class="block rounded-lg px-4 py-2 text-lg
|
|
||||||
bg-surface0 text-text transition hover:bg-surface2"
|
|
||||||
>
|
|
||||||
{ item.name }
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
if user == nil {
|
|
||||||
<div class="px-4 pb-6">
|
|
||||||
<ul class="space-y-1">
|
|
||||||
<li class="flex justify-center items-center gap-2">
|
|
||||||
<a
|
|
||||||
class="w-26 px-4 py-2 rounded-lg
|
|
||||||
bg-green text-mantle transition hover:bg-green/75
|
|
||||||
text-center"
|
|
||||||
href="/login"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -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
|
// Page content for the index page
|
||||||
templ Index() {
|
templ IndexPage() {
|
||||||
@layout.Global("OSL Stats") {
|
@baseview.Layout("OSL Stats") {
|
||||||
<div class="text-center mt-25">
|
<div class="text-center mt-25">
|
||||||
<div class="text-4xl lg:text-6xl">OSL Stats</div>
|
<div class="text-4xl lg:text-6xl">OSL Stats</div>
|
||||||
<div>Placeholder text</div>
|
<div>Placeholder text</div>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package layout
|
|
||||||
|
|
||||||
templ AdminDashboard() {
|
|
||||||
@Global("Admin")
|
|
||||||
<div>
|
|
||||||
{ children... }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package page
|
package seasonsview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
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/baseview"
|
||||||
import seasoncomp "git.haelnorr.com/h/oslstats/internal/view/component/season"
|
|
||||||
import "time"
|
import "time"
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
templ SeasonPage(season *db.Season) {
|
templ DetailPage(season *db.Season) {
|
||||||
@layout.Global(season.Name) {
|
@baseview.Layout(season.Name) {
|
||||||
<div class="max-w-screen-2xl mx-auto px-4 py-8">
|
<div class="max-w-screen-2xl mx-auto px-4 py-8">
|
||||||
@SeasonDetails(season)
|
@SeasonDetails(season)
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +124,7 @@ templ SeasonDetails(season *db.Season) {
|
|||||||
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
|
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
|
||||||
<h2 class="text-2xl font-bold text-text mb-4">Status</h2>
|
<h2 class="text-2xl font-bold text-text mb-4">Status</h2>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@seasoncomp.StatusBadge(season, false, false)
|
@StatusBadge(season, false, false)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
package page
|
package seasonsview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
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/baseview"
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/component/pagination"
|
import "git.haelnorr.com/h/oslstats/internal/view/pagination"
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/component/sort"
|
import "git.haelnorr.com/h/oslstats/internal/view/sort"
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/component/season"
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
import "time"
|
import "time"
|
||||||
import "github.com/uptrace/bun"
|
import "github.com/uptrace/bun"
|
||||||
|
|
||||||
templ SeasonsPage(seasons *db.List[db.Season]) {
|
templ ListPage(seasons *db.List[db.Season]) {
|
||||||
@layout.Global("Seasons") {
|
@baseview.Layout("Seasons") {
|
||||||
<div class="max-w-screen-2xl mx-auto px-2">
|
<div class="max-w-screen-2xl mx-auto px-2">
|
||||||
@SeasonsList(seasons)
|
@SeasonsList(seasons)
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +83,7 @@ templ SeasonsList(seasons *db.List[db.Season]) {
|
|||||||
<!-- Header: Name + Status Badge -->
|
<!-- Header: Name + Status Badge -->
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<h3 class="text-xl font-bold text-text">{ s.Name }</h3>
|
<h3 class="text-xl font-bold text-text">{ s.Name }</h3>
|
||||||
@season.StatusBadge(s, true, true)
|
@StatusBadge(s, true, true)
|
||||||
</div>
|
</div>
|
||||||
<!-- Info Row: Short Name + Start Date -->
|
<!-- Info Row: Short Name + Start Date -->
|
||||||
<div class="flex items-center gap-3 text-sm">
|
<div class="flex items-center gap-3 text-sm">
|
||||||
@@ -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() {
|
||||||
<form
|
<form
|
||||||
hx-post="/seasons/new"
|
hx-post="/seasons/new"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package page
|
package seasonsview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/component/form"
|
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/layout"
|
|
||||||
|
|
||||||
templ NewSeason() {
|
templ NewPage() {
|
||||||
@layout.Global("New Season") {
|
@baseview.Layout("New Season") {
|
||||||
<div class="max-w-screen-lg mx-auto px-4 py-8">
|
<div class="max-w-screen-lg mx-auto px-4 py-8">
|
||||||
<div class="bg-mantle border border-surface1 rounded-xl">
|
<div class="bg-mantle border border-surface1 rounded-xl">
|
||||||
<div class="p-6 sm:p-8">
|
<div class="p-6 sm:p-8">
|
||||||
@@ -15,7 +14,7 @@ templ NewSeason() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-md mx-auto">
|
<div class="max-w-md mx-auto">
|
||||||
@form.NewSeason()
|
@NewForm()
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package season
|
package seasonsview
|
||||||
|
|
||||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||||
import "time"
|
import "time"
|
||||||
@@ -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
|
// Page content for the test notification page
|
||||||
templ Test() {
|
templ NotificationTestPage() {
|
||||||
@layout.Global("Notification Test") {
|
@baseview.Layout("Notification Test") {
|
||||||
<div class="flex items-center justify-center min-h-[calc(100vh-200px)]">
|
<div class="flex items-center justify-center min-h-[calc(100vh-200px)]">
|
||||||
<div class="w-full max-w-md px-4">
|
<div class="w-full max-w-md px-4">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
Reference in New Issue
Block a user