diff --git a/index.html b/index.html index 2597e46..de01d15 100644 --- a/index.html +++ b/index.html @@ -59,6 +59,11 @@ +

Förslag på startläge

+

Startpengar och varan är bara förslag. De sparas först när du redigerat och godkänt dem.

+ + +
@@ -68,6 +73,15 @@

+

Ekonomi och ägodelar

+

Saldo: kr

+ + +

Samtal

@@ -125,6 +139,21 @@ document.querySelector('#message-label').firstChild.textContent = `Skriv till ${world.npc.name} `; document.querySelector('#player-heading').textContent = `${gameState.character.name}, ${gameState.character.age} år`; document.querySelector('#player-biography').textContent = gameState.character.biography; + document.querySelector('#balance').textContent = (world.balance_sek ?? 0).toLocaleString('sv-SE'); + const inventory = document.querySelector('#inventory'); + inventory.replaceChildren(...(world.inventory ?? []).map((item) => { + const entry = document.createElement('li'); + entry.textContent = `${item.name}: ${item.quantity}`; + return entry; + })); + const shop = document.querySelector('#shop'); + shop.hidden = !world.offer; + if (world.offer) { + document.querySelector('#offer-description').textContent = `${world.offer.name} — ${world.offer.price_sek.toLocaleString('sv-SE')} kr`; + document.querySelector('#buy-offer').disabled = (world.balance_sek ?? 0) < world.offer.price_sek; + document.querySelector('#offer-affordability').textContent = (world.balance_sek ?? 0) < world.offer.price_sek ? 'Du har inte tillräckligt saldo för det här köpet.' : ''; + document.querySelector('#buy-offer').setAttribute('aria-label', `Köp ${world.offer.name} för ${world.offer.price_sek} kronor`); + } renderConversation(await request('/api/npc/conversation'), world.npc.name); } document.querySelector('#auth-form').addEventListener('submit', async (event) => { @@ -148,7 +177,7 @@ try { const proposal = await request('/api/world/draft', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({idea:data.get('idea')})}); const form = document.querySelector('#world-draft-form'); - for (const key of ['name','location_name','location_description','npc_name','npc_biography']) form.elements[key].value = proposal[key]; + for (const key of ['name','location_name','location_description','npc_name','npc_biography','starting_balance_sek','offer_name','offer_price_sek']) form.elements[key].value = proposal[key]; form.hidden = false; status.textContent='Förslaget är klart. Granska och redigera det innan du godkänner.'; } catch(error) { status.textContent=`Kunde inte skapa ett förslag: ${error.message}`; } }); @@ -158,7 +187,10 @@ document.querySelector('#world-draft-form').addEventListener('submit', async (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); status.textContent='Sparar din godkända värld…'; try { - await request('/api/world', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(data.entries()))}); + const approval = Object.fromEntries(data.entries()); + approval.starting_balance_sek = Number(approval.starting_balance_sek); + approval.offer_price_sek = Number(approval.offer_price_sek); + await request('/api/world', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(approval)}); await openGame(); status.textContent='Din värld har sparats.'; } catch(error) { status.textContent=`Kunde inte spara världen: ${error.message}`; } }); @@ -168,6 +200,27 @@ try { const result=await request('/api/npc/messages',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({message,idempotency_key:crypto.randomUUID()})}); field.value=''; await openGame(); status.textContent='Samtalet har sparats.'; } catch(error){status.textContent=`Samtalet kunde inte skickas: ${error.message}`;} }); + document.querySelector('#buy-offer').addEventListener('click', async () => { + if (!gameState?.world?.offer) return; + const world = gameState.world; + const storageKey = `pending-purchase:${world.id}:${world.offer.id}`; + const idempotencyKey = sessionStorage.getItem(storageKey) || crypto.randomUUID(); + sessionStorage.setItem(storageKey, idempotencyKey); + const button = document.querySelector('#buy-offer'); + button.disabled = true; + status.textContent = 'Genomför köpet…'; + try { + await request('/api/purchases', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({offer_id:world.offer.id,expected_world_version:world.version,idempotency_key:idempotencyKey})}); + sessionStorage.removeItem(storageKey); + } catch(error) { + button.disabled = false; + status.textContent = `Köpet kunde inte genomföras: ${error.message}`; + return; + } + try { + await openGame(); status.textContent = 'Köpet är sparat i din värld.'; + } catch(error) { status.textContent = 'Köpet är sparat. Världen kunde inte läsas in just nu; ladda om sidan för att visa det.'; } + }); document.querySelector('#logout').addEventListener('click', async () => { try { await request('/api/logout',{method:'POST'}); showAuth(); status.textContent='Du har loggat ut.'; } catch(error){status.textContent=`Kunde inte logga ut: ${error.message}`;} }); request('/api/me').then(async () => { await openGame(); status.textContent='Din sparade spelvärld har lästs in.'; }).catch(() => { showAuth(); }); diff --git a/scripts/check-frontend.sh b/scripts/check-frontend.sh index e81c171..b4bc54e 100755 --- a/scripts/check-frontend.sh +++ b/scripts/check-frontend.sh @@ -8,6 +8,8 @@ grep -q 'id="frontend-version">__FRONTEND_VERSION__' index.html grep -q "request('/version')" index.html grep -q 'id="api-version"' index.html grep -Fq "credentials: 'same-origin'" index.html +grep -q 'Köp på platsen' index.html +grep -q "request('/api/purchases'" index.html grep -q 'location = /healthz' nginx/default.conf echo 'Frontend source checks passed.'