feat: add boilerplate

This commit is contained in:
2026-02-16 00:12:46 +01:00
parent a0e9786c07
commit 0691c8e04e
2 changed files with 430 additions and 0 deletions

319
webapp/index.html Normal file
View File

@@ -0,0 +1,319 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AnA Character Sheet</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=IM+Fell+English&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
</head>
<body>
<header>
<div class="title">
<i class="fa-solid fa-dragon"></i>
<span>AnA Character Sheet</span>
<span id="lastSaved" class="last-saved">Last saved: —</span>
</div>
<div>
<select id="characterSelect"></select>
<button onclick="newCharacter()" title="New"><i class="fa-solid fa-user-plus"></i></button>
<button onclick="deleteCharacter()" title="Delete"><i class="fa-solid fa-skull-crossbones"></i></button>
<button onclick="exportCharacter()" title="Export"><i class="fa-solid fa-scroll"></i></button>
<button onclick="importCharacter()" title="Import"><i class="fa-solid fa-file-import"></i></button>
</div>
</header>
<div class="container">
<!-- CHARACTER INFO -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-id-card section-icon"></i>
Character Info
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div class="collapsible-content">
<div class="grid">
<div class="field"><label>Name</label><input id="name"></div>
<div class="field"><label>Race</label><input id="race"></div>
<div class="field"><label>Class</label><input id="class"></div>
<div class="field"><label>Background</label><input id="background"></div>
<div class="field"><label>Alignment</label><input id="alignment"></div>
<div class="field"><label>Proficiency Bonus</label><input id="proficiency" type="number" value="2"></div>
</div>
</div>
</div>
<!-- ABILITIES -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-dumbbell section-icon"></i>
Ability Scores
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div id="abilities" class="collapsible-content ability-grid"></div>
</div>
<!-- COMBAT -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-swords section-icon"></i>
Combat
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div class="collapsible-content grid">
<div class="field"><label>Max HP</label><input id="hp_max" type="number"></div>
<div class="field"><label>Current HP</label><input id="hp_current" type="number"></div>
<div class="field"><label>Temp HP</label><input id="hp_temp" type="number"></div>
<div class="field"><label>Hit Dice</label><input id="hit_dice"></div>
</div>
</div>
<!-- ACTIONS -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-bolt section-icon"></i>
Actions & Abilities
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div class="collapsible-content">
<div id="actionsList"></div>
<button onclick="addAction()">Add Ability</button>
</div>
</div>
<!-- INVENTORY -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-backpack section-icon"></i>
Inventory & Currency
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div class="collapsible-content">
<label>Inventory</label>
<textarea id="inventory"></textarea>
<div class="grid">
<div class="field"><label>Gold</label><input id="gp" type="number"></div>
<div class="field"><label>Silver</label><input id="sp" type="number"></div>
<div class="field"><label>Copper</label><input id="cp" type="number"></div>
</div>
</div>
</div>
<!-- SPELLS -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-hat-wizard section-icon"></i>
Spells
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div class="collapsible-content">
<label>Spell List</label>
<textarea id="spells"></textarea>
<div class="field"><label>Spell Slots</label><input id="spell_slots"></div>
</div>
</div>
<!-- NOTES -->
<div class="section">
<h2 onclick="toggleSection(this)">
<i class="fa-solid fa-scroll section-icon"></i>
Notes
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</h2>
<div class="collapsible-content">
<textarea id="notes"></textarea>
</div>
</div>
</div>
<script>
const abilities=[
{short:"STR",full:"Strength"},
{short:"DEX",full:"Dexterity"},
{short:"CON",full:"Constitution"},
{short:"INT",full:"Intelligence"},
{short:"WIS",full:"Wisdom"},
{short:"CHA",full:"Charisma"}
];
let currentCharacter=null;
let autosaveTimer=null;
/* SECTION TOGGLE (REAL) */
function toggleSection(header){
const content = header.nextElementSibling;
const icon = header.querySelector(".toggle-icon");
const isHidden = content.style.display === "none";
content.style.display = isHidden ? "block" : "none";
icon.style.transform = isHidden ? "rotate(0deg)" : "rotate(-90deg)";
}
/* ABILITIES */
function initAbilities(){
const c=document.getElementById("abilities");
c.innerHTML="";
abilities.forEach(s=>{
c.innerHTML+=`
<div class="ability-card">
<div class="ability-name">${s.full}</div>
<div class="ability-short">${s.short}</div>
<input type="number" id="${s.short}" value="10">
<div class="modifier" id="${s.short}_mod">+0</div>
</div>`;
});
}
function calculate(){
abilities.forEach(s=>{
const v=parseInt(document.getElementById(s.short).value)||10;
const m=Math.floor((v-10)/2);
document.getElementById(s.short+"_mod").innerText=m>=0?`+${m}`:m;
});
}
/* ACTIONS */
function addAction(data=null){
const c=document.getElementById("actionsList");
const d=document.createElement("div");
d.className="action-item";
d.innerHTML=`
<label>Name</label><input class="action-name" value="${data?.name||""}">
<label>Type</label>
<select class="action-type">
<option>Action</option><option>Bonus Action</option>
<option>Reaction</option><option>Passive</option>
</select>
<label>Attack Bonus</label><input class="action-bonus" value="${data?.bonus||""}">
<label>Damage</label><input class="action-damage" value="${data?.damage||""}">
<label>Description</label><textarea class="action-desc">${data?.desc||""}</textarea>
<button onclick="confirmRemoveAction(this)">
<i class="fa-solid fa-trash-can"></i>
</button>`;
c.appendChild(d);
}
function confirmRemoveAction(btn){
if(confirm("Remove this ability?")){
btn.parentElement.remove();
saveCharacter();
}
}
/* SAVE / LOAD */
function saveCharacter(){
if(!currentCharacter) return;
const data={};
document.querySelectorAll("input[id], textarea[id]").forEach(e=>data[e.id]=e.value);
data.actions=[];
document.querySelectorAll(".action-item").forEach(i=>{
data.actions.push({
name:i.querySelector(".action-name").value,
type:i.querySelector(".action-type").value,
bonus:i.querySelector(".action-bonus").value,
damage:i.querySelector(".action-damage").value,
desc:i.querySelector(".action-desc").value
});
});
const now=new Date().toLocaleString();
data._lastSaved=now;
localStorage.setItem("char_"+currentCharacter,JSON.stringify(data));
document.getElementById("lastSaved").innerText="Last saved: "+now;
}
function loadCharacter(n){
const d=JSON.parse(localStorage.getItem("char_"+n));
if(!d) return;
currentCharacter=n;
document.querySelectorAll("input[id], textarea[id]").forEach(e=>{
if(d[e.id]!=null) e.value=d[e.id];
});
document.getElementById("actionsList").innerHTML="";
d.actions?.forEach(a=>addAction(a));
document.getElementById("lastSaved").innerText="Last saved: "+(d._lastSaved||"—");
startAutosave();
calculate();
}
/* AUTOSAVE */
function startAutosave(){
clearInterval(autosaveTimer);
autosaveTimer=setInterval(()=>currentCharacter&&saveCharacter(),5000);
}
/* CHARACTER CRUD */
function newCharacter(){
const n=prompt("Character name?");
if(!n) return;
localStorage.setItem("char_"+n,"{}");
updateDropdown();
loadCharacter(n);
}
function deleteCharacter(){
if(!currentCharacter||!confirm("Delete this character permanently?")) return;
localStorage.removeItem("char_"+currentCharacter);
currentCharacter=null;
clearInterval(autosaveTimer);
document.getElementById("lastSaved").innerText="Last saved: —";
updateDropdown();
}
/* EXPORT / IMPORT */
function exportCharacter(){
if(!currentCharacter) return;
const data=localStorage.getItem("char_"+currentCharacter);
const blob=new Blob([data],{type:"application/json"});
const a=document.createElement("a");
a.href=URL.createObjectURL(blob);
a.download=currentCharacter+".json";
a.click();
}
function importCharacter(){
const input=document.createElement("input");
input.type="file";
input.accept=".json";
input.onchange=e=>{
const reader=new FileReader();
reader.onload=()=>{
const n=prompt("Name for imported character?");
if(!n) return;
localStorage.setItem("char_"+n,reader.result);
updateDropdown();
};
reader.readAsText(e.target.files[0]);
};
input.click();
}
/* DROPDOWN */
function updateDropdown(){
const s=document.getElementById("characterSelect");
s.innerHTML="";
for(let i=0;i<localStorage.length;i++){
const k=localStorage.key(i);
if(k.startsWith("char_")){
const o=document.createElement("option");
o.value=o.textContent=k.replace("char_","");
s.appendChild(o);
}
}
}
document.getElementById("characterSelect").onchange=e=>loadCharacter(e.target.value);
/* INIT */
initAbilities();
updateDropdown();
document.addEventListener("input",calculate);
</script>
</body>
</html>