add monster panel to admin

This commit is contained in:
Sky Johnson 2025-08-29 07:51:04 -05:00
parent 0c756c425f
commit 59724dee81
5 changed files with 331 additions and 0 deletions

Binary file not shown.

View File

@ -105,6 +105,15 @@ func ByImmunity(immunityType int) ([]*Monster, error) {
return monsters, err return monsters, err
} }
func ByName(name string) (*Monster, error) {
var monster Monster
err := database.Get(&monster, "SELECT * FROM monsters WHERE name = %s COLLATE NOCASE", name)
if err != nil {
return nil, fmt.Errorf("monster with name '%s' not found", name)
}
return &monster, nil
}
// Helper methods // Helper methods
func (m *Monster) IsHurtImmune() bool { func (m *Monster) IsHurtImmune() bool {
return m.Immune == ImmuneHurt return m.Immune == ImmuneHurt

View File

@ -4,6 +4,7 @@ import (
"dk/internal/components" "dk/internal/components"
"dk/internal/database" "dk/internal/database"
"dk/internal/helpers" "dk/internal/helpers"
"dk/internal/models/monsters"
"dk/internal/models/news" "dk/internal/models/news"
"dk/internal/models/spells" "dk/internal/models/spells"
"dk/internal/models/towns" "dk/internal/models/towns"
@ -46,6 +47,11 @@ func RegisterAdminRoutes(app *sushi.App) {
group.Post("/spells/new", adminSpellCreate) group.Post("/spells/new", adminSpellCreate)
group.Get("/spells/:id", adminSpellEdit) group.Get("/spells/:id", adminSpellEdit)
group.Post("/spells/:id", adminSpellUpdate) group.Post("/spells/:id", adminSpellUpdate)
group.Get("/monsters", adminMonstersIndex)
group.Get("/monsters/new", adminMonsterNew)
group.Post("/monsters/new", adminMonsterCreate)
group.Get("/monsters/:id", adminMonsterEdit)
group.Post("/monsters/:id", adminMonsterUpdate)
} }
func adminIndex(ctx sushi.Ctx) { func adminIndex(ctx sushi.Ctx) {
@ -610,6 +616,204 @@ func checkSpellNameConflict(name string, excludeID int) error {
return nil return nil
} }
func adminMonstersIndex(ctx sushi.Ctx) {
pagination := helpers.Pagination{
Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1),
PerPage: 30,
}
type MonsterData struct {
ID int
Name string
Level int
MaxHP int
MaxDmg int
Armor int
MaxExp int
MaxGold int
Immune int
ImmunityName string
}
var monsterList []*MonsterData
err := database.Select(&monsterList, `
SELECT id, name, level, max_hp, max_dmg, armor, max_exp, max_gold, immune,
CASE immune
WHEN 0 THEN 'None'
WHEN 1 THEN 'Hurt Spells'
WHEN 2 THEN 'Sleep Spells'
ELSE 'Unknown'
END as immunity_name
FROM monsters
ORDER BY level ASC, id ASC
LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset())
if err != nil {
fmt.Printf("Error getting monster list for admin index: %s", err.Error())
monsterList = make([]*MonsterData, 0)
}
type CountResult struct{ Count int }
var result CountResult
database.Get(&result, "SELECT COUNT(*) as count FROM monsters")
pagination.Total = result.Count
components.RenderAdminPage(ctx, "Monster Management", "admin/monsters/index.html", map[string]any{
"monsters": monsterList,
"currentPage": pagination.Page,
"totalPages": pagination.TotalPages(),
"hasNext": pagination.HasNext(),
"hasPrev": pagination.HasPrev(),
})
}
func adminMonsterNew(ctx sushi.Ctx) {
monster := monsters.New()
components.RenderAdminPage(ctx, "Add New Monster", "admin/monsters/edit.html", map[string]any{
"monster": monster,
})
}
func adminMonsterCreate(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
monster := monsters.New()
if err := populateMonsterFromForm(ctx, monster); err != nil {
sess.SetFlash("error", err.Error())
ctx.Redirect("/admin/monsters/new")
return
}
if err := monster.Validate(); err != nil {
sess.SetFlash("error", err.Error())
ctx.Redirect("/admin/monsters/new")
return
}
if err := checkMonsterNameConflict(monster.Name, 0); err != nil {
sess.SetFlash("error", err.Error())
ctx.Redirect("/admin/monsters/new")
return
}
if err := monster.Insert(); err != nil {
sess.SetFlash("error", "Failed to create monster")
ctx.Redirect("/admin/monsters/new")
return
}
sess.SetFlash("success", fmt.Sprintf("Monster %s created successfully", monster.Name))
ctx.Redirect("/admin/monsters")
}
func adminMonsterEdit(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
id := ctx.Param("id").Int()
monster, err := monsters.Find(id)
if err != nil {
sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id))
ctx.Redirect("/admin/monsters")
return
}
components.RenderAdminPage(ctx, fmt.Sprintf("Edit Monster: %s", monster.Name), "admin/monsters/edit.html", map[string]any{
"monster": monster,
})
}
func adminMonsterUpdate(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
id := ctx.Param("id").Int()
monster, err := monsters.Find(id)
if err != nil {
sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id))
ctx.Redirect("/admin/monsters")
return
}
// Check if delete was requested
if ctx.Form("delete").String() == "1" {
if err := monster.Delete(); err != nil {
sess.SetFlash("error", "Failed to delete monster")
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
return
}
sess.SetFlash("success", fmt.Sprintf("Monster %s deleted successfully", monster.Name))
ctx.Redirect("/admin/monsters")
return
}
if err := populateMonsterFromForm(ctx, monster); err != nil {
sess.SetFlash("error", err.Error())
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
return
}
if err := monster.Validate(); err != nil {
sess.SetFlash("error", err.Error())
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
return
}
if err := checkMonsterNameConflict(monster.Name, monster.ID); err != nil {
sess.SetFlash("error", err.Error())
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
return
}
fields := map[string]any{
"name": monster.Name,
"level": monster.Level,
"max_hp": monster.MaxHP,
"max_dmg": monster.MaxDmg,
"armor": monster.Armor,
"max_exp": monster.MaxExp,
"max_gold": monster.MaxGold,
"immune": monster.Immune,
}
if err := database.Update("monsters", fields, "id", id); err != nil {
sess.SetFlash("error", "Failed to update monster")
ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id))
return
}
sess.SetFlash("success", fmt.Sprintf("Monster %s updated successfully", monster.Name))
ctx.Redirect("/admin/monsters")
}
func populateMonsterFromForm(ctx sushi.Ctx, monster *monsters.Monster) error {
monster.Name = strings.TrimSpace(ctx.Form("name").String())
monster.Level = ctx.Form("level").Int()
monster.MaxHP = ctx.Form("max_hp").Int()
monster.MaxDmg = ctx.Form("max_dmg").Int()
monster.Armor = ctx.Form("armor").Int()
monster.MaxExp = ctx.Form("max_exp").Int()
monster.MaxGold = ctx.Form("max_gold").Int()
monster.Immune = ctx.Form("immune").Int()
if monster.Name == "" {
return fmt.Errorf("monster name is required")
}
return nil
}
func checkMonsterNameConflict(name string, excludeID int) error {
existingMonster, err := monsters.ByName(name)
if err != nil {
return nil // No conflict if no monster found or database error
}
if existingMonster != nil && existingMonster.ID != excludeID {
return fmt.Errorf("a monster with the name '%s' already exists", name)
}
return nil
}
func bToMb(b uint64) uint64 { func bToMb(b uint64) uint64 {
return b / 1024 / 1024 return b / 1024 / 1024
} }

View File

@ -0,0 +1,52 @@
{include "admin/layout.html"}
{block "content"}
<h1>{if monster.ID}Edit Monster: {monster.Name}{else}Add New Monster{/if}</h1>
<form class="standard" method="post">
{csrf}
<div>
<label for="name">Monster Name:</label>
<input type="text" name="name" id="name" value="{monster.Name}" required>
</div>
<div>
<label for="level">Level:</label>
<input type="number" name="level" id="level" value="{monster.Level}" min="1" required>
</div>
<div>
<label for="max_hp">Max HP:</label>
<input type="number" name="max_hp" id="max_hp" value="{monster.MaxHP}" min="1" required>
</div>
<div>
<label for="max_dmg">Max Damage:</label>
<input type="number" name="max_dmg" id="max_dmg" value="{monster.MaxDmg}" min="0" required>
</div>
<div>
<label for="armor">Armor:</label>
<input type="number" name="armor" id="armor" value="{monster.Armor}" min="0" required>
</div>
<div>
<label for="max_exp">Max Experience:</label>
<input type="number" name="max_exp" id="max_exp" value="{monster.MaxExp}" min="0" required>
</div>
<div>
<label for="max_gold">Max Gold:</label>
<input type="number" name="max_gold" id="max_gold" value="{monster.MaxGold}" min="0" required>
</div>
<div>
<label for="immune">Immunity:</label>
<select name="immune" id="immune" required>
<option value="0"{if monster.Immune == 0} selected{/if}>None</option>
<option value="1"{if monster.Immune == 1} selected{/if}>Hurt Spells</option>
<option value="2"{if monster.Immune == 2} selected{/if}>Sleep Spells</option>
</select>
</div>
<div>
<a href="/admin/monsters"><button type="button" class="btn">Cancel</button></a>
<button type="submit" class="btn btn-primary">{if monster.ID}Update{else}Create{/if} Monster</button>
{if monster.ID}
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this monster?')">Delete Monster</button>
{/if}
</div>
</form>
{/block}

View File

@ -0,0 +1,66 @@
{include "admin/layout.html"}
{block "content"}
<h1>Monster Management</h1>
<div style="margin-bottom: 1rem;">
<a href="/admin/monsters/new" class="btn btn-primary">Add New Monster</a>
</div>
<p>
Total monsters: {#monsters} | Page {currentPage} of {totalPages}
</p>
{if #monsters > 0}
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Level</th>
<th>HP</th>
<th>Damage</th>
<th>Armor</th>
<th>Exp</th>
<th>Gold</th>
<th>Immunity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{for monster in monsters}
<tr>
<td>{monster.ID}</td>
<td>{monster.Name}</td>
<td>{monster.Level}</td>
<td>{monster.MaxHP}</td>
<td>{monster.MaxDmg}</td>
<td>{monster.Armor}</td>
<td>{monster.MaxExp}</td>
<td>{monster.MaxGold}</td>
<td>{monster.ImmunityName}</td>
<td>
<a href="/admin/monsters/{monster.ID}">Edit</a>
</td>
</tr>
{/for}
</tbody>
</table>
{if totalPages > 1}
<div class="pagination">
{if hasPrev}
<a href="/admin/monsters?page={currentPage - 1}">← Previous</a>
{/if}
{if hasNext}
<a href="/admin/monsters?page={currentPage + 1}">Next →</a>
{/if}
</div>
{/if}
{else}
<div>
No monsters found.
</div>
{/if}
{/block}