massive layout fixes and improvements, rework asides, add template functionality
This commit is contained in:
parent
6968e8822c
commit
ae7b4a3066
@ -108,8 +108,10 @@ a {
|
||||
}
|
||||
|
||||
div.title {
|
||||
border: solid 1px black;
|
||||
background-color: #eeeeee;
|
||||
background-image: url("/assets/images/overlay.png");
|
||||
background-repeat: repeat;
|
||||
border: 1px solid #aaa;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
margin-bottom: 0.1rem;
|
||||
|
39
internal/helpers/ordered_map.go
Normal file
39
internal/helpers/ordered_map.go
Normal file
@ -0,0 +1,39 @@
|
||||
package helpers
|
||||
|
||||
type OrderedMap[K comparable, V any] struct {
|
||||
keys []K
|
||||
data map[K]V
|
||||
}
|
||||
|
||||
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
|
||||
return &OrderedMap[K, V]{
|
||||
keys: make([]K, 0),
|
||||
data: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
func (om *OrderedMap[K, V]) Set(key K, value V) {
|
||||
if _, exists := om.data[key]; !exists {
|
||||
om.keys = append(om.keys, key)
|
||||
}
|
||||
om.data[key] = value
|
||||
}
|
||||
|
||||
func (om *OrderedMap[K, V]) Range(fn func(K, V) bool) {
|
||||
for _, key := range om.keys {
|
||||
if !fn(key, om.data[key]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (om *OrderedMap[K, V]) ToSlice() []map[string]any {
|
||||
result := make([]map[string]any, 0, len(om.keys))
|
||||
for _, key := range om.keys {
|
||||
result = append(result, map[string]any{
|
||||
"id": key,
|
||||
"name": om.data[key],
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
@ -48,9 +48,7 @@ func showLogin(ctx router.Ctx, _ []string) {
|
||||
id = formData["id"]
|
||||
}
|
||||
|
||||
components.RenderPageTemplate(ctx, "Log In", "auth/login.html", map[string]any{
|
||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||
components.RenderPage(ctx, "Log In", "auth/login.html", map[string]any{
|
||||
"error_message": errorHTML,
|
||||
"id": id,
|
||||
})
|
||||
@ -111,9 +109,7 @@ func showRegister(ctx router.Ctx, _ []string) {
|
||||
email = formData["email"]
|
||||
}
|
||||
|
||||
components.RenderPageTemplate(ctx, "Register", "auth/register.html", map[string]any{
|
||||
"csrf_token": csrf.GetToken(ctx, auth.Manager),
|
||||
"csrf_field": csrf.HiddenField(ctx, auth.Manager),
|
||||
components.RenderPage(ctx, "Register", "auth/register.html", map[string]any{
|
||||
"error_message": errorHTML,
|
||||
"username": username,
|
||||
"email": email,
|
||||
|
@ -4,34 +4,15 @@ import (
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/router"
|
||||
"dk/internal/template/components"
|
||||
"dk/internal/users"
|
||||
"fmt"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Index(ctx router.Ctx, _ []string) {
|
||||
currentUser := middleware.GetCurrentUser(ctx)
|
||||
var username string
|
||||
if currentUser != nil {
|
||||
username = currentUser.Username
|
||||
user, _ := users.Find(currentUser.ID)
|
||||
|
||||
user := middleware.GetCurrentUser(ctx)
|
||||
if user != nil {
|
||||
if user.Currently == "In Town" {
|
||||
ctx.Redirect("/town", 303)
|
||||
}
|
||||
} else {
|
||||
username = "Guest"
|
||||
}
|
||||
|
||||
pageData := components.NewPageData(
|
||||
"Dragon Knight",
|
||||
fmt.Sprintf("Hello %s!", username),
|
||||
)
|
||||
|
||||
if err := components.RenderPage(ctx, pageData, nil); err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
fmt.Fprintf(ctx, "Template error: %v", err)
|
||||
return
|
||||
}
|
||||
components.RenderPage(ctx, "", "intro.html", nil)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func RegisterTownRoutes(r *router.Router) {
|
||||
|
||||
func showTown(ctx router.Ctx, _ []string) {
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
components.RenderPageTemplate(ctx, town.Name, "town/town.html", map[string]any{
|
||||
components.RenderPage(ctx, town.Name, "town/town.html", map[string]any{
|
||||
"town": town,
|
||||
"newscontent": components.GenerateTownNews(),
|
||||
"whosonline": components.GenerateTownWhosOnline(),
|
||||
|
@ -5,148 +5,65 @@ import (
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/router"
|
||||
"dk/internal/spells"
|
||||
"dk/internal/template"
|
||||
"dk/internal/towns"
|
||||
)
|
||||
|
||||
// LeftAside generates the left sidebar content
|
||||
func LeftAside(ctx router.Ctx) string {
|
||||
// LeftAside generates the data map for the left sidebar.
|
||||
// Returns an empty map when not auth'd.
|
||||
func LeftAside(ctx router.Ctx) map[string]any {
|
||||
data := map[string]any{}
|
||||
|
||||
user := middleware.GetCurrentUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
return data
|
||||
}
|
||||
|
||||
tmpl, err := template.Cache.Load("leftside.html")
|
||||
if err != nil {
|
||||
return "leftside failed to load?"
|
||||
}
|
||||
|
||||
cardinalX := "E"
|
||||
if user.X < 0 {
|
||||
cardinalX = "W"
|
||||
}
|
||||
|
||||
cardinalY := "S"
|
||||
if user.Y < 0 {
|
||||
cardinalY = "N"
|
||||
}
|
||||
|
||||
townname := ""
|
||||
if user.Currently == "In Town" {
|
||||
town, err := towns.ByCoords(user.X, user.Y)
|
||||
if err != nil {
|
||||
townname = "error finding town"
|
||||
}
|
||||
townname = "<div class=\"mb-05\"><b>In " + town.Name + "</b></div>"
|
||||
}
|
||||
|
||||
townlist := ""
|
||||
townIDs := user.GetTownIDs()
|
||||
if len(townIDs) > 0 {
|
||||
townlist = "<i>Teleport to:</i><br>"
|
||||
for _, id := range townIDs {
|
||||
town, err := towns.Find(id)
|
||||
if err != nil {
|
||||
continue
|
||||
// Build owned town maps list
|
||||
if user.Towns != "" {
|
||||
townMap := helpers.NewOrderedMap[int, string]()
|
||||
for _, id := range user.GetTownIDs() {
|
||||
if town, err := towns.Find(id); err == nil {
|
||||
townMap.Set(id, town.Name)
|
||||
}
|
||||
|
||||
townlist += `<a href="#">` + town.Name + "</a><br>"
|
||||
}
|
||||
} else {
|
||||
townlist = "<i>No town maps</i>"
|
||||
data["_towns"] = townMap.ToSlice()
|
||||
}
|
||||
|
||||
return tmpl.RenderNamed(map[string]any{
|
||||
"user": user.ToMap(),
|
||||
"cardinalX": cardinalX,
|
||||
"cardinalY": cardinalY,
|
||||
"townname": townname,
|
||||
"townlist": townlist,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// RightAside generates the right sidebar content
|
||||
func RightAside(ctx router.Ctx) string {
|
||||
// RightAside generates the data map for the right sidebar.
|
||||
// Returns an empty map when not auth'd.
|
||||
func RightAside(ctx router.Ctx) map[string]any {
|
||||
data := map[string]any{}
|
||||
|
||||
user := middleware.GetCurrentUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
tmpl, err := template.Cache.Load("rightside.html")
|
||||
if err != nil {
|
||||
return ""
|
||||
return data
|
||||
}
|
||||
|
||||
hpPct := helpers.ClampPct(float64(user.HP), float64(user.MaxHP), 0, 100)
|
||||
mpPct := helpers.ClampPct(float64(user.MP), float64(user.MaxMP), 0, 100)
|
||||
tpPct := helpers.ClampPct(float64(user.TP), float64(user.MaxTP), 0, 100)
|
||||
data["hpPct"] = hpPct
|
||||
data["mpPct"] = helpers.ClampPct(float64(user.MP), float64(user.MaxMP), 0, 100)
|
||||
data["tpPct"] = helpers.ClampPct(float64(user.TP), float64(user.MaxTP), 0, 100)
|
||||
|
||||
hpColor := ""
|
||||
data["hpColor"] = ""
|
||||
if hpPct < 35 {
|
||||
hpColor = "danger"
|
||||
data["hpColor"] = "danger"
|
||||
} else if hpPct < 75 {
|
||||
hpColor = "warning"
|
||||
data["hpColor"] = "warning"
|
||||
}
|
||||
|
||||
weaponName := "<i>No weapon</i>"
|
||||
if user.WeaponName != "" {
|
||||
weaponName = user.WeaponName
|
||||
}
|
||||
|
||||
armorName := "<i>No armor</i>"
|
||||
if user.ArmorName != "" {
|
||||
armorName = user.ArmorName
|
||||
}
|
||||
|
||||
shieldName := "<i>No shield</i>"
|
||||
if user.ShieldName != "" {
|
||||
shieldName = user.ShieldName
|
||||
}
|
||||
|
||||
slot1Name := ""
|
||||
if user.Slot1Name != "" {
|
||||
slot1Name = user.Slot1Name
|
||||
}
|
||||
|
||||
slot2Name := ""
|
||||
if user.Slot2Name != "" {
|
||||
slot2Name = user.Slot2Name
|
||||
}
|
||||
|
||||
slot3Name := ""
|
||||
if user.Slot3Name != "" {
|
||||
slot3Name = user.Slot3Name
|
||||
}
|
||||
|
||||
magicList := ""
|
||||
list := user.GetSpellIDs()
|
||||
if len(list) > 0 {
|
||||
for i := range list {
|
||||
spell, err := spells.Find(i)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if spell.IsHealing() {
|
||||
magicList += `<a href="#">` + spell.Name + "</a>"
|
||||
// Build known healing spells list
|
||||
if user.Spells != "" {
|
||||
spellMap := helpers.NewOrderedMap[int, string]()
|
||||
for _, id := range user.GetSpellIDs() {
|
||||
if spell, err := spells.Find(id); err == nil {
|
||||
spellMap.Set(id, spell.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
magicList = "<i>No healing spells known</i>"
|
||||
data["_spells"] = spellMap.ToSlice()
|
||||
}
|
||||
|
||||
return tmpl.RenderNamed(map[string]any{
|
||||
"user": user.ToMap(),
|
||||
"hppct": hpPct,
|
||||
"hpcolor": hpColor,
|
||||
"mppct": mpPct,
|
||||
"tppct": tpPct,
|
||||
"weaponname": weaponName,
|
||||
"armorname": armorName,
|
||||
"shieldname": shieldName,
|
||||
"slot1name": slot1Name,
|
||||
"slot2name": slot2Name,
|
||||
"slot3name": slot3Name,
|
||||
"magiclist": magicList,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
@ -6,87 +6,42 @@ import (
|
||||
"strings"
|
||||
|
||||
"dk/internal/auth"
|
||||
"dk/internal/csrf"
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/router"
|
||||
"dk/internal/template"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// PageData holds common page template data
|
||||
type PageData struct {
|
||||
Title string
|
||||
Content string
|
||||
TopNav string
|
||||
LeftSide string
|
||||
RightSide string
|
||||
TotalTime string
|
||||
NumQueries string
|
||||
Version string
|
||||
Build string
|
||||
}
|
||||
|
||||
// RenderPage renders a page using the layout template with common data and additional custom data
|
||||
func RenderPage(ctx router.Ctx, pageData PageData, additionalData map[string]any) error {
|
||||
func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[string]any) error {
|
||||
if template.Cache == nil || auth.Manager == nil {
|
||||
return fmt.Errorf("template.Cache or auth.Manager not initialized")
|
||||
}
|
||||
|
||||
layoutTmpl, err := template.Cache.Load("layout.html")
|
||||
tmpl, err := template.Cache.Load(tmplPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load layout template: %w", err)
|
||||
}
|
||||
|
||||
// Build the base template data with common fields
|
||||
data := map[string]any{
|
||||
"title": pageData.Title,
|
||||
"content": pageData.Content,
|
||||
"topnav": GenerateTopNav(ctx),
|
||||
"leftside": pageData.LeftSide,
|
||||
"rightside": pageData.RightSide,
|
||||
"totaltime": middleware.GetRequestTime(ctx),
|
||||
"numqueries": pageData.NumQueries,
|
||||
"version": pageData.Version,
|
||||
"build": pageData.Build,
|
||||
"_title": PageTitle(title),
|
||||
"authenticated": middleware.IsAuthenticated(ctx),
|
||||
"csrf": csrf.HiddenField(ctx, auth.Manager),
|
||||
"_totaltime": middleware.GetRequestTime(ctx),
|
||||
"_numqueries": 0,
|
||||
"_version": "1.0.0",
|
||||
"_build": "dev",
|
||||
"user": middleware.GetCurrentUser(ctx),
|
||||
}
|
||||
|
||||
// Merge in additional data (overwrites common data if keys conflict)
|
||||
maps.Copy(data, LeftAside(ctx))
|
||||
maps.Copy(data, RightAside(ctx))
|
||||
maps.Copy(data, additionalData)
|
||||
|
||||
// Set defaults for empty fields
|
||||
if data["leftside"] == "" {
|
||||
data["leftside"] = LeftAside(ctx)
|
||||
}
|
||||
if data["rightside"] == "" {
|
||||
data["rightside"] = RightAside(ctx)
|
||||
}
|
||||
if data["numqueries"] == "" {
|
||||
data["numqueries"] = "0"
|
||||
}
|
||||
if data["version"] == "" {
|
||||
data["version"] = "1.0.0"
|
||||
}
|
||||
if data["build"] == "" {
|
||||
data["build"] = "dev"
|
||||
}
|
||||
|
||||
layoutTmpl.WriteTo(ctx, data)
|
||||
tmpl.WriteTo(ctx, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPageData creates a new PageData with sensible defaults
|
||||
func NewPageData(title, content string) PageData {
|
||||
return PageData{
|
||||
Title: title,
|
||||
Content: content,
|
||||
LeftSide: "",
|
||||
RightSide: "",
|
||||
NumQueries: "0",
|
||||
Version: "1.0.0",
|
||||
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.
|
||||
@ -101,24 +56,3 @@ func PageTitle(title string) string {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,75 +0,0 @@
|
||||
// Package template provides a high-performance template engine with in-memory
|
||||
// caching, automatic reloading, and advanced template composition features.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// cache := template.NewCache("") // Auto-detects binary location
|
||||
// tmpl, err := cache.Load("page.html")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// data := map[string]any{
|
||||
// "title": "Welcome",
|
||||
// "user": map[string]any{
|
||||
// "name": "Alice",
|
||||
// "email": "alice@example.com",
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// result := tmpl.RenderNamed(data)
|
||||
//
|
||||
// # Placeholder Types
|
||||
//
|
||||
// Named placeholders: {name}, {title}
|
||||
//
|
||||
// Dot notation: {user.name}, {user.contact.email}
|
||||
//
|
||||
// Positional: {0}, {1}, {2}
|
||||
//
|
||||
// # Template Composition
|
||||
//
|
||||
// Includes - embed other templates with data sharing:
|
||||
//
|
||||
// {include "header.html"}
|
||||
// {include "nav.html"}
|
||||
//
|
||||
// Blocks - define reusable content sections:
|
||||
//
|
||||
// {block "content"}
|
||||
// <h1>Default content</h1>
|
||||
// {/block}
|
||||
//
|
||||
// Yield - template inheritance points:
|
||||
//
|
||||
// <main>{yield content}</main>
|
||||
// <footer>{yield footer}</footer>
|
||||
//
|
||||
// # Template Inheritance Example
|
||||
//
|
||||
// layout.html:
|
||||
//
|
||||
// <!DOCTYPE html>
|
||||
// <html>
|
||||
// <head><title>{title}</title></head>
|
||||
// <body>{yield content}</body>
|
||||
// </html>
|
||||
//
|
||||
// page.html:
|
||||
//
|
||||
// {include "layout.html"}
|
||||
// {block "content"}
|
||||
// <h1>Welcome {user.name}!</h1>
|
||||
// {/block}
|
||||
//
|
||||
// # Advanced Features
|
||||
//
|
||||
// Disable includes for partial rendering:
|
||||
//
|
||||
// opts := RenderOptions{ResolveIncludes: false}
|
||||
// chunk := tmpl.RenderNamedWithOptions(opts, data)
|
||||
//
|
||||
// FastHTTP integration:
|
||||
//
|
||||
// tmpl.WriteTo(ctx, data) // Sets content-type and writes response
|
||||
package template
|
@ -2,9 +2,11 @@ package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -21,11 +23,6 @@ type TemplateCache struct {
|
||||
basePath string
|
||||
}
|
||||
|
||||
type RenderOptions struct {
|
||||
ResolveIncludes bool
|
||||
Blocks map[string]string
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
name string
|
||||
content string
|
||||
@ -120,37 +117,33 @@ func (c *TemplateCache) checkAndReload(tmpl *Template) error {
|
||||
}
|
||||
|
||||
func (t *Template) RenderPositional(args ...any) string {
|
||||
return t.RenderPositionalWithOptions(RenderOptions{ResolveIncludes: true}, args...)
|
||||
}
|
||||
|
||||
func (t *Template) RenderPositionalWithOptions(opts RenderOptions, args ...any) string {
|
||||
result := t.content
|
||||
for i, arg := range args {
|
||||
placeholder := fmt.Sprintf("{%d}", i)
|
||||
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", arg))
|
||||
}
|
||||
if opts.ResolveIncludes {
|
||||
result = t.processIncludes(result, nil, opts)
|
||||
}
|
||||
result = t.processIncludes(result, nil)
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *Template) RenderNamed(data map[string]any) string {
|
||||
return t.RenderNamedWithOptions(RenderOptions{ResolveIncludes: true}, data)
|
||||
}
|
||||
|
||||
func (t *Template) RenderNamedWithOptions(opts RenderOptions, data map[string]any) string {
|
||||
result := t.content
|
||||
|
||||
// Process blocks first to extract them
|
||||
result = t.processBlocks(result, &opts)
|
||||
blocks := make(map[string]string)
|
||||
result = t.processBlocks(result, blocks)
|
||||
|
||||
// Process includes next so they get the data substitutions
|
||||
if opts.ResolveIncludes {
|
||||
result = t.processIncludes(result, data, opts)
|
||||
}
|
||||
// Process includes
|
||||
result = t.processIncludes(result, data)
|
||||
|
||||
// Apply data substitutions after includes are processed
|
||||
// Process loops and conditionals
|
||||
result = t.processLoops(result, data)
|
||||
result = t.processConditionals(result, data)
|
||||
|
||||
// Process yield before variable substitution
|
||||
result = t.processYield(result, blocks)
|
||||
|
||||
// Apply data substitutions
|
||||
for key, value := range data {
|
||||
placeholder := fmt.Sprintf("{%s}", key)
|
||||
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
|
||||
@ -158,8 +151,6 @@ func (t *Template) RenderNamedWithOptions(opts RenderOptions, data map[string]an
|
||||
|
||||
result = t.replaceDotNotation(result, data)
|
||||
|
||||
result = t.processYield(result, opts)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -283,17 +274,13 @@ func (t *Template) getStructField(obj any, fieldName string) any {
|
||||
}
|
||||
|
||||
func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) {
|
||||
t.WriteToWithOptions(ctx, data, RenderOptions{ResolveIncludes: true})
|
||||
}
|
||||
|
||||
func (t *Template) WriteToWithOptions(ctx *fasthttp.RequestCtx, data any, opts RenderOptions) {
|
||||
var result string
|
||||
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
result = t.RenderNamedWithOptions(opts, v)
|
||||
result = t.RenderNamed(v)
|
||||
case []any:
|
||||
result = t.RenderPositionalWithOptions(opts, v...)
|
||||
result = t.RenderPositional(v...)
|
||||
default:
|
||||
rv := reflect.ValueOf(data)
|
||||
if rv.Kind() == reflect.Slice {
|
||||
@ -301,9 +288,9 @@ func (t *Template) WriteToWithOptions(ctx *fasthttp.RequestCtx, data any, opts R
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
args[i] = rv.Index(i).Interface()
|
||||
}
|
||||
result = t.RenderPositionalWithOptions(opts, args...)
|
||||
result = t.RenderPositional(args...)
|
||||
} else {
|
||||
result = t.RenderPositionalWithOptions(opts, data)
|
||||
result = t.RenderPositional(data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,7 +299,7 @@ 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 {
|
||||
func (t *Template) processIncludes(content string, data map[string]any) string {
|
||||
result := content
|
||||
|
||||
for {
|
||||
@ -333,12 +320,7 @@ func (t *Template) processIncludes(content string, data map[string]any, opts Ren
|
||||
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,
|
||||
}
|
||||
includedContent = includedTemplate.RenderNamedWithOptions(includeOpts, data)
|
||||
includedContent = includedTemplate.RenderNamed(data)
|
||||
} else {
|
||||
includedContent = includedTemplate.content
|
||||
}
|
||||
@ -352,30 +334,8 @@ func (t *Template) processIncludes(content string, data map[string]any, opts Ren
|
||||
return result
|
||||
}
|
||||
|
||||
// processYield handles {yield} directives for template inheritance
|
||||
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
|
||||
}
|
||||
|
||||
// processBlocks extracts {block "name"}...{/block} sections
|
||||
func (t *Template) processBlocks(content string, opts *RenderOptions) string {
|
||||
if opts.Blocks == nil {
|
||||
opts.Blocks = make(map[string]string)
|
||||
}
|
||||
|
||||
func (t *Template) processBlocks(content string, blocks map[string]string) string {
|
||||
result := content
|
||||
|
||||
for {
|
||||
@ -401,7 +361,7 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
|
||||
contentEnd += contentStart
|
||||
|
||||
blockContent := result[contentStart:contentEnd]
|
||||
opts.Blocks[blockName] = blockContent
|
||||
blocks[blockName] = blockContent
|
||||
|
||||
// Remove the block definition from the template
|
||||
result = result[:start] + result[contentEnd+len(endTag):]
|
||||
@ -410,6 +370,364 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
|
||||
return result
|
||||
}
|
||||
|
||||
// processYield handles {yield} directives for template inheritance
|
||||
func (t *Template) processYield(content string, blocks map[string]string) string {
|
||||
result := content
|
||||
|
||||
for blockName, blockContent := range 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
|
||||
}
|
||||
|
||||
// processLoops handles {for item in items}...{/for} and {for key,value in map}...{/for}
|
||||
func (t *Template) processLoops(content string, data map[string]any) string {
|
||||
result := content
|
||||
|
||||
for {
|
||||
start := strings.Index(result, "{for ")
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
headerEnd := strings.Index(result[start:], "}")
|
||||
if headerEnd == -1 {
|
||||
break
|
||||
}
|
||||
headerEnd += start
|
||||
|
||||
header := result[start+5 : headerEnd] // Skip "{for "
|
||||
|
||||
contentStart := headerEnd + 1
|
||||
endTag := "{/for}"
|
||||
contentEnd := strings.Index(result[contentStart:], endTag)
|
||||
if contentEnd == -1 {
|
||||
break
|
||||
}
|
||||
contentEnd += contentStart
|
||||
|
||||
loopContent := result[contentStart:contentEnd]
|
||||
expanded := t.expandLoop(header, loopContent, data)
|
||||
|
||||
result = result[:start] + expanded + result[contentEnd+len(endTag):]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// expandLoop processes a single loop construct
|
||||
func (t *Template) expandLoop(header, content string, data map[string]any) string {
|
||||
parts := strings.Split(strings.TrimSpace(header), " in ")
|
||||
if len(parts) != 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
varPart := strings.TrimSpace(parts[0])
|
||||
sourcePart := strings.TrimSpace(parts[1])
|
||||
|
||||
source := t.getNestedValue(data, sourcePart)
|
||||
if source == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
// Handle key,value pairs
|
||||
if strings.Contains(varPart, ",") {
|
||||
keyVar, valueVar := strings.TrimSpace(varPart[:strings.Index(varPart, ",")]), strings.TrimSpace(varPart[strings.Index(varPart, ",")+1:])
|
||||
|
||||
rv := reflect.ValueOf(source)
|
||||
switch rv.Kind() {
|
||||
case reflect.Map:
|
||||
for _, key := range rv.MapKeys() {
|
||||
iterData := make(map[string]any)
|
||||
maps.Copy(iterData, data)
|
||||
iterData[keyVar] = key.Interface()
|
||||
iterData[valueVar] = rv.MapIndex(key).Interface()
|
||||
|
||||
iterResult := content
|
||||
iterResult = t.processLoops(iterResult, iterData)
|
||||
iterResult = t.processConditionals(iterResult, iterData)
|
||||
for k, v := range iterData {
|
||||
placeholder := fmt.Sprintf("{%s}", k)
|
||||
iterResult = strings.ReplaceAll(iterResult, placeholder, fmt.Sprintf("%v", v))
|
||||
}
|
||||
iterResult = t.replaceDotNotation(iterResult, iterData)
|
||||
result.WriteString(iterResult)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
iterData := make(map[string]any)
|
||||
maps.Copy(iterData, data)
|
||||
iterData[keyVar] = i
|
||||
iterData[valueVar] = rv.Index(i).Interface()
|
||||
|
||||
iterResult := content
|
||||
iterResult = t.processLoops(iterResult, iterData)
|
||||
iterResult = t.processConditionals(iterResult, iterData)
|
||||
for k, v := range iterData {
|
||||
placeholder := fmt.Sprintf("{%s}", k)
|
||||
iterResult = strings.ReplaceAll(iterResult, placeholder, fmt.Sprintf("%v", v))
|
||||
}
|
||||
iterResult = t.replaceDotNotation(iterResult, iterData)
|
||||
result.WriteString(iterResult)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single variable iteration
|
||||
rv := reflect.ValueOf(source)
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
iterData := make(map[string]any)
|
||||
maps.Copy(iterData, data)
|
||||
iterData[varPart] = rv.Index(i).Interface()
|
||||
|
||||
iterResult := content
|
||||
iterResult = t.processLoops(iterResult, iterData)
|
||||
iterResult = t.processConditionals(iterResult, iterData)
|
||||
for k, v := range iterData {
|
||||
placeholder := fmt.Sprintf("{%s}", k)
|
||||
iterResult = strings.ReplaceAll(iterResult, placeholder, fmt.Sprintf("%v", v))
|
||||
}
|
||||
iterResult = t.replaceDotNotation(iterResult, iterData)
|
||||
result.WriteString(iterResult)
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, key := range rv.MapKeys() {
|
||||
iterData := make(map[string]any)
|
||||
maps.Copy(iterData, data)
|
||||
iterData[varPart] = rv.MapIndex(key).Interface()
|
||||
|
||||
iterResult := content
|
||||
iterResult = t.processLoops(iterResult, iterData)
|
||||
iterResult = t.processConditionals(iterResult, iterData)
|
||||
for k, v := range iterData {
|
||||
placeholder := fmt.Sprintf("{%s}", k)
|
||||
iterResult = strings.ReplaceAll(iterResult, placeholder, fmt.Sprintf("%v", v))
|
||||
}
|
||||
iterResult = t.replaceDotNotation(iterResult, iterData)
|
||||
result.WriteString(iterResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// processConditionals handles {if condition}...{/if} and {if condition}...{else}...{/if}
|
||||
func (t *Template) processConditionals(content string, data map[string]any) string {
|
||||
result := content
|
||||
|
||||
for {
|
||||
start := strings.Index(result, "{if ")
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
headerEnd := strings.Index(result[start:], "}")
|
||||
if headerEnd == -1 {
|
||||
break
|
||||
}
|
||||
headerEnd += start
|
||||
|
||||
condition := strings.TrimSpace(result[start+4 : headerEnd]) // Skip "{if "
|
||||
|
||||
contentStart := headerEnd + 1
|
||||
endTag := "{/if}"
|
||||
contentEnd := strings.Index(result[contentStart:], endTag)
|
||||
if contentEnd == -1 {
|
||||
break
|
||||
}
|
||||
contentEnd += contentStart
|
||||
|
||||
ifContent := result[contentStart:contentEnd]
|
||||
|
||||
// Check for else clause
|
||||
elseStart := strings.Index(ifContent, "{else}")
|
||||
var trueContent, falseContent string
|
||||
if elseStart != -1 {
|
||||
trueContent = ifContent[:elseStart]
|
||||
falseContent = ifContent[elseStart+6:] // Skip "{else}"
|
||||
} else {
|
||||
trueContent = ifContent
|
||||
falseContent = ""
|
||||
}
|
||||
|
||||
var selectedContent string
|
||||
if t.evaluateCondition(condition, data) {
|
||||
selectedContent = trueContent
|
||||
} else {
|
||||
selectedContent = falseContent
|
||||
}
|
||||
|
||||
// Recursively process the selected content
|
||||
selectedContent = t.processLoops(selectedContent, data)
|
||||
selectedContent = t.processConditionals(selectedContent, data)
|
||||
|
||||
result = result[:start] + selectedContent + result[contentEnd+len(endTag):]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evaluateCondition evaluates simple conditions like "user.name", "count > 0", "items"
|
||||
func (t *Template) evaluateCondition(condition string, data map[string]any) bool {
|
||||
condition = strings.TrimSpace(condition)
|
||||
|
||||
// Handle 'and' operator
|
||||
if strings.Contains(condition, " and ") {
|
||||
parts := strings.SplitSeq(condition, " and ")
|
||||
for part := range parts {
|
||||
if !t.evaluateCondition(strings.TrimSpace(part), data) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle comparison operators
|
||||
for _, op := range []string{">=", "<=", "!=", "==", ">", "<"} {
|
||||
if strings.Contains(condition, op) {
|
||||
parts := strings.Split(condition, op)
|
||||
if len(parts) == 2 {
|
||||
left := strings.TrimSpace(parts[0])
|
||||
right := strings.TrimSpace(parts[1])
|
||||
return t.compareValues(t.getConditionValue(left, data), t.getConditionValue(right, data), op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple existence check
|
||||
value := t.getConditionValue(condition, data)
|
||||
return t.isTruthy(value)
|
||||
}
|
||||
|
||||
// getConditionValue gets a value for condition evaluation
|
||||
func (t *Template) getConditionValue(expr string, data map[string]any) any {
|
||||
expr = strings.TrimSpace(expr)
|
||||
|
||||
// Handle length operator
|
||||
if strings.HasPrefix(expr, "#") {
|
||||
varName := expr[1:] // Remove the #
|
||||
value := t.getNestedValue(data, varName)
|
||||
return t.getLength(value)
|
||||
}
|
||||
|
||||
// Try to parse as number
|
||||
if num, err := strconv.ParseFloat(expr, 64); err == nil {
|
||||
return num
|
||||
}
|
||||
|
||||
// Try to parse as string literal
|
||||
if strings.HasPrefix(expr, "\"") && strings.HasSuffix(expr, "\"") {
|
||||
return expr[1 : len(expr)-1]
|
||||
}
|
||||
|
||||
// Try as variable reference
|
||||
if strings.Contains(expr, ".") {
|
||||
return t.getNestedValue(data, expr)
|
||||
}
|
||||
|
||||
if value, ok := data[expr]; ok {
|
||||
return value
|
||||
}
|
||||
|
||||
return expr
|
||||
}
|
||||
|
||||
// compareValues compares two values with the given operator
|
||||
func (t *Template) compareValues(left, right any, op string) bool {
|
||||
switch op {
|
||||
case "==":
|
||||
return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
|
||||
case "!=":
|
||||
return fmt.Sprintf("%v", left) != fmt.Sprintf("%v", right)
|
||||
case ">", ">=", "<", "<=":
|
||||
leftNum, leftOk := t.toFloat(left)
|
||||
rightNum, rightOk := t.toFloat(right)
|
||||
if !leftOk || !rightOk {
|
||||
return false
|
||||
}
|
||||
switch op {
|
||||
case ">":
|
||||
return leftNum > rightNum
|
||||
case ">=":
|
||||
return leftNum >= rightNum
|
||||
case "<":
|
||||
return leftNum < rightNum
|
||||
case "<=":
|
||||
return leftNum <= rightNum
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// toFloat converts a value to float64 if possible
|
||||
func (t *Template) toFloat(value any) (float64, bool) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return float64(v), true
|
||||
case int64:
|
||||
return float64(v), true
|
||||
case float32:
|
||||
return float64(v), true
|
||||
case float64:
|
||||
return v, true
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// isTruthy determines if a value is truthy
|
||||
func (t *Template) isTruthy(value any) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case int:
|
||||
return v != 0
|
||||
case float64:
|
||||
return v != 0
|
||||
case string:
|
||||
return v != ""
|
||||
default:
|
||||
rv := reflect.ValueOf(value)
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
return rv.Len() > 0
|
||||
case reflect.Ptr:
|
||||
return !rv.IsNil()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Template) getLength(value any) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(value)
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice, reflect.Array, reflect.Map, reflect.String:
|
||||
return rv.Len()
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
@ -253,40 +253,6 @@ func TestIncludeSupport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeDisabled(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "template_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
templatesDir := filepath.Join(tmpDir, "templates")
|
||||
err = os.MkdirAll(templatesDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mainTemplate := `<div>{include "partial.html"}</div>`
|
||||
err = os.WriteFile(filepath.Join(templatesDir, "main.html"), []byte(mainTemplate), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cache := NewCache(tmpDir)
|
||||
tmpl, err := cache.Load("main.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opts := RenderOptions{ResolveIncludes: false}
|
||||
result := tmpl.RenderNamedWithOptions(opts, map[string]any{})
|
||||
expected := `<div>{include "partial.html"}</div>`
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlocksAndYield(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
name: "test",
|
||||
|
@ -68,7 +68,6 @@ type User struct {
|
||||
Towns string `db:"towns" json:"towns"`
|
||||
}
|
||||
|
||||
// Implement Model interface
|
||||
func (u *User) GetTableName() string {
|
||||
return "users"
|
||||
}
|
||||
@ -81,7 +80,6 @@ func (u *User) SetID(id int) {
|
||||
u.ID = id
|
||||
}
|
||||
|
||||
// Convenience methods wrapping generic functions
|
||||
func (u *User) Set(field string, value any) error {
|
||||
return database.Set(u, field, value)
|
||||
}
|
||||
@ -94,7 +92,6 @@ func (u *User) Delete() error {
|
||||
return database.Delete(u)
|
||||
}
|
||||
|
||||
// New creates a new User with sensible defaults
|
||||
func New() *User {
|
||||
now := time.Now().Unix()
|
||||
return &User{
|
||||
@ -138,7 +135,6 @@ func scanUser(stmt *sqlite.Stmt) *User {
|
||||
return user
|
||||
}
|
||||
|
||||
// Query functions
|
||||
func Find(id int) (*User, error) {
|
||||
var user *User
|
||||
query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?`
|
||||
@ -250,7 +246,6 @@ func (u *User) Insert() error {
|
||||
return database.Insert(u, columns, values...)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (u *User) RegisteredTime() time.Time {
|
||||
return time.Unix(u.Registered, 0)
|
||||
}
|
||||
@ -345,7 +340,6 @@ func (u *User) SetPosition(x, y int) {
|
||||
u.Set("Y", y)
|
||||
}
|
||||
|
||||
// ToMap converts the user to a map for efficient template rendering
|
||||
func (u *User) ToMap() map[string]any {
|
||||
return map[string]any{
|
||||
"ID": u.ID,
|
||||
|
@ -1,9 +1,12 @@
|
||||
{include "layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Log In</h1>
|
||||
|
||||
{error_message}
|
||||
|
||||
<form class="standard mb-1" action="/login" method="post">
|
||||
{csrf_field}
|
||||
{csrf}
|
||||
|
||||
<div class="row">
|
||||
<label for="id">Email/Username</label>
|
||||
@ -27,4 +30,5 @@
|
||||
<p>
|
||||
You may also <a href="/change-password">change your password</a>, or
|
||||
<a href="/lost-password">request a new one</a> if you've lost yours.
|
||||
</p>
|
||||
</p>
|
||||
{/block}
|
@ -1,9 +1,12 @@
|
||||
{include "layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Register</h1>
|
||||
|
||||
{error_message}
|
||||
|
||||
<form class="standard" action="/register" method="post">
|
||||
{csrf_field}
|
||||
{csrf}
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
@ -43,4 +46,4 @@
|
||||
<button class="btn" type="reset" name="reset">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/block}
|
||||
|
5
templates/intro.html
Normal file
5
templates/intro.html
Normal file
@ -0,0 +1,5 @@
|
||||
{include "layout.html"}
|
||||
|
||||
{block "content"}
|
||||
Hey there, Guest!
|
||||
{/block}
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<title>{_title}</title>
|
||||
|
||||
<link rel="stylesheet" href="/assets/reset.css">
|
||||
<link rel="stylesheet" href="/assets/dk.css">
|
||||
@ -24,20 +24,28 @@
|
||||
<div id="container">
|
||||
<header>
|
||||
<div><img src="/assets/images/logo.gif" alt="Dragon Knight" title="Dragon Knight"></div>
|
||||
<nav>{topnav}</nav>
|
||||
<nav>{include "topnav.html"}</nav>
|
||||
</header>
|
||||
|
||||
<section id="game">
|
||||
<aside id="left">{leftside}</aside>
|
||||
<main>{content}</main>
|
||||
<aside id="right">{rightside}</aside>
|
||||
<aside id="left">
|
||||
{if authenticated}
|
||||
{include "leftside.html"}
|
||||
{/if}
|
||||
</aside>
|
||||
<main>{yield "content"}</main>
|
||||
<aside id="right">
|
||||
{if authenticated}
|
||||
{include "rightside.html"}
|
||||
{/if}
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div>Powered by <a href="/">Dragon Knight</a></div>
|
||||
<div>© 2025 Sharkk</div>
|
||||
<div>{totaltime} Seconds, {numqueries} Queries</div>
|
||||
<div>Version {version} {build}</div>
|
||||
<div>{_totaltime} Seconds, {_numqueries} Queries</div>
|
||||
<div>Version {_version} {_build}</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -1,10 +1,16 @@
|
||||
<section>
|
||||
<div class="title"><img src="/assets/images/button_location.gif" alt="Location" title="Location"></div>
|
||||
|
||||
<div><b>{user.Currently}</b></div>
|
||||
<div>{user.X}{cardinalX}, {user.Y}{cardinalY}</div>
|
||||
<div><b>
|
||||
{if user.Currently == "In Town" and town != nil}
|
||||
In {town.Name}
|
||||
{else}
|
||||
{user.Currently}
|
||||
{/if}
|
||||
</b></div>
|
||||
<div>{user.X}{if user.X < 0}W{else}E{/if}, {user.Y}{if user.Y < 0}N{else}S{/if}</div>
|
||||
|
||||
<a href="javascript:openmappopup()">View Map</a>
|
||||
<a href="javascript:open_map_popup()">View Map</a>
|
||||
|
||||
<form id="move-compass" action="/move" method="post" >
|
||||
<button id="north" name="direction" value="north">North</button>
|
||||
@ -18,8 +24,14 @@
|
||||
|
||||
<section>
|
||||
<div class="title"><img src="/assets/images/button_towns.gif" alt="Towns" title="Towns"></div>
|
||||
<div>{townname}</div>
|
||||
<div>{townlist}</div>
|
||||
{if #_towns > 0}
|
||||
<i>Teleport to:</i>
|
||||
{for map in _towns}
|
||||
<a href="#">{map.name}</a>
|
||||
{/for}
|
||||
{else}
|
||||
<i>No town maps</i>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section id="functions">
|
||||
|
@ -13,17 +13,17 @@
|
||||
<div id="statbars">
|
||||
<div class="stat">
|
||||
<div id="hp" class="container {hpcolor}"><div class="bar" style="height: {hppct}%;"></div></div>
|
||||
<label>HP</label>
|
||||
<span>HP</span>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div id="mp" class="container"><div class="bar" style="height: {mppct}%;"></div></div>
|
||||
<label>MP</label>
|
||||
<span>MP</span>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div id="tp" class="container"><div class="bar" style="height: {tppct}%;"></div></div>
|
||||
<label>TP</label>
|
||||
<span>TP</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -35,22 +35,40 @@
|
||||
<div class="title"><img src="/assets/images/button_inventory.gif" alt="Inventory" title="Inventory"></div>
|
||||
<div>
|
||||
<img src="/assets/images/icon_weapon.gif" alt="Weapon" title="Weapon">
|
||||
{weaponname}
|
||||
{if user.WeaponName != ""}
|
||||
{user.WeaponName}
|
||||
{else}
|
||||
<i>No weapon</i>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<img src="/assets/images/icon_armor.gif" alt="Armor" title="Armor">
|
||||
{armorname}
|
||||
{if user.ArmorName != ""}
|
||||
{user.ArmorName}
|
||||
{else}
|
||||
<i>No armor</i>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<img src="/assets/images/icon_shield.gif" alt="Shield" title="Shield">
|
||||
{shieldname}
|
||||
{if user.ShieldName != ""}
|
||||
{user.ShieldName}
|
||||
{else}
|
||||
<i>No shield</i>
|
||||
{/if}
|
||||
</div>
|
||||
{slot1name}
|
||||
{slot2name}
|
||||
{slot3name}
|
||||
{if user.Slot1Name != ""}{slot1name}{/if}
|
||||
{if user.Slot2Name != ""}{slot2name}{/if}
|
||||
{if user.Slot3Name != ""}{slot3name}{/if}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="title"><img src="/assets/images/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div>
|
||||
{magiclist}
|
||||
{if #_spells > 0}
|
||||
{for spell in _spells}
|
||||
<a href="#">{spell.name}</a>
|
||||
{/for}
|
||||
{else}
|
||||
<i>No known healing spells</i>
|
||||
{/if}
|
||||
</section>
|
11
templates/topnav.html
Normal file
11
templates/topnav.html
Normal file
@ -0,0 +1,11 @@
|
||||
{if authenticated}
|
||||
<form action="/logout" method="post" class="logout">
|
||||
{csrf}
|
||||
<button class="img-button" type="submit"><img src="/assets/images/button_logout.gif" alt="Log Out" title="Log Out"></button>
|
||||
</form>
|
||||
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>
|
||||
{else}
|
||||
<a href="/login"><img src="/assets/images/button_login.gif" alt="Log In" title="Log In"></a>
|
||||
<a href="/register"><img src="/assets/images/button_register.gif" alt="Register" title="Register"></a>
|
||||
<a href="/help"><img src="/assets/images/button_help.gif" alt="Help" title="Help"></a>
|
||||
{/if}
|
3
templates/town/inn.html
Normal file
3
templates/town/inn.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="town inn">
|
||||
|
||||
</div>
|
@ -1,3 +1,6 @@
|
||||
{include layout.html}
|
||||
|
||||
{block "content"}
|
||||
<div class="town">
|
||||
<div class="options">
|
||||
<div class="title"><img src="/assets/images/town_{town.ID}.gif" alt="Welcome to {town.Name}" title="Welcome to {town.Name}"></div>
|
||||
@ -24,3 +27,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
|
Loading…
x
Reference in New Issue
Block a user