templates: Add a proper menu and navigation

This commit is contained in:
Tom Wiesing 2023-01-11 14:24:13 +01:00
parent 0bb7f99fa3
commit a00195be16
No known key found for this signature in database
76 changed files with 336 additions and 233 deletions

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@ func (dis *Distillery) init() {
lazy.RegisterPoolGroup[component.Cronable](&dis.pool) lazy.RegisterPoolGroup[component.Cronable](&dis.pool)
lazy.RegisterPoolGroup[component.UserDeleteHook](&dis.pool) lazy.RegisterPoolGroup[component.UserDeleteHook](&dis.pool)
lazy.RegisterPoolGroup[component.Table](&dis.pool) lazy.RegisterPoolGroup[component.Table](&dis.pool)
lazy.RegisterPoolGroup[component.Menuable](&dis.pool)
}) })
} }

View file

@ -25,6 +25,7 @@ type Auth struct {
var ( var (
_ component.Routeable = (*Auth)(nil) _ component.Routeable = (*Auth)(nil)
_ component.Menuable = (*Auth)(nil)
_ component.Table = (*Auth)(nil) _ component.Table = (*Auth)(nil)
) )

View file

@ -35,6 +35,9 @@ func (panel *UserPanel) Routes() component.Routes {
Prefix: "/user/", Prefix: "/user/",
CSRF: true, CSRF: true,
Decorator: panel.Dependencies.Auth.Require(nil), Decorator: panel.Dependencies.Auth.Require(nil),
MenuPriority: component.MenuUser,
MenuTitle: "User",
} }
} }
@ -81,13 +84,20 @@ type userFormContext struct {
User *models.User User *models.User
} }
func (panel *UserPanel) UserFormContext(ctx httpx.FormContext, r *http.Request) any { func (panel *UserPanel) UserFormContext(last component.MenuItem) func(ctx httpx.FormContext, r *http.Request) any {
crumbs := []component.MenuItem{
{Title: "User", Path: "/user/"},
last,
}
return func(ctx httpx.FormContext, r *http.Request) any {
user, err := panel.Dependencies.Auth.UserOf(r) user, err := panel.Dependencies.Auth.UserOf(r)
uctx := userFormContext{FormContext: ctx} uctx := userFormContext{FormContext: ctx}
panel.Dependencies.Custom.Update(&uctx, r) panel.Dependencies.Custom.Update(&uctx, r, crumbs)
if err == nil { if err == nil {
uctx.User = &user.User uctx.User = &user.User
} }
return uctx return uctx
}
} }

View file

@ -7,6 +7,7 @@ import (
_ "embed" _ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field"
@ -37,7 +38,7 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: passwordTemplate, RenderTemplate: passwordTemplate,
RenderTemplateContext: panel.UserFormContext, RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Change Password", Path: "/user/password/"}),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"] old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"]

View file

@ -1,13 +1,3 @@
{{ template "_form.html" . }} {{ template "_form.html" . }}
{{ define "form/title" }}Change Password{{ end }} {{ define "form/title" }}Change Password{{ end }}
{{ define "form/button" }}Update{{ end }} {{ define "form/button" }}Update{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/user/">{{ .User.User }}</a> &gt;
<a class="pure-button pure-button-primary" href="/user/password/">Change Password</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}

View file

@ -2,16 +2,6 @@
{{ define "form/title" }}Disable TOTP{{ end }} {{ define "form/title" }}Disable TOTP{{ end }}
{{ define "form/button" }}Disable{{ end }} {{ define "form/button" }}Disable{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/user/">{{ .User.User }}</a> &gt;
<a class="pure-button pure-button-primary" href="/user/totp/disable/">Disable TOTP</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div> <div>
<ul> <ul>

View file

@ -1,15 +1,6 @@
{{ template "_form.html" . }} {{ template "_form.html" . }}
{{ define "form/title" }}Enable TOTP{{ end }} {{ define "form/title" }}Enable TOTP{{ end }}
{{ define "form/button" }}Enable{{ end }} {{ define "form/button" }}Enable{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/user/">{{ .User.User }}</a> &gt;
<a class="pure-button pure-button-primary" href="/user/totp/enable/">Enable TOTP</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div> <div>
<ul> <ul>

View file

@ -1,15 +1,6 @@
{{ template "_form.html" . }} {{ template "_form.html" . }}
{{ define "form/title" }}Enable TOTP{{ end }} {{ define "form/title" }}Enable TOTP{{ end }}
{{ define "form/button" }}Enable{{ end }} {{ define "form/button" }}Enable{{ end }}
{{ define "header" }}
<p>
<a class="pure-button" href="/user/">{{ .User.User }}</a> &gt;
<a class="pure-button pure-button-primary" href="/user/totp/enroll/">Enroll TOTP</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}
{{ define "form/inside" }} {{ define "form/inside" }}
<div> <div>
<a href="{{ .TOTPURL }}"> <a href="{{ .TOTPURL }}">

View file

@ -1,15 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}User{{ end }} {{ define "title" }}User{{ end }}
{{ define "header"}}
<p>
<a class="pure-button pure-button-primary" href="/user/">{{ .User.User }}</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<p> <p>

