make error/success notices global, add framework for confirmation modals

This commit is contained in:
Sky Johnson 2025-08-14 18:12:24 -05:00
parent 9a5ed65f04
commit b42f4fc983
15 changed files with 150 additions and 168 deletions

View File

@ -391,3 +391,7 @@ div.modal {
justify-content: center;
}
}
.mt-1 {
margin-top: 1rem;
}

View File

@ -0,0 +1,62 @@
class ConfirmModal {
constructor(options = {}) {
this.modal = document.querySelector(options.modalSelector || "#confirm-modal")
this.message = this.modal.querySelector(options.messageSelector || "#msg")
this.confirmBtn = this.modal.querySelector(options.confirmSelector || "#confirm")
this.cancelBtn = this.modal.querySelector(options.cancelSelector || "#cancel")
this.linkSelector = options.linkSelector || '.confirm-link'
this.messageGenerator = options.messageGenerator || this.defaultMessageGenerator.bind(this)
this.currentUrl = null
if (options.confirmText) this.confirmBtn.textContent = options.confirmText
if (options.cancelText) this.cancelBtn.textContent = options.cancelText
this.init()
}
init() {
document.querySelectorAll(this.linkSelector).forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault()
const message = this.messageGenerator(link)
this.show(link.href, message)
})
})
this.confirmBtn.addEventListener('click', () => this.confirm())
this.cancelBtn.addEventListener('click', () => this.hide())
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.hide()
})
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.modal.style.display === 'block') {
this.hide()
}
})
}
defaultMessageGenerator(link) {
return link.dataset.message || 'Are you sure you want to continue?'
}
show(url, message) {
this.currentUrl = url
this.message.textContent = message
this.modal.style.display = 'block'
this.confirmBtn.focus()
}
hide() {
this.modal.style.display = 'none'
this.currentUrl = null
}
confirm() {
if (this.currentUrl) {
window.location.href = this.currentUrl
}
this.hide()
}
}

View File

@ -10,6 +10,7 @@ import (
"dk/internal/csrf"
"dk/internal/middleware"
"dk/internal/router"
"dk/internal/session"
"dk/internal/template"
)
@ -24,6 +25,8 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
return fmt.Errorf("failed to load layout template: %w", err)
}
sess := ctx.UserValue("session").(*session.Session)
var m runtime.MemStats
runtime.ReadMemStats(&m)
@ -36,6 +39,8 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
"_build": "dev",
"user": auth.GetCurrentUser(ctx),
"_memalloc": m.Alloc / 1024 / 1024,
"_errormsg": sess.GetFlashMessage("error"),
"_successmsg": sess.GetFlashMessage("success"),
}
maps.Copy(data, LeftAside(ctx))

View File

@ -1,23 +0,0 @@
package components
import (
"dk/internal/auth"
"dk/internal/csrf"
"dk/internal/router"
"fmt"
)
// GenerateTopNav generates the top navigation HTML based on authentication status
func GenerateTopNav(ctx router.Ctx) string {
if auth.IsAuthenticated(ctx) {
return fmt.Sprintf(`<form action="/logout" method="post" class="logout">
%s
<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>`, csrf.HiddenField(ctx))
} else {
return `<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>`
}
}

View File

@ -99,5 +99,6 @@ func Teleport(ctx router.Ctx, params []string) {
user.Currently = "In Town"
user.Save()
sess.SetFlash("success", "You teleported to "+town.Name+" successfully!")
ctx.Redirect("/town", 302)
}

View File

