diff --git a/data/dk.db b/data/dk.db index 3682e65..fa290bb 100644 Binary files a/data/dk.db and b/data/dk.db differ diff --git a/internal/routes/admin.go b/internal/routes/admin.go index 74c57f4..8dd8583 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -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 } diff --git a/templates/admin/spells/edit.html b/templates/admin/spells/edit.html new file mode 100644 index 0000000..cf13356 --- /dev/null +++ b/templates/admin/spells/edit.html @@ -0,0 +1,47 @@ +{include "admin/layout.html"} + +{block "content"} +
+ Total spells: {#spells} | Page {currentPage} of {totalPages} +
+ +{if #spells > 0} +ID | +Name | +Type | +MP Cost | +Power | +Icon | +Actions | +
---|---|---|---|---|---|---|
{spell.ID} | +{spell.Name} | +{spell.TypeName} | +{spell.MP} | +{spell.Power} | +{if spell.Icon}{spell.Icon}{else}-{/if} | ++ Edit + | +
+ Total towns: {#towns} | Page {currentPage} of {totalPages} +
+ +{if #towns > 0} +ID | +Name | +Position | +Inn Cost | +Map Cost | +TP Cost | +Shop Items | +Actions | +
---|---|---|---|---|---|---|---|
{town.ID} | +{town.Name} | +({town.X}, {town.Y}) | +{town.InnCost}g | +{town.MapCost}g | +{town.TPCost}g | +{town.ShopItemCount} | ++ Edit + | +