View file

@ -5,6 +5,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -32,7 +33,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
}, },
RenderTemplate: totpEnableTemplate, RenderTemplate: totpEnableTemplate,
RenderTemplateContext: panel.UserFormContext, RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Enable TOTP", Path: "/user/totp/enable/"}),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password := values["password"] password := values["password"]
@ -77,7 +78,10 @@ type totpEnrollContext struct {
func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
totpEnrollTemplate := panel.Dependencies.Custom.Template(totpEnrollTemplate) totpEnrollTemplate := panel.Dependencies.Custom.Template(totpEnrollTemplate)
crumbs := []component.MenuItem{
{Title: "User", Path: "/user/"},
{Title: "Enable TOTP", Path: "/user/totp/enable/"},
}
return &httpx.Form[struct{}]{ return &httpx.Form[struct{}]{
Fields: []field.Field{ Fields: []field.Field{
{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"}, {Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"},
@ -99,7 +103,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
FormContext: context, FormContext: context,
}, },
} }
panel.Dependencies.Custom.Update(&ctx.userFormContext, r) panel.Dependencies.Custom.Update(&ctx.userFormContext, r, crumbs)
if err == nil && user != nil { if err == nil && user != nil {
ctx.userFormContext.User = &user.User ctx.userFormContext.User = &user.User
@ -164,7 +168,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled() return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
}, },
RenderTemplate: totpDisableTemplate, RenderTemplate: totpDisableTemplate,
RenderTemplateContext: panel.UserFormContext, RenderTemplateContext: panel.UserFormContext(component.MenuItem{Title: "Disable TOTP", Path: "/user/totp/disable/"}),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) { Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"] password, otp := values["password"], values["otp"]

View file

@ -7,6 +7,7 @@ import (
_ "embed" _ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
@ -35,9 +36,13 @@ type GrantWithURL struct {
func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
userTemplate := panel.Dependencies.Custom.Template(userTemplate) userTemplate := panel.Dependencies.Custom.Template(userTemplate)
crumbs := []component.MenuItem{
{Title: "User", Path: "/user/"},
}
return &httpx.HTMLHandler[routeUserContext]{ return &httpx.HTMLHandler[routeUserContext]{
Handler: func(r *http.Request) (ruc routeUserContext, err error) { Handler: func(r *http.Request) (ruc routeUserContext, err error) {
panel.Dependencies.Custom.Update(&ruc, r) panel.Dependencies.Custom.Update(&ruc, r, crumbs)
// find the user // find the user
ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r) ruc.AuthUser, err = panel.Dependencies.Auth.UserOf(r)

View file

@ -80,6 +80,17 @@ func (auth *Auth) Require(perm Permission) func(http.Handler) http.Handler {
} }
} }
// Has checks if the given request has the given permission.
// If an error occurs, returns false.
func (auth *Auth) Has(perm Permission, r *http.Request) bool {
user, err := auth.UserOf(r)
if err != nil || user == nil {
return false
}
ok, err := perm.Permit(user, r)
return err == nil && ok.Granted()
}
// Admin represents a permission that checks if a user is an administrator and has totp enabled. // Admin represents a permission that checks if a user is an administrator and has totp enabled.
var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) { var Admin Permission = func(user *AuthUser, r *http.Request) (ok Grant, err error) {
return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and passcode enabled"), nil return Bool2Grant(user != nil && user.IsAdmin() && user.IsTOTPEnabled(), "user needs to have admin permissions and passcode enabled"), nil

View file

@ -3,8 +3,10 @@ package auth
import ( import (
"context" "context"
"errors" "errors"
"html/template"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
@ -67,6 +69,22 @@ func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
}).Get(r, control.SessionCookie) }).Get(r, control.SessionCookie)
} }
func (auth *Auth) Menu(r *http.Request) []component.MenuItem {
user, err := auth.UserOf(r)
if user == nil || err != nil {
return nil
}
return []component.MenuItem{
{
Title: "Logout",
Path: "/auth/logout",
Priority: component.MenuAuth,
},
}
}
type contextUserKey struct{} type contextUserKey struct{}
var ctxUserKey = contextUserKey{} var ctxUserKey = contextUserKey{}
@ -126,7 +144,9 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
if context.Err != nil { if context.Err != nil {
context.Err = errLoginFailed context.Err = errLoginFailed
} }
httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context, r), nil, loginTemplate, "", w, r) httpx.WriteHTML(auth.Dependencies.Custom.NewForm(context, r, []component.MenuItem{
{Title: "Login", Path: template.URL(r.URL.RequestURI())},
}), nil, loginTemplate, "", w, r)
}, },
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) { Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {

View file

@ -40,6 +40,7 @@ type Admin struct {
var ( var (
_ component.DistilleryFetcher = (*Admin)(nil) _ component.DistilleryFetcher = (*Admin)(nil)
_ component.Routeable = (*Admin)(nil) _ component.Routeable = (*Admin)(nil)
_ component.Menuable = (*Admin)(nil)
) )
func (admin *Admin) Routes() component.Routes { func (admin *Admin) Routes() component.Routes {
@ -50,6 +51,19 @@ func (admin *Admin) Routes() component.Routes {
} }
} }
func (admin *Admin) Menu(r *http.Request) []component.MenuItem {
if !admin.Dependencies.Auth.Has(auth.Admin, r) {
return nil
}
return []component.MenuItem{
{
Title: "Admin",
Path: "/admin/",
Priority: component.MenuAdmin,
},
}
}
func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) { func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
router := httprouter.New() router := httprouter.New()
@ -62,17 +76,17 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
} }
} }
// handle everything
router.HandlerFunc(http.MethodGet, route, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, route+"index", http.StatusTemporaryRedirect)
})
// add a handler for the index page // add a handler for the index page
router.Handler(http.MethodGet, route+"index", httpx.HTMLHandler[indexContext]{ router.Handler(http.MethodGet, route, httpx.HTMLHandler[indexContext]{
Handler: admin.index, Handler: admin.index,
Template: admin.Dependencies.Custom.Template(indexTemplate), Template: admin.Dependencies.Custom.Template(indexTemplate),
}) })
// fallback to the "/" page
router.HandlerFunc(http.MethodGet, route+"index", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, route, http.StatusTemporaryRedirect)
})
// add a handler for the user page // add a handler for the user page
router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{ router.Handler(http.MethodGet, route+"users", httpx.HTMLHandler[userContext]{
Handler: admin.users, Handler: admin.users,

View file

@ -1,10 +1,12 @@
package admin package admin
import ( import (
"html/template"
"net/http" "net/http"
_ "embed" _ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
@ -28,7 +30,10 @@ type componentContext struct {
} }
func (admin *Admin) components(r *http.Request) (cp componentContext, err error) { func (admin *Admin) components(r *http.Request) (cp componentContext, err error) {
admin.Dependencies.Custom.Update(&cp, r) admin.Dependencies.Custom.Update(&cp, r, []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Components", Path: "/admin/components/"},
})
cp.Analytics = *admin.Analytics cp.Analytics = *admin.Analytics
return return
@ -49,10 +54,15 @@ type ingredientsContext struct {
} }
func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) { func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) {
admin.Dependencies.Custom.Update(&cp, r) slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
admin.Dependencies.Custom.Update(&cp, r, []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Instance", Path: template.URL("/admin/instance/" + slug)},
{Title: "Ingredients", Path: template.URL("/admin/instance/" + slug + "/ingredients/")},
})
// find the instance itself! // find the instance itself!
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound { if err == instances.ErrWissKINotFound {
return cp, httpx.ErrNotFound return cp, httpx.ErrNotFound

View file

@ -3,8 +3,10 @@ package admin
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
@ -38,7 +40,11 @@ type grantsContext struct {
} }
func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (err error) { func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (err error) {
admin.Dependencies.Custom.Update(gc, r) admin.Dependencies.Custom.Update(gc, r, []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Instance", Path: template.URL("/admin/instance/" + slug)},
{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")},
})
// find the instance itself // find the instance itself
gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug) gc.instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)

View file

@ -1,13 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - Components Page{{ end }} {{ define "title" }}Distillery Admin - Components Page{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/components">Components</a>
</p>
{{ end }}
{{ define "content" }} {{ define "content" }}
{{ template "_anal.html" .Analytics }} {{ template "_anal.html" .Analytics }}
{{ end }} {{ end }}

View file

@ -1,14 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Grants{{ end }} {{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Grants{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/grants/{{ .Instance.Slug }}">Grants</a>
</p>
{{ end }}
{{ define "content" }} {{ define "content" }}
{{ $csrf := .CSRF }} {{ $csrf := .CSRF }}
{{ $slug := .Instance.Slug }} {{ $slug := .Instance.Slug }}

View file

@ -1,10 +1,7 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Distillery Admin{{ end }} {{ define "title" }}Distillery Admin{{ end }}
{{ define "header"}} {{ define "header" }}
<p>
<a class="pure-button pure-button-primary" href="/admin/index">Admin</a>
</p>
<p> <p>
<div class="pure-button-group" role="group" aria-label="Actions"> <div class="pure-button-group" role="group" aria-label="Actions">
<a class="pure-button" href="/admin/users">Users</a> <a class="pure-button" href="/admin/users">Users</a>

View file

@ -1,14 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Ingredients{{ end }} {{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Ingredients{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/ingredients/{{ .Instance.Slug }}">Ingredients</a>
</p>
{{ end }}
{{ define "content" }} {{ define "content" }}
{{ template "_anal.html" .Analytics }} {{ template "_anal.html" .Analytics }}
{{ end }} {{ end }}

View file

@ -1,11 +1,7 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }}{{ end }} {{ define "title" }}Distillery Admin - {{ .Instance.Slug }}{{ end }}
{{ define "header"}} {{ define "header" }}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/instance/{{ .Instance.Slug }}">Instance</a>
</p>
<p> <p>
<div class="pure-button-group" role="group" aria-label="Actions"> <div class="pure-button-group" role="group" aria-label="Actions">
<a class="pure-button" href="/admin/grants/{{ .Info.Slug }}">Grants</a> <a class="pure-button" href="/admin/grants/{{ .Info.Slug }}">Grants</a>

View file

@ -2,10 +2,3 @@
{{ define "form/title" }}Distillery Admin - Create User{{ end }} {{ define "form/title" }}Distillery Admin - Create User{{ end }}
{{ define "form/button" }}Create{{ end }} {{ define "form/button" }}Create{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button" href="/admin/users">Users</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/users/create">Create</a>
</p>
{{ end }}

View file

@ -1,11 +1,7 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - Users{{ end }} {{ define "title" }}Distillery Admin - Users{{ end }}
{{ define "header"}} {{ define "header" }}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/users">Users</a>
</p>
<p> <p>
<div class="pure-button-group" role="group" aria-label="Actions"> <div class="pure-button-group" role="group" aria-label="Actions">
<a class="pure-button" href="/admin/users/create">Create New</a> <a class="pure-button" href="/admin/users/create">Create New</a>

View file

@ -87,7 +87,9 @@ type indexContext struct {
} }
func (admin *Admin) index(r *http.Request) (idx indexContext, err error) { func (admin *Admin) index(r *http.Request) (idx indexContext, err error) {
admin.Dependencies.Custom.Update(&idx, r) admin.Dependencies.Custom.Update(&idx, r, []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
})
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true) idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
return return
} }

View file

@ -2,8 +2,10 @@ package admin
import ( import (
_ "embed" _ "embed"
"html/template"
"net/http" "net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
@ -28,10 +30,14 @@ type instanceContext struct {
} }
func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) { func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) {
admin.Dependencies.Custom.Update(&is, r) slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
admin.Dependencies.Custom.Update(&is, r, []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Instance", Path: template.URL("/admin/instance/" + slug)},
})
// find the instance itself! // find the instance itself!
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug) instance, err := admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound { if err == instances.ErrWissKINotFound {
return is, httpx.ErrNotFound return is, httpx.ErrNotFound

View file

@ -8,6 +8,7 @@ import (
_ "embed" _ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
@ -31,7 +32,10 @@ type userContext struct {
} }
func (admin *Admin) users(r *http.Request) (uc userContext, err error) { func (admin *Admin) users(r *http.Request) (uc userContext, err error) {
admin.Dependencies.Custom.Update(&uc, r) admin.Dependencies.Custom.Update(&uc, r, []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users/"},
})
uc.Error = r.URL.Query().Get("error") uc.Error = r.URL.Query().Get("error")
uc.Users, err = admin.Dependencies.Auth.Users(r.Context()) uc.Users, err = admin.Dependencies.Auth.Users(r.Context())
@ -58,6 +62,11 @@ type createUserResult struct {
func (admin *Admin) createUser(ctx context.Context) http.Handler { func (admin *Admin) createUser(ctx context.Context) http.Handler {
userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate) userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate)
crumbs := []component.MenuItem{
{Title: "Admin", Path: "/admin/"},
{Title: "Users", Path: "/admin/users"},
{Title: "Create", Path: "/admin/users/create"},
}
return &httpx.Form[createUserResult]{ return &httpx.Form[createUserResult]{
Fields: []field.Field{ Fields: []field.Field{
@ -68,7 +77,9 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
FieldTemplate: field.PureCSSFieldTemplate, FieldTemplate: field.PureCSSFieldTemplate,
RenderTemplate: userCreateTemplate, RenderTemplate: userCreateTemplate,
RenderTemplateContext: admin.Dependencies.Custom.RenderContext, RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any {
return admin.Dependencies.Custom.NewForm(ctx, r, crumbs)
},
Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) { Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) {
cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked

View file

@ -32,6 +32,9 @@ func (*Home) Routes() component.Routes {
Prefix: "/", Prefix: "/",
MatchAllDomains: true, MatchAllDomains: true,
CSRF: false, CSRF: false,
MenuTitle: "WissKI Distillery",
MenuPriority: component.MenuHome,
} }
} }

View file

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
"github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/internal/status"
@ -14,7 +15,7 @@ import (
//go:embed "public.html" //go:embed "public.html"
var publicHTMLStr string var publicHTMLStr string
var publicTemplate = static.AssetsHome.MustParseShared("public.html", publicHTMLStr) var publicTemplate = static.AssetsDefault.MustParseShared("public.html", publicHTMLStr)
type publicContext struct { type publicContext struct {
custom.BaseContext custom.BaseContext
@ -24,6 +25,9 @@ type publicContext struct {
} }
func (home *Home) publicHandler(ctx context.Context) http.Handler { func (home *Home) publicHandler(ctx context.Context) http.Handler {
crumbs := []component.MenuItem{
{Title: "WissKI Distillery", Path: "/"},
}
return httpx.HTMLHandler[publicContext]{ return httpx.HTMLHandler[publicContext]{
Handler: func(r *http.Request) (pc publicContext, err error) { Handler: func(r *http.Request) (pc publicContext, err error) {
// only act on the root path! // only act on the root path!
@ -31,7 +35,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
return pc, httpx.ErrNotFound return pc, httpx.ErrNotFound
} }
home.Dependencies.Custom.Update(&pc, r) home.Dependencies.Custom.Update(&pc, r, crumbs)
pc.Instances = home.homeInstances.Get(nil) pc.Instances = home.homeInstances.Get(nil)
pc.SelfRedirect = home.Config.SelfRedirect.String() pc.SelfRedirect = home.Config.SelfRedirect.String()

View file

@ -1,16 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}WissKI Distillery{{ end }} {{ define "title" }}WissKI Distillery{{ end }}
{{ define "header"}}
<p>
<a class="pure-button pure-button-primary" href="/">Home</a>
</p>
<p>
<a class="pure-button" href="/news/">News</a>
</p>
{{ end }}
{{ define "content" }} {{ define "content" }}
{{ block "@custom/about" . }} {{ block "@custom/about" . }}
<div class="pure-u-1"> <div class="pure-u-1">

View file

@ -28,7 +28,7 @@ var (
//go:embed "legal.html" //go:embed "legal.html"
var legalTemplateString string var legalTemplateString string
var legalTemplate = static.AssetsLegal.MustParseShared("legal.html", legalTemplateString) var legalTemplate = static.AssetsDefault.MustParseShared("legal.html", legalTemplateString)
func (legal *Legal) Routes() component.Routes { func (legal *Legal) Routes() component.Routes {
return component.Routes{ return component.Routes{
@ -59,7 +59,9 @@ type legalContext struct {
} }
func (legal *Legal) context(r *http.Request) (lc legalContext, err error) { func (legal *Legal) context(r *http.Request) (lc legalContext, err error) {
legal.Dependencies.Custom.Update(&lc, r) legal.Dependencies.Custom.Update(&lc, r, []component.MenuItem{
{Title: "Legal", Path: "/legal/"},
})
lc.LegalNotices = cli.LegalNotices lc.LegalNotices = cli.LegalNotices

View file

@ -1,10 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}Legal{{ end }} {{ define "title" }}Legal{{ end }}
{{ define "header"}}
<!-- no header -->
{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<h2 id="cookies">Cookie Usage</h2> <h2 id="cookies">Cookie Usage</h2>

View file

@ -36,6 +36,9 @@ func (*News) Routes() component.Routes {
Prefix: "/news/", Prefix: "/news/",
Exact: true, Exact: true,
CSRF: false, CSRF: false,
MenuTitle: "News",
MenuPriority: component.MenuNews,
} }
} }
@ -111,7 +114,7 @@ func Items() ([]Item, error) {
//go:embed "news.html" //go:embed "news.html"
var newsHTMLStr string var newsHTMLStr string
var newsTemplate = static.AssetsHome.MustParseShared("news.html", newsHTMLStr) var newsTemplate = static.AssetsDefault.MustParseShared("news.html", newsHTMLStr)
type newsContext struct { type newsContext struct {
custom.BaseContext custom.BaseContext
@ -120,6 +123,10 @@ type newsContext struct {
// HandleRoute returns the handler for the requested path // HandleRoute returns the handler for the requested path
func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) { func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
crumbs := []component.MenuItem{
{Title: "News", Path: "/news/"},
}
items, itemsErr := Items() items, itemsErr := Items()
if itemsErr != nil { if itemsErr != nil {
zerolog.Ctx(ctx).Err(itemsErr).Msg("Unable to load news items") zerolog.Ctx(ctx).Err(itemsErr).Msg("Unable to load news items")
@ -127,7 +134,7 @@ func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, e
return httpx.HTMLHandler[newsContext]{ return httpx.HTMLHandler[newsContext]{
Handler: func(r *http.Request) (nc newsContext, err error) { Handler: func(r *http.Request) (nc newsContext, err error) {
news.Dependencies.Custom.Update(&nc, r) news.Dependencies.Custom.Update(&nc, r, crumbs)
nc.Items, err = items, itemsErr nc.Items, err = items, itemsErr
return return

View file

@ -1,13 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}News{{ end }} {{ define "title" }}News{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/">Home</a> &gt;
<a class="pure-button pure-button-primary" href="/news/">News</a>
</p>
{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">

View file

@ -21,7 +21,7 @@ type Assets struct {
Styles string // <link> tags inserted by the asset Styles string // <link> tags inserted by the asset
} }
//go:generate node build.mjs Home User Admin Legal //go:generate node build.mjs Default User Admin
// MustParse parses a new template from the given source // MustParse parses a new template from the given source
// and calls [RegisterAssoc] on it. // and calls [RegisterAssoc] on it.

View file

@ -7,26 +7,20 @@ import _ "embed"
//go:embed "assets_disclaimer.txt" //go:embed "assets_disclaimer.txt"
var AssetsDisclaimer string var AssetsDisclaimer string
// AssetsHome contains assets for the 'Home' entrypoint. // AssetsDefault contains assets for the 'Default' entrypoint.
var AssetsHome = Assets{ var AssetsDefault = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Home.f9675eae.css">`, Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Default.f9675eae.css">`,
} }
// AssetsUser contains assets for the 'User' entrypoint. // AssetsUser contains assets for the 'User' entrypoint.
var AssetsUser = Assets{ var AssetsUser = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`, Scripts: `<script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/User.4197014b.js"></script><script src="/static/User.30d54198.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/User.38d394c2.css">`,
} }
// AssetsAdmin contains assets for the 'Admin' entrypoint. // AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{ var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/static/User.30d54198.js"></script><script type="module" src="/static/User.4197014b.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`, Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.38d394c2.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
}
// AssetsLegal contains assets for the 'Legal' entrypoint.
var AssetsLegal = Assets{
Scripts: `<script type="module" src="/static/Home.38d394c2.js"></script><script src="/static/Home.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Legal.38d394c2.js"></script><script src="/static/Legal.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Home.4b303448.css"><link rel="stylesheet" href="/static/Legal.d1531eba.css"><link rel="stylesheet" href="/static/Legal.20259812.css">`,
} }

View file

@ -10,6 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"github.com/tkw1536/goprogram/lib/reflectx" "github.com/tkw1536/goprogram/lib/reflectx"
"golang.org/x/exp/slices"
) )
// baseContextName is the name of the [BaseContext] type // baseContextName is the name of the [BaseContext] type
@ -25,6 +26,10 @@ type BaseContext struct {
GeneratedAt time.Time // time this page was generated at GeneratedAt time.Time // time this page was generated at
// Menu and breadcrumbs
Menu []component.MenuItem
Crumbs []component.MenuItem
CSRF template.HTML // CSRF Field CSRF template.HTML // CSRF Field
} }
@ -43,7 +48,8 @@ const (
// The given request *must not* be nil. // The given request *must not* be nil.
// //
// For convenience the passed context is also returned. // For convenience the passed context is also returned.
func (tc *BaseContext) use(base component.Base, r *http.Request) *BaseContext { func (tc *BaseContext) use(custom *Custom, r *http.Request, crumbs []component.MenuItem) *BaseContext {
// tc.custom = custom
tc.inited = true tc.inited = true
tc.requestWasNil = r == nil tc.requestWasNil = r == nil
@ -55,9 +61,20 @@ func (tc *BaseContext) use(base component.Base, r *http.Request) *BaseContext {
tc.CSRF = csrf.TemplateField(r) tc.CSRF = csrf.TemplateField(r)
} }
// build the menu
tc.Menu = custom.BuildMenu(r)
// build the breadcrumbs
tc.Crumbs = slices.Clone(crumbs)
last := len(tc.Crumbs) - 1
for i := range tc.Crumbs {
tc.Crumbs[i].Active = i == last
}
return tc return tc
} }
// DoInitCheck is called by the template to check that the BaseContext was initialized properly
func (bc BaseContext) DoInitCheck() template.HTML { func (bc BaseContext) DoInitCheck() template.HTML {
if !bc.inited { if !bc.inited {
return initError return initError
@ -69,26 +86,21 @@ func (bc BaseContext) DoInitCheck() template.HTML {
} }
// NewForm is like New, but returns a new BaseFormContext // NewForm is like New, but returns a new BaseFormContext
func (custom *Custom) NewForm(context httpx.FormContext, r *http.Request) (ctx BaseFormContext) { func (custom *Custom) NewForm(context httpx.FormContext, r *http.Request, crumbs []component.MenuItem) (ctx BaseFormContext) {
ctx.FormContext = context ctx.FormContext = context
ctx.use(custom.Base, r) ctx.use(custom, r, crumbs)
return return
} }
// RenderContext is exactly like NewForm, but returns any to be used as httpx.Form.RenderTemplateContext
func (custom *Custom) RenderContext(ctx httpx.FormContext, r *http.Request) any {
return custom.NewForm(ctx, r)
}
// Update updates an embedded BaseContext field in context. // Update updates an embedded BaseContext field in context.
// //
// Assumes that context is a pointer to a struct type. // Assumes that context is a pointer to a struct type.
// If this is not the case, might call panic(). // If this is not the case, might call panic().
func (custom *Custom) Update(context any, r *http.Request) *BaseContext { func (custom *Custom) Update(context any, r *http.Request, crumbs []component.MenuItem) *BaseContext {
ctx := reflect.ValueOf(context). ctx := reflect.ValueOf(context).
Elem().FieldByName(baseContextName).Addr(). Elem().FieldByName(baseContextName).Addr().
Interface().(*BaseContext) Interface().(*BaseContext)
ctx.use(custom.Base, r) ctx.use(custom, r, crumbs)
return ctx return ctx
} }

View file

@ -2,16 +2,20 @@ package custom
import ( import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
) )
// Custom implements theme and page customization. // Custom implements theme and page customization.
type Custom struct { type Custom struct {
component.Base component.Base
Dependencies struct { Dependencies struct {
// nothing yet Routeables []component.Routeable
Menuable []component.Menuable
} }
menu lazy.Lazy[[]component.MenuItem]
} }
var ( var (
_ component.Backupable = (*Custom)(nil) _ component.Backupable = (*Custom)(nil)
_ component.Menuable = (*Custom)(nil)
) )

View file

@ -0,0 +1,47 @@
package custom
import (
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/pkg/mux"
"golang.org/x/exp/slices"
)
// getMenuItems gets a fresh copy of the cached slice of menu items
func (custom *Custom) Menu(r *http.Request) []component.MenuItem {
return custom.menu.Get(func() []component.MenuItem {
items := make([]component.MenuItem, 0, len(custom.Dependencies.Routeables))
for _, route := range custom.Dependencies.Routeables {
routes := route.Routes()
if routes.MenuTitle == "" {
continue
}
items = append(items, component.MenuItem{
Title: routes.MenuTitle,
Priority: routes.MenuPriority,
Path: template.URL(routes.Prefix),
})
}
slices.SortFunc(items, component.MenuItemSort)
return items
})
}
func (custom *Custom) BuildMenu(r *http.Request) []component.MenuItem {
// NOTE(twiesing): Don't name this method "Menu", as it will cause
// a stack overflow.
path := mux.NormalizePath(r.URL.Path)
// get the static menu items, and then return all the regular ones
var items []component.MenuItem
for _, m := range custom.Dependencies.Menuable {
items = append(items, m.Menu(r)...)
}
for i, item := range items {
items[i].Active = string(item.Path) == path
}
slices.SortFunc(items, component.MenuItemSort)
return items
}

View file

@ -1 +0,0 @@
html{background-color:#87a485}body{max-width:80vw!important}

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,30 @@ footer {
margin: 2em; margin: 2em;
} }
nav.pure-menu, nav.breadcrumbs {
padding-top: 1em;
padding-bottom: 1em;
border-bottom: 1px solid black;
}
nav.breadcrumbs {
padding-left: 1em;
font-size: small;
}
nav.breadcrumbs a:not(:last-child)::after {
cursor: default;
content: " > ";
color: black;
}
nav.breadcrumbs a {
text-decoration: none;
color: blue !important;
}
nav.breadcrumbs a.active {
font-weight: bold;
}
footer { footer {
font-size: small; font-size: small;
border-top: 1px solid black; border-top: 1px solid black;

View file

@ -1,7 +0,0 @@
html {
background-color: #87A485;
}
body {
max-width: 80vw !important;
}

View file

@ -1 +0,0 @@
import "latex.css/style.min.css"

View file

@ -10,6 +10,21 @@
<body> <body>
{{ .BaseContext.DoInitCheck }} {{ .BaseContext.DoInitCheck }}
<nav class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
{{ range .BaseContext.Menu }}
<li class="pure-menu-item{{ if .Active }} pure-menu-selected{{ end }}">
<a href="{{ .Path }}" class="pure-menu-link">{{ .Title }}</a>
</li>
{{ end }}
</ul>
</nav>
<nav class="breadcrumbs" role="group" aria-label="Breadcrumbs">
{{ range .BaseContext.Crumbs }}
<a class="{{ if .Active }}active{{ end }}" href="{{ .Path }}">{{ .Title }}</a>
{{ end }}
</nav>
<header> <header>
<h1 id="top">{{ template "title" . }}</h1> <h1 id="top">{{ template "title" . }}</h1>
{{ block "header" . }}<!-- no header by default -->{{ end }} {{ block "header" . }}<!-- no header by default -->{{ end }}

View file

@ -1,7 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}{{ block "form/title" . }}Form{{ end }}{{ end }} {{ define "title" }}{{ block "form/title" . }}Form{{ end }}{{ end }}
{{ define "header" }}<!-- no header -->{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
{{ block "form/extra" . }}<!-- no extra -->{{ end }} {{ block "form/extra" . }}<!-- no extra -->{{ end }}

View file

@ -0,0 +1,36 @@
package component
import (
"html/template"
"net/http"
)
// Menuable is a component that provides a menu
type Menuable interface {
Component
Menu(r *http.Request) []MenuItem
}
type MenuItem struct {
Title string
Path template.URL
Active bool
Priority MenuPriority // menu priority
}
func MenuItemSort(a, b MenuItem) bool {
return a.Priority < b.Priority
}
type MenuPriority int
// Menu* indicates priorities of the menu
const (
MenuHome MenuPriority = iota
MenuNews
MenuResolver
MenuUser
MenuAdmin
MenuAuth
)

View file

@ -41,12 +41,15 @@ func (resolver *Resolver) Routes() component.Routes {
Prefix: "/wisski/get/", Prefix: "/wisski/get/",
Aliases: []string{"/go/"}, Aliases: []string{"/go/"},
CSRF: false, CSRF: false,
MenuTitle: "Resolver",
MenuPriority: component.MenuResolver,
} }
} }
//go:embed "resolver.html" //go:embed "resolver.html"
var resolverHTMLStr string var resolverHTMLStr string
var resolverTemplate = static.AssetsHome.MustParseShared("resolver.html", resolverHTMLStr) var resolverTemplate = static.AssetsDefault.MustParseShared("resolver.html", resolverHTMLStr)
type resolverContext struct { type resolverContext struct {
custom.BaseContext custom.BaseContext
@ -55,6 +58,9 @@ type resolverContext struct {
func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) { func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.Handler, error) {
resolverTemplate := resolver.Dependencies.Custom.Template(resolverTemplate) resolverTemplate := resolver.Dependencies.Custom.Template(resolverTemplate)
crumbs := []component.MenuItem{
{Title: "Resolver", Path: "/wisski/get/"},
}
logger := zerolog.Ctx(ctx) logger := zerolog.Ctx(ctx)
@ -65,7 +71,7 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
ctx := resolverContext{ ctx := resolverContext{
IndexContext: context, IndexContext: context,
} }
resolver.Dependencies.Custom.Update(&ctx, r) resolver.Dependencies.Custom.Update(&ctx, r, crumbs)
httpx.WriteHTML(ctx, nil, resolverTemplate, "", w, r) httpx.WriteHTML(ctx, nil, resolverTemplate, "", w, r)
} }

View file

@ -1,10 +1,6 @@
{{ template "_base.html" . }} {{ template "_base.html" . }}
{{ define "title" }}WissKI Resolver{{ end }} {{ define "title" }}WissKI Resolver{{ end }}
{{ define "header"}}
<!-- no header -->
{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="pure-u-1"> <div class="pure-u-1">
<h1>wdresolve</h1> <h1>wdresolve</h1>

View file

@ -26,6 +26,10 @@ type Routes struct {
// MatchAllDomains indicates that all domains, even the non-default domain, should be matched // MatchAllDomains indicates that all domains, even the non-default domain, should be matched
MatchAllDomains bool MatchAllDomains bool
// MenuTitle and MenuPriority return the priority and title of this menu item
MenuTitle string
MenuPriority MenuPriority
// Exact indicates that only the exact prefix, as opposed to any sub-paths, are matched. // Exact indicates that only the exact prefix, as opposed to any sub-paths, are matched.
// Trailing '/'s are automatically trimmed, even with an exact match. // Trailing '/'s are automatically trimmed, even with an exact match.
Exact bool Exact bool

View file

@ -52,7 +52,7 @@ func (mux *Mux[T]) Add(path string, predicate Predicate, exact bool, h http.Hand
mux.prefixes = make(map[string][]handler) mux.prefixes = make(map[string][]handler)
} }
mPath := normalizePath(path) mPath := NormalizePath(path)
mHandler := handler{Predicate: predicate, Handler: h} mHandler := handler{Predicate: predicate, Handler: h}
if exact { if exact {
mux.exacts[mPath] = append(mux.exacts[mPath], mHandler) mux.exacts[mPath] = append(mux.exacts[mPath], mHandler)
@ -71,7 +71,7 @@ func (mux *Mux[T]) Match(r *http.Request, prepare bool) (http.Handler, bool) {
r = mux.Prepare(r) r = mux.Prepare(r)
} }
candidate := normalizePath(r.URL.Path) candidate := NormalizePath(r.URL.Path)
// match the exact path first // match the exact path first
for _, h := range mux.exacts[candidate] { for _, h := range mux.exacts[candidate] {

View file

@ -4,9 +4,9 @@ import (
"path" "path"
) )
// normalizePath normalizes the provided path. // NormalizePath normalizes the provided path.
// It ensures that there is both a leading and trailing slash. // It ensures that there is both a leading and trailing slash.
func normalizePath(value string) string { func NormalizePath(value string) string {
value = path.Clean(value) value = path.Clean(value)
if value != "/" { if value != "/" {
value = value + "/" value = value + "/"