@ -71,6 +71,16 @@ func (s *Session) GetFlash(key string) (any, bool) {
return value, exists
}
// GetFlashMessage retrieves and removes a flash message as string or empty string
func (s *Session) GetFlashMessage(key string) string {
if flash, exists := s.GetFlash(key); exists {
if msg, ok := flash.(string); ok {
return msg
}
}
return ""
}
// RegenerateID creates a new session ID and updates storage
func (s *Session) RegenerateID() {
oldID := s.ID

View File

@ -3,8 +3,6 @@
{block "content"}
<h1>Log In</h1>
{error_message}
<form class="standard mb-1" action="/login" method="post">
{csrf}

View File

@ -3,8 +3,6 @@
{block "content"}
<h1>Register</h1>
{error_message}
<form class="standard" action="/register" method="post">
{csrf}

7
templates/flashes.html Normal file
View File

@ -0,0 +1,7 @@
{if _errormsg != ""}
<div style="color: red; margin-bottom: 1rem;">{_errormsg}</div>
{/if}
{if _successmsg != ""}
<div style="color: green; margin-bottom: 1rem;">{_successmsg}</div>
{/if}

View File

@ -1,5 +1,5 @@
{include "layout.html"}
{block "content"}
Hey there, Guest!
{/block}
Hey there!
{/block}

View File

@ -8,6 +8,8 @@
<link rel="stylesheet" href="/assets/reset.css">
<link rel="stylesheet" href="/assets/dk.css">
<script src="/assets/scripts/confirm_modal.js"></script>
<script>
function open_char_popup()
{
@ -33,7 +35,10 @@
{include "leftside.html"}
{/if}
</aside>
<main>{yield "content"}</main>
<main>
{include "flashes.html"}
{yield "content"}
</main>
<aside id="right">
{if authenticated}
{include "rightside.html"}
@ -49,6 +54,16 @@
</footer>
</div>
<div id="confirm-modal" class="modal">
<div class="content">
<p id="msg">Are you sure you want to continue?</p>
<div class="buttons">
<button id="confirm" class="btn btn-primary">Yes</button>
<button id="cancel" class="btn">Cancel</button>
</div>
</div>
</div>
{yield "scripts"}
</body>
</html>

View File

@ -31,7 +31,11 @@
{if town != nil and t.Name == town.Name}
<span>{t.Name} <i>(here)</i></span>
{else}
<a href="/teleport/{t.ID}">{t.Name}</a>
{if user.TP < t.TPCost}
<span class="light">{t.Name}</span>
{else}
<a class="teleport-link" href="/teleport/{t.ID}" data-town="{t.Name}" data-cost="{t.TPCost}">{t.Name}</a>
{/if}
{/if}
{/for}
{else}
@ -47,3 +51,14 @@
<a href="#">Log Out</a>
<a href="/help">Help</a>
</section>
<script>
document.addEventListener('DOMContentLoaded', () => {
const teleportModal = new ConfirmModal({
linkSelector: '.teleport-link',
messageGenerator: (link) => `Are you sure you want to teleport to ${link.dataset.town} for ${link.dataset.cost} TP?`,
confirmText: 'Teleport',
cancelText: 'Stay'
})
})
</script>

View File

@ -20,19 +20,20 @@
<div id="mp" class="container"><div class="bar" style="height: {mppct}%;"></div></div>
<span>MP</span>
</div>
<div class="stat">
<div id="tp" class="container"><div class="bar" style="height: {tppct}%;"></div></div>
<span>TP</span>
</div>
</div>
<a href="javascript:open_char_popup()">Extended Stats</a>
</section>
<section>
<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">
{if user.WeaponName != ""}
@ -60,6 +61,13 @@
{if user.Slot1Name != ""}{slot1name}{/if}
{if user.Slot2Name != ""}{slot2name}{/if}
{if user.Slot3Name != ""}{slot3name}{/if}
<ul class="unstyled mt-1">
<li>Strength: {user.Strength}</li>
<li>Dexterity: {user.Dexterity}</li>
<li>Attack: {user.Attack}</li>
<li>Defense: {user.Defense}</li>
</ul>
</section>
<section>
@ -71,4 +79,4 @@
{else}
<i>No known healing spells</i>
{/if}
</section>
</section>

View File

@ -3,7 +3,6 @@
{block "content"}
<div class="town shop">
<div class="title"><h3>{town.Name} Maps</h3></div>
{error_message}
<section>
<p>Buying maps will put the town in your Travel To box, and it won't cost you as many TP to get there.</p>
<p>Click a town name to purchase its map.</p>
@ -16,7 +15,7 @@
<td>
{if map.Owned == false}
{if user.Gold >= map.Cost}
<a class="buy-item" data-item="{map.Name} Map" data-cost="{map.Cost}" href="/town/maps/buy/{map.ID}">{map.Name}</a>
<a class="buy-item" data-town="{map.Name}" data-cost="{map.Cost}" href="/town/maps/buy/{map.ID}">{map.Name}</a>
{else}
<span class="light">{map.Name}</span>
{/if}
@ -44,76 +43,18 @@
<section>
<p>If you've changed your mind, you may also return back to <a href="/town">town</a>.</p>
</section>
<div id="shop-modal" class="modal">
<div class="content">
<p id="msg">Are you sure you want to buy this item?</p>
<div class="buttons">
<button id="confirm" class="btn btn-primary">Buy Now</button>
<button id="cancel" class="btn">Cancel</button>
</div>
</div>
</div>
</div>
{/block}
{block "scripts"}
<script>
class ShopModal {
constructor() {
this.modal = document.querySelector("#shop-modal")
this.message = this.modal.querySelector("#msg")
this.confirmBtn = this.modal.querySelector("#confirm")
this.cancelBtn = this.modal.querySelector("#cancel")
this.currentUrl = null
this.init()
}
init() {
document.querySelectorAll('.buy-item').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault()
this.show(link.href, link.dataset.item, link.dataset.cost)
})
})
this.confirmBtn.addEventListener('click', () => this.confirm())
this.cancelBtn.addEventListener('click', () => this.hide())
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.hide()
})
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.modal.style.display === 'block') {
this.hide()
}
})
}
show(url, itemName, itemCost) {
this.currentUrl = url
this.message.textContent = `Are you sure you want to buy ${itemName} for ${itemCost}G?`
this.modal.style.display = 'block'
this.confirmBtn.focus()
}
hide() {
this.modal.style.display = 'none'
this.currentUrl = null
}
confirm() {
if (this.currentUrl) {
window.location.href = this.currentUrl
}
this.hide()
}
}
document.addEventListener('DOMContentLoaded', () => {
new ShopModal()
const shopModal = new ConfirmModal({
linkSelector: '.buy-item',
messageGenerator: (link) => `Are you sure you want to buy the map to ${link.dataset.town} for ${link.dataset.cost}G?`,
confirmText: 'Buy Now',
cancelText: 'Nevermind'
})
})
</script>
{/block}

