add spells and items panels
This commit is contained in:
parent
78d0e5debe
commit
0c756c425f
BIN
data/dk.db
BIN
data/dk.db
Binary file not shown.
@ -5,6 +5,8 @@ import (
|
||||
"dk/internal/database"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/models/news"
|
||||
"dk/internal/models/spells"
|
||||
"dk/internal/models/towns"
|
||||
"dk/internal/models/users"
|
||||
"fmt"
|
||||
"runtime"
|
||||
@ -34,6 +36,16 @@ func RegisterAdminRoutes(app *sushi.App) {
|
||||
group.Get("/users", adminUsersIndex)
|
||||
group.Get("/users/:id", adminUserEdit)
|
||||
group.Post("/users/:id", adminUserUpdate)
|
||||
group.Get("/towns", adminTownsIndex)
|
||||
group.Get("/towns/new", adminTownNew)
|
||||
group.Post("/towns/new", adminTownCreate)
|
||||
group.Get("/towns/:id", adminTownEdit)
|
||||
group.Post("/towns/:id", adminTownUpdate)
|
||||
group.Get("/spells", adminSpellsIndex)
|
||||
group.Get("/spells/new", adminSpellNew)
|
||||
group.Post("/spells/new", adminSpellCreate)
|
||||
group.Get("/spells/:id", adminSpellEdit)
|
||||
group.Post("/spells/:id", adminSpellUpdate)
|
||||
}
|
||||
|
||||
func adminIndex(ctx sushi.Ctx) {
|
||||
@ -211,6 +223,393 @@ func adminUserUpdate(ctx sushi.Ctx) {
|
||||
ctx.Redirect("/admin/users")
|
||||
}
|
||||
|
||||
func adminTownsIndex(ctx sushi.Ctx) {
|
||||
pagination := helpers.Pagination{
|
||||
Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1),
|
||||
PerPage: 30,
|
||||
}
|
||||
|
||||
type TownData struct {
|
||||
ID int
|
||||
Name string
|
||||
X int
|
||||
Y int
|
||||
InnCost int
|
||||
MapCost int
|
||||
TPCost int
|
||||
ShopList string
|
||||
ShopItemCount int
|
||||
}
|
||||
|
||||
var townList []*TownData
|
||||
err := database.Select(&townList, `
|
||||
SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list,
|
||||
CASE
|
||||
WHEN shop_list = '' THEN 0
|
||||
ELSE (LENGTH(shop_list) - LENGTH(REPLACE(shop_list, ',', '')) + 1)
|
||||
END as shop_item_count
|
||||
FROM towns
|
||||
ORDER BY id ASC
|
||||
LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset())
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting town list for admin index: %s", err.Error())
|
||||
townList = make([]*TownData, 0)
|
||||
}
|
||||
|
||||
type CountResult struct{ Count int }
|
||||
var result CountResult
|
||||
database.Get(&result, "SELECT COUNT(*) as count FROM towns")
|
||||
pagination.Total = result.Count
|
||||
|
||||
components.RenderAdminPage(ctx, "Town Management", "admin/towns/index.html", map[string]any{
|
||||
"towns": townList,
|
||||
"currentPage": pagination.Page,
|
||||
"totalPages": pagination.TotalPages(),
|
||||
"hasNext": pagination.HasNext(),
|
||||
"hasPrev": pagination.HasPrev(),
|
||||
})
|
||||
}
|
||||
|
||||
func adminTownNew(ctx sushi.Ctx) {
|
||||
town := towns.New()
|
||||
components.RenderAdminPage(ctx, "Add New Town", "admin/towns/edit.html", map[string]any{
|
||||
"town": town,
|
||||
})
|
||||
}
|
||||
|
||||
func adminTownCreate(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
town := towns.New()
|
||||
|
||||
if err := populateTownFromForm(ctx, town); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/towns/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := town.Validate(); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/towns/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkTownCoordinateConflict(town.X, town.Y, 0); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/towns/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := town.Insert(); err != nil {
|
||||
sess.SetFlash("error", "Failed to create town")
|
||||
ctx.Redirect("/admin/towns/new")
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetFlash("success", fmt.Sprintf("Town %s created successfully", town.Name))
|
||||
ctx.Redirect("/admin/towns")
|
||||
}
|
||||
|
||||
func adminTownEdit(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
town, err := towns.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", fmt.Sprintf("Town %d not found", id))
|
||||
ctx.Redirect("/admin/towns")
|
||||
return
|
||||
}
|
||||
|
||||
components.RenderAdminPage(ctx, fmt.Sprintf("Edit Town: %s", town.Name), "admin/towns/edit.html", map[string]any{
|
||||
"town": town,
|
||||
})
|
||||
}
|
||||
|
||||
func adminTownUpdate(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
town, err := towns.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", fmt.Sprintf("Town %d not found", id))
|
||||
ctx.Redirect("/admin/towns")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if delete was requested
|
||||
if ctx.Form("delete").String() == "1" {
|
||||
if err := town.Delete(); err != nil {
|
||||
sess.SetFlash("error", "Failed to delete town")
|
||||
ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id))
|
||||
return
|
||||
}
|
||||
sess.SetFlash("success", fmt.Sprintf("Town %s deleted successfully", town.Name))
|
||||
ctx.Redirect("/admin/towns")
|
||||
return
|
||||
}
|
||||
|
||||
if err := populateTownFromForm(ctx, town); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
if err := town.Validate(); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkTownCoordinateConflict(town.X, town.Y, town.ID); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
fields := map[string]any{
|
||||
"name": town.Name,
|
||||
"x": town.X,
|
||||
"y": town.Y,
|
||||
"inn_cost": town.InnCost,
|
||||
"map_cost": town.MapCost,
|
||||
"tp_cost": town.TPCost,
|
||||
"shop_list": town.ShopList,
|
||||
}
|
||||
|
||||
if err := database.Update("towns", fields, "id", id); err != nil {
|
||||
sess.SetFlash("error", "Failed to update town")
|
||||
ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetFlash("success", fmt.Sprintf("Town %s updated successfully", town.Name))
|
||||
ctx.Redirect("/admin/towns")
|
||||
}
|
||||
|
||||
func populateTownFromForm(ctx sushi.Ctx, town *towns.Town) error {
|
||||
town.Name = strings.TrimSpace(ctx.Form("name").String())
|
||||
town.X = ctx.Form("x").Int()
|
||||
town.Y = ctx.Form("y").Int()
|
||||
town.InnCost = ctx.Form("inn_cost").Int()
|
||||
town.MapCost = ctx.Form("map_cost").Int()
|
||||
town.TPCost = ctx.Form("tp_cost").Int()
|
||||
town.ShopList = strings.TrimSpace(ctx.Form("shop_list").String())
|
||||
|
||||
if town.Name == "" {
|
||||
return fmt.Errorf("town name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkTownCoordinateConflict(x, y, excludeID int) error {
|
||||
existingTown, err := towns.ByCoords(x, y)
|
||||
if err != nil {
|
||||
return nil // No conflict if no town found or database error
|
||||
}
|
||||
|
||||
if existingTown != nil && existingTown.ID != excludeID {
|
||||
return fmt.Errorf("a town already exists at coordinates (%d, %d): %s", x, y, existingTown.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func adminSpellsIndex(ctx sushi.Ctx) {
|
||||
pagination := helpers.Pagination{
|
||||
Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1),
|
||||
PerPage: 30,
|
||||
}
|
||||
|
||||
type SpellData struct {
|
||||
ID int
|
||||
Name string
|
||||
Type int
|
||||
TypeName string
|
||||
MP int
|
||||
Power int
|
||||
Icon string
|
||||
Lore string
|
||||
}
|
||||
|
||||
var spellList []*SpellData
|
||||
err := database.Select(&spellList, `
|
||||
SELECT id, name, type, mp, power, icon, lore,
|
||||
CASE type
|
||||
WHEN 0 THEN 'Heal'
|
||||
WHEN 1 THEN 'Damage'
|
||||
WHEN 2 THEN 'Sleep'
|
||||
WHEN 3 THEN 'Uber Attack'
|
||||
WHEN 4 THEN 'Uber Defense'
|
||||
ELSE 'Unknown'
|
||||
END as type_name
|
||||
FROM spells
|
||||
ORDER BY type ASC, mp ASC, id ASC
|
||||
LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset())
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting spell list for admin index: %s", err.Error())
|
||||
spellList = make([]*SpellData, 0)
|
||||
}
|
||||
|
||||
type CountResult struct{ Count int }
|
||||
var result CountResult
|
||||
database.Get(&result, "SELECT COUNT(*) as count FROM spells")
|
||||
pagination.Total = result.Count
|
||||
|
||||
components.RenderAdminPage(ctx, "Spell Management", "admin/spells/index.html", map[string]any{
|
||||
"spells": spellList,
|
||||
"currentPage": pagination.Page,
|
||||
"totalPages": pagination.TotalPages(),
|
||||
"hasNext": pagination.HasNext(),
|
||||
"hasPrev": pagination.HasPrev(),
|
||||
})
|
||||
}
|
||||
|
||||
func adminSpellNew(ctx sushi.Ctx) {
|
||||
spell := spells.New()
|
||||
components.RenderAdminPage(ctx, "Add New Spell", "admin/spells/edit.html", map[string]any{
|
||||
"spell": spell,
|
||||
})
|
||||
}
|
||||
|
||||
func adminSpellCreate(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
spell := spells.New()
|
||||
|
||||
if err := populateSpellFromForm(ctx, spell); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/spells/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := spell.Validate(); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/spells/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkSpellNameConflict(spell.Name, 0); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect("/admin/spells/new")
|
||||
return
|
||||
}
|
||||
|
||||
if err := spell.Insert(); err != nil {
|
||||
sess.SetFlash("error", "Failed to create spell")
|
||||
ctx.Redirect("/admin/spells/new")
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetFlash("success", fmt.Sprintf("Spell %s created successfully", spell.Name))
|
||||
ctx.Redirect("/admin/spells")
|
||||
}
|
||||
|
||||
func adminSpellEdit(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
spell, err := spells.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", fmt.Sprintf("Spell %d not found", id))
|
||||
ctx.Redirect("/admin/spells")
|
||||
return
|
||||
}
|
||||
|
||||
components.RenderAdminPage(ctx, fmt.Sprintf("Edit Spell: %s", spell.Name), "admin/spells/edit.html", map[string]any{
|
||||
"spell": spell,
|
||||
})
|
||||
}
|
||||
|
||||
func adminSpellUpdate(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
spell, err := spells.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", fmt.Sprintf("Spell %d not found", id))
|
||||
ctx.Redirect("/admin/spells")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if delete was requested
|
||||
if ctx.Form("delete").String() == "1" {
|
||||
if err := spell.Delete(); err != nil {
|
||||
sess.SetFlash("error", "Failed to delete spell")
|
||||
ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id))
|
||||
return
|
||||
}
|
||||
sess.SetFlash("success", fmt.Sprintf("Spell %s deleted successfully", spell.Name))
|
||||
ctx.Redirect("/admin/spells")
|
||||
return
|
||||
}
|
||||
|
||||
if err := populateSpellFromForm(ctx, spell); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
if err := spell.Validate(); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkSpellNameConflict(spell.Name, spell.ID); err != nil {
|
||||
sess.SetFlash("error", err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
fields := map[string]any{
|
||||
"name": spell.Name,
|
||||
"type": spell.Type,
|
||||
"mp": spell.MP,
|
||||
"power": spell.Power,
|
||||
"icon": spell.Icon,
|
||||
"lore": spell.Lore,
|
||||
}
|
||||
|
||||
if err := database.Update("spells", fields, "id", id); err != nil {
|
||||
sess.SetFlash("error", "Failed to update spell")
|
||||
ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id))
|
||||
return
|
||||
}
|
||||
|
||||
sess.SetFlash("success", fmt.Sprintf("Spell %s updated successfully", spell.Name))
|
||||
ctx.Redirect("/admin/spells")
|
||||
}
|
||||
|
||||
func populateSpellFromForm(ctx sushi.Ctx, spell *spells.Spell) error {
|
||||
spell.Name = strings.TrimSpace(ctx.Form("name").String())
|
||||
spell.Type = ctx.Form("type").Int()
|
||||
spell.MP = ctx.Form("mp").Int()
|
||||
spell.Power = ctx.Form("power").Int()
|
||||
spell.Icon = strings.TrimSpace(ctx.Form("icon").String())
|
||||
spell.Lore = strings.TrimSpace(ctx.Form("lore").String())
|
||||
|
||||
if spell.Name == "" {
|
||||
return fmt.Errorf("spell name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSpellNameConflict(name string, excludeID int) error {
|
||||
existingSpell, err := spells.ByName(name)
|
||||
if err != nil {
|
||||
return nil // No conflict if no spell found or database error
|
||||
}
|
||||
|
||||
if existingSpell != nil && existingSpell.ID != excludeID {
|
||||
return fmt.Errorf("a spell with the name '%s' already exists", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
||||
|
47
templates/admin/spells/edit.html
Normal file
47
templates/admin/spells/edit.html
Normal file
@ -0,0 +1,47 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>{if spell.ID}Edit Spell: {spell.Name}{else}Add New Spell{/if}</h1>
|
||||
|
||||
<form class="standard" method="post">
|
||||
{csrf}
|
||||
<div>
|
||||
<label for="name">Spell Name:</label>
|
||||
<input type="text" name="name" id="name" value="{spell.Name}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="type">Spell Type:</label>
|
||||
<select name="type" id="type" required>
|
||||
<option value="0"{if spell.Type == 0} selected{/if}>Heal</option>
|
||||
<option value="1"{if spell.Type == 1} selected{/if}>Damage</option>
|
||||
<option value="2"{if spell.Type == 2} selected{/if}>Sleep</option>
|
||||
<option value="3"{if spell.Type == 3} selected{/if}>Uber Attack</option>
|
||||
<option value="4"{if spell.Type == 4} selected{/if}>Uber Defense</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mp">MP Cost:</label>
|
||||
<input type="number" name="mp" id="mp" value="{spell.MP}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="power">Power:</label>
|
||||
<input type="number" name="power" id="power" value="{spell.Power}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="icon">Icon:</label>
|
||||
<input type="text" name="icon" id="icon" value="{spell.Icon}" placeholder="spell_icon.png">
|
||||
<small>Optional icon filename or path</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lore">Lore/Description:</label>
|
||||
<textarea name="lore" id="lore" rows="3" placeholder="Spell description or lore text">{spell.Lore}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/spells"><button type="button" class="btn">Cancel</button></a>
|
||||
<button type="submit" class="btn btn-primary">{if spell.ID}Update{else}Create{/if} Spell</button>
|
||||
{if spell.ID}
|
||||
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this spell?')">Delete Spell</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/block}
|
60
templates/admin/spells/index.html
Normal file
60
templates/admin/spells/index.html
Normal file
@ -0,0 +1,60 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Spell Management</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/spells/new" class="btn btn-primary">Add New Spell</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Total spells: {#spells} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
|
||||
{if #spells > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>MP Cost</th>
|
||||
<th>Power</th>
|
||||
<th>Icon</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{for spell in spells}
|
||||
<tr>
|
||||
<td>{spell.ID}</td>
|
||||
<td>{spell.Name}</td>
|
||||
<td>{spell.TypeName}</td>
|
||||
<td>{spell.MP}</td>
|
||||
<td>{spell.Power}</td>
|
||||
<td>{if spell.Icon}{spell.Icon}{else}-{/if}</td>
|
||||
<td>
|
||||
<a href="/admin/spells/{spell.ID}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{if totalPages > 1}
|
||||
<div class="pagination">
|
||||
{if hasPrev}
|
||||
<a href="/admin/spells?page={currentPage - 1}">← Previous</a>
|
||||
{/if}
|
||||
{if hasNext}
|
||||
<a href="/admin/spells?page={currentPage + 1}">Next →</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<div>
|
||||
No spells found.
|
||||
</div>
|
||||
{/if}
|
||||
{/block}
|
45
templates/admin/towns/edit.html
Normal file
45
templates/admin/towns/edit.html
Normal file
@ -0,0 +1,45 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>{if town.ID}Edit Town: {town.Name}{else}Add New Town{/if}</h1>
|
||||
|
||||
<form class="standard" method="post">
|
||||
{csrf}
|
||||
<div>
|
||||
<label for="name">Town Name:</label>
|
||||
<input type="text" name="name" id="name" value="{town.Name}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="x">X Position:</label>
|
||||
<input type="number" name="x" id="x" value="{town.X}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="y">Y Position:</label>
|
||||
<input type="number" name="y" id="y" value="{town.Y}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="inn_cost">Inn Cost (gold):</label>
|
||||
<input type="number" name="inn_cost" id="inn_cost" value="{town.InnCost}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="map_cost">Map Cost (gold):</label>
|
||||
<input type="number" name="map_cost" id="map_cost" value="{town.MapCost}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tp_cost">Teleport Cost (gold):</label>
|
||||
<input type="number" name="tp_cost" id="tp_cost" value="{town.TPCost}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="shop_list">Shop Items (comma-separated item IDs):</label>
|
||||
<input type="text" name="shop_list" id="shop_list" value="{town.ShopList}" placeholder="1,2,3">
|
||||
<small>Enter item IDs separated by commas (e.g. "1,2,3,4")</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/towns"><button type="button" class="btn">Cancel</button></a>
|
||||
<button type="submit" class="btn btn-primary">{if town.ID}Update{else}Create{/if} Town</button>
|
||||
{if town.ID}
|
||||
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this town?')">Delete Town</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/block}
|
62
templates/admin/towns/index.html
Normal file
62
templates/admin/towns/index.html
Normal file
@ -0,0 +1,62 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Town Management</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/towns/new" class="btn btn-primary">Add New Town</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Total towns: {#towns} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
|
||||
{if #towns > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Position</th>
|
||||
<th>Inn Cost</th>
|
||||
<th>Map Cost</th>
|
||||
<th>TP Cost</th>
|
||||
<th>Shop Items</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{for town in towns}
|
||||
<tr>
|
||||
<td>{town.ID}</td>
|
||||
<td>{town.Name}</td>
|
||||
<td>({town.X}, {town.Y})</td>
|
||||
<td>{town.InnCost}g</td>
|
||||
<td>{town.MapCost}g</td>
|
||||
<td>{town.TPCost}g</td>
|
||||
<td>{town.ShopItemCount}</td>
|
||||
<td>
|
||||
<a href="/admin/towns/{town.ID}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{if totalPages > 1}
|
||||
<div class="pagination">
|
||||
{if hasPrev}
|
||||
<a href="/admin/towns?page={currentPage - 1}">← Previous</a>
|
||||
{/if}
|
||||
{if hasNext}
|
||||
<a href="/admin/towns?page={currentPage + 1}">Next →</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<div>
|
||||
No towns found.
|
||||
</div>
|
||||
{/if}
|
||||
{/block}
|
Loading…
x
Reference in New Issue
Block a user