feat: add personajes page with level tracking and fix timer reactivity
CI / Build Native (push) Failing after 58s
CI / Build Native (push) Failing after 58s
- New /personajes page with CRUD for characters (name, class, level) - Level editable inline, deletable (blocked if has active sales) - ventas.html now loads characters list as datalist suggestions - Fix timer: add reactive 'now' property so countdown updates - Fix script loading: add defaults.js + app.js to ventas/personajes - Fix table auto-reload: use loadCharacters()/loadSales() after mutations - Timer now uses setTimeout recursion + visibilitychange handler
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
<!doctype html>
|
||||
<html lang="es" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Personajes — Calculadora de Craft de Shots</title>
|
||||
<link rel="stylesheet" href="/static/css/pico.min.css"/>
|
||||
<link rel="stylesheet" href="/static/css/app.css"/>
|
||||
</head>
|
||||
<body x-data="appShell()">
|
||||
|
||||
{#include partials/header.html /}
|
||||
{#include partials/tab-bar.html /}
|
||||
|
||||
<main class="container" x-data="personajesSection()">
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2>Agregar personaje</h2>
|
||||
</header>
|
||||
<form @submit.prevent="submitCharacter()">
|
||||
<div class="grid" style="grid-template-columns: 1fr 1fr 100px;">
|
||||
<label>
|
||||
<span class="text-sm text-muted">Nombre</span>
|
||||
<input type="text"
|
||||
x-model="form.name"
|
||||
maxlength="50"
|
||||
required
|
||||
placeholder="p.ej. DarkMage"/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="text-sm text-muted">Clase</span>
|
||||
<input type="text"
|
||||
x-model="form.characterClass"
|
||||
maxlength="30"
|
||||
required
|
||||
placeholder="p.ej. DC, HE, SWS, EE"/>
|
||||
</label>
|
||||
<label>
|
||||
<span class="text-sm text-muted">Nivel</span>
|
||||
<input type="number"
|
||||
x-model.number="form.level"
|
||||
min="1"
|
||||
max="100"
|
||||
required
|
||||
placeholder="85"/>
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<button type="submit"
|
||||
class="primary"
|
||||
:disabled="submitting">
|
||||
<span x-show="!submitting">Agregar</span>
|
||||
<span x-show="submitting">Guardando…</span>
|
||||
</button>
|
||||
</div>
|
||||
<p x-show="error" class="text-error text-sm" x-text="error"></p>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2 style="margin: 0;">Mis personajes</h2>
|
||||
<p class="text-sm text-muted" x-text="characters.length + ' personaje' + (characters.length === 1 ? '' : 's')"></p>
|
||||
</header>
|
||||
|
||||
<div x-show="loading && characters.length === 0" class="text-center p-4">
|
||||
<p class="text-sm text-muted">Cargando…</p>
|
||||
</div>
|
||||
|
||||
<div x-show="characters.length === 0 && !loading" class="text-center p-4">
|
||||
<p class="text-sm text-muted">No hay personajes. Agregá uno arriba.</p>
|
||||
</div>
|
||||
|
||||
<div x-show="characters.length > 0" class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Clase</th>
|
||||
<th>Nivel</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="c in characters" :key="c.id">
|
||||
<tr>
|
||||
<td x-text="c.name"></td>
|
||||
<td x-text="c.characterClass"></td>
|
||||
<td>
|
||||
<div class="flex-row gap-1" style="align-items: center;">
|
||||
<input type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
x-model.number="c.level"
|
||||
@keydown.enter="updateLevel(c)"
|
||||
@blur="updateLevel(c)"
|
||||
style="width: 70px; padding: 0.25rem 0.5rem; font-size: 0.875rem;"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="secondary outline"
|
||||
style="font-size: 0.75rem; padding: 0.25rem 0.5rem;"
|
||||
@click="deleteCharacter(c.id, c.name)">
|
||||
Eliminar
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<p class="text-center text-xs text-muted">
|
||||
Hacé clic en el nivel para editarlo inline · Enter o perder foco guarda
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script src="/static/js/defaults.js"></script>
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script defer src="/static/js/alpine.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('personajesSection', () => ({
|
||||
characters: [],
|
||||
loading: true,
|
||||
submitting: false,
|
||||
error: null,
|
||||
|
||||
form: {
|
||||
name: '',
|
||||
characterClass: '',
|
||||
level: null,
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.loadCharacters();
|
||||
},
|
||||
|
||||
async loadCharacters() {
|
||||
try {
|
||||
this.characters = await window.api.listCharacters();
|
||||
} catch (e) {
|
||||
this.error = e.message || 'Error cargando personajes';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async submitCharacter() {
|
||||
if (!this.form.name.trim() || !this.form.characterClass.trim() || !this.form.level) {
|
||||
this.error = 'Completá todos los campos';
|
||||
return;
|
||||
}
|
||||
this.error = null;
|
||||
this.submitting = true;
|
||||
try {
|
||||
await window.api.createCharacter({
|
||||
name: this.form.name.trim(),
|
||||
characterClass: this.form.characterClass.trim(),
|
||||
level: this.form.level,
|
||||
});
|
||||
this.form.name = '';
|
||||
this.form.characterClass = '';
|
||||
this.form.level = null;
|
||||
await this.loadCharacters();
|
||||
} catch (e) {
|
||||
this.error = e.message || 'Error al guardar';
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
},
|
||||
|
||||
async updateLevel(c) {
|
||||
if (c.level <= 0 || c.level > 100) {
|
||||
c.level = Math.min(100, Math.max(1, c.level));
|
||||
}
|
||||
try {
|
||||
await window.api.updateCharacterLevel(c.id, c.level);
|
||||
} catch (e) {
|
||||
this.error = e.message || 'Error al actualizar nivel';
|
||||
}
|
||||
},
|
||||
|
||||
async deleteCharacter(id, name) {
|
||||
if (!confirm('Eliminar el personaje "' + name + '"?')) return;
|
||||
try {
|
||||
await window.api.deleteCharacter(id);
|
||||
await this.loadCharacters();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Error al eliminar personaje');
|
||||
}
|
||||
},
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user