View File

@ -3,11 +3,10 @@
{block "content"}
<div class="town shop">
<div class="title"><h3>{town.Name} Shop</h3></div>
{error_message}
<section>
<p>Buying weapons will increase your Attack. Buying armor and shields will increase your Defense.</p>
<p>Click an item name to purchase it.</p>
<p>The following items are available at this town:</p>
<p>The following items are available in {town.Name}:</p>
</section>
<section>
@ -38,7 +37,7 @@
</td>
<td>
{item.Att}
<span class="light">{if item.Type == 1}Attack{else}Defense{/if}</span>
{if item.Type == 1}Attack{else}Defense{/if}
</td>
<td>
{if user.WeaponID == item.ID or user.ArmorID == item.ID or user.ShieldID == item.ID}
@ -59,76 +58,18 @@
<section>
<p>If you've changed your mind, you may also return back to <a href="/town">town</a>.</p>
</section>
<div id="shop-modal" class="modal">
<div class="content">
<p id="msg">Are you sure you want to buy this item?</p>
<div class="buttons">
<button id="confirm" class="btn btn-primary">Buy Now</button>
<button id="cancel" class="btn">Cancel</button>
</div>
</div>
</div>
</div>
{/block}
{block "scripts"}
<script>
class ShopModal {
constructor() {
this.modal = document.querySelector("#shop-modal")
this.message = this.modal.querySelector("#msg")
this.confirmBtn = this.modal.querySelector("#confirm")
this.cancelBtn = this.modal.querySelector("#cancel")
this.currentUrl = null
this.init()
}
init() {
document.querySelectorAll('.buy-item').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault()
this.show(link.href, link.dataset.item, link.dataset.cost)
})
})
this.confirmBtn.addEventListener('click', () => this.confirm())
this.cancelBtn.addEventListener('click', () => this.hide())
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.hide()
})
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.modal.style.display === 'block') {
this.hide()
}
})
}
show(url, itemName, itemCost) {
this.currentUrl = url
this.message.textContent = `Are you sure you want to buy ${itemName} for ${itemCost}G?`
this.modal.style.display = 'block'
this.confirmBtn.focus()
}
hide() {
this.modal.style.display = 'none'
this.currentUrl = null
}
confirm() {
if (this.currentUrl) {
window.location.href = this.currentUrl
}
this.hide()
}
}
document.addEventListener('DOMContentLoaded', () => {
new ShopModal()
const shopModal = new ConfirmModal({
linkSelector: '.buy-item',
messageGenerator: (link) => `Are you sure you want to buy ${link.dataset.item} for ${link.dataset.cost}G?`,
confirmText: 'Buy Now',
cancelText: 'Nevermind'
})
})
</script>
{/block}