diff --git a/internal/routes/town.go b/internal/routes/town.go index 6a2d243..ad61ee1 100644 --- a/internal/routes/town.go +++ b/internal/routes/town.go @@ -3,11 +3,8 @@ package routes import ( "dk/internal/middleware" "dk/internal/router" - "dk/internal/template" "dk/internal/template/components" - "fmt" - - "github.com/valyala/fasthttp" + "dk/internal/towns" ) func RegisterTownRoutes(r *router.Router) { @@ -19,21 +16,8 @@ func RegisterTownRoutes(r *router.Router) { } func showTown(ctx router.Ctx, _ []string) { - tmpl, err := template.Cache.Load("town/town.html") - if err != nil { - ctx.SetStatusCode(fasthttp.StatusInternalServerError) - fmt.Fprintf(ctx, "Template error: %v", err) - return - } - - content := tmpl.RenderNamed(map[string]any{ - "town": ctx.UserValue("town"), + town := ctx.UserValue("town").(*towns.Town) + components.RenderPageTemplate(ctx, town.Name, "town/town.html", map[string]any{ + "town": town, }) - - pageData := components.NewPageData("Town - Dragon Knight", content) - if err := components.RenderPage(ctx, pageData, nil); err != nil { - ctx.SetStatusCode(fasthttp.StatusInternalServerError) - fmt.Fprintf(ctx, "Template error: %v", err) - return - } } diff --git a/internal/template/components/components.go b/internal/template/components/page.go similarity index 61% rename from internal/template/components/components.go rename to internal/template/components/page.go index 535db86..d295936 100644 --- a/internal/template/components/components.go +++ b/internal/template/components/page.go @@ -3,29 +3,15 @@ package components import ( "fmt" "maps" + "strings" "dk/internal/auth" - "dk/internal/csrf" "dk/internal/middleware" "dk/internal/router" "dk/internal/template" -) -// GenerateTopNav generates the top navigation HTML based on authentication status -func GenerateTopNav(ctx router.Ctx) string { - if middleware.IsAuthenticated(ctx) { - csrfField := csrf.HiddenField(ctx, auth.Manager) - return fmt.Sprintf(`
- %s - -
- Help`, csrfField) - } else { - return `Log In - Register - Help` - } -} + "github.com/valyala/fasthttp" +) // PageData holds common page template data type PageData struct { @@ -100,3 +86,39 @@ func NewPageData(title, content string) PageData { Build: "dev", } } + +// PageTitle returns a proper title for a rendered page. If an empty string +// is given, returns "Dragon Knight". If the provided title already has " - Dragon Knight" +// at the end, returns title as-is. Appends " - Dragon Knight" to title otherwise. +func PageTitle(title string) string { + if title == "" { + return "Dragon Knight" + } + + if strings.HasSuffix(" - Dragon Knight", title) { + return title + } + + return title + " - Dragon Knight" +} + +// RenderPageTemplate is a simplified helper that renders a template within the page layout. +// It loads the template, renders it with the provided data, and then renders the full page. +// Returns true if successful, false if an error occurred (error is written to response). +func RenderPageTemplate(ctx router.Ctx, title, templateName string, data map[string]any) bool { + content, err := template.RenderNamed(templateName, data) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + fmt.Fprintf(ctx, "Template error: %v", err) + return false + } + + pageData := NewPageData(PageTitle(title), content) + if err := RenderPage(ctx, pageData, nil); err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + fmt.Fprintf(ctx, "Template error: %v", err) + return false + } + + return true +} diff --git a/internal/template/components/topnav.go b/internal/template/components/topnav.go new file mode 100644 index 0000000..a5128d3 --- /dev/null +++ b/internal/template/components/topnav.go @@ -0,0 +1,24 @@ +package components + +import ( + "dk/internal/auth" + "dk/internal/csrf" + "dk/internal/middleware" + "dk/internal/router" + "fmt" +) + +// GenerateTopNav generates the top navigation HTML based on authentication status +func GenerateTopNav(ctx router.Ctx) string { + if middleware.IsAuthenticated(ctx) { + return fmt.Sprintf(`
+ %s + +
+ Help`, csrf.HiddenField(ctx, auth.Manager)) + } else { + return `Log In + Register + Help` + } +} diff --git a/internal/template/template.go b/internal/template/template.go index 9c0dd2b..4fc0560 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -23,7 +23,7 @@ type TemplateCache struct { type RenderOptions struct { ResolveIncludes bool - Blocks map[string]string + Blocks map[string]string } type Template struct { @@ -261,7 +261,7 @@ func (t *Template) getStructField(obj any, fieldName string) any { if obj == nil { return nil } - + rv := reflect.ValueOf(obj) if rv.Kind() == reflect.Ptr { if rv.IsNil() { @@ -269,16 +269,16 @@ func (t *Template) getStructField(obj any, fieldName string) any { } rv = rv.Elem() } - + if rv.Kind() != reflect.Struct { return nil } - + field := rv.FieldByName(fieldName) if !field.IsValid() { return nil } - + return field.Interface() } @@ -314,29 +314,29 @@ func (t *Template) WriteToWithOptions(ctx *fasthttp.RequestCtx, data any, opts R // processIncludes handles {include "template.html"} directives func (t *Template) processIncludes(content string, data map[string]any, opts RenderOptions) string { result := content - + for { start := strings.Index(result, "{include ") if start == -1 { break } - + end := strings.Index(result[start:], "}") if end == -1 { break } end += start - - directive := result[start+9:end] // Skip "{include " + + directive := result[start+9 : end] // Skip "{include " templateName := strings.Trim(directive, "\" ") - + if includedTemplate, err := t.cache.Load(templateName); err == nil { var includedContent string if data != nil { // Create new options to pass blocks to included template includeOpts := RenderOptions{ ResolveIncludes: opts.ResolveIncludes, - Blocks: opts.Blocks, + Blocks: opts.Blocks, } includedContent = includedTemplate.RenderNamedWithOptions(includeOpts, data) } else { @@ -348,7 +348,7 @@ func (t *Template) processIncludes(content string, data map[string]any, opts Ren result = result[:start] + result[end+1:] } } - + return result } @@ -357,16 +357,16 @@ func (t *Template) processYield(content string, opts RenderOptions) string { if opts.Blocks == nil { return strings.ReplaceAll(content, "{yield}", "") } - + result := content for blockName, blockContent := range opts.Blocks { yieldPlaceholder := fmt.Sprintf("{yield %s}", blockName) result = strings.ReplaceAll(result, yieldPlaceholder, blockContent) } - + // Replace any remaining {yield} with empty string result = strings.ReplaceAll(result, "{yield}", "") - + return result } @@ -375,23 +375,23 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string { if opts.Blocks == nil { opts.Blocks = make(map[string]string) } - + result := content - + for { start := strings.Index(result, "{block ") if start == -1 { break } - + nameEnd := strings.Index(result[start:], "}") if nameEnd == -1 { break } nameEnd += start - + blockName := strings.Trim(result[start+7:nameEnd], "\" ") - + contentStart := nameEnd + 1 endTag := "{/block}" contentEnd := strings.Index(result[contentStart:], endTag) @@ -399,13 +399,38 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string { break } contentEnd += contentStart - + blockContent := result[contentStart:contentEnd] opts.Blocks[blockName] = blockContent - + // Remove the block definition from the template result = result[:start] + result[contentEnd+len(endTag):] } - + return result } + +// RenderToContext is a simplified helper that renders a template and writes it to the request context +// with error handling. Returns true if successful, false if an error occurred (error is written to response). +func RenderToContext(ctx *fasthttp.RequestCtx, templateName string, data map[string]any) bool { + tmpl, err := Cache.Load(templateName) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + fmt.Fprintf(ctx, "Template error: %v", err) + return false + } + + tmpl.WriteTo(ctx, data) + return true +} + +// RenderNamed is a simplified helper that loads and renders a template with the given data, +// returning the rendered content or an error. +func RenderNamed(templateName string, data map[string]any) (string, error) { + tmpl, err := Cache.Load(templateName) + if err != nil { + return "", fmt.Errorf("failed to load template %s: %w", templateName, err) + } + + return tmpl.RenderNamed(data), nil +}