a0da1bf420
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
228 lines
10 KiB
HTML
228 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Mantenedor — Cantina{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="admin-container">
|
|
<div class="admin-header">
|
|
<h1>Mantenedor</h1>
|
|
<div style="display:flex;gap:.5rem">
|
|
<a href="/admin/voting" class="btn-sm">🗳 Votación</a>
|
|
<a href="/auth/logout" class="btn-sm btn-danger"
|
|
onclick="return confirm('¿Desconectar la cuenta de Spotify?')">⏏ Cuenta Spotify</a>
|
|
<a href="/admin/logout" class="btn-sm btn-danger">Cerrar sesión</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if error %}
|
|
<div class="alert alert-error">{{ error }}</div>
|
|
{% endif %}
|
|
{% if success %}
|
|
<div class="alert alert-success">{{ success }}</div>
|
|
{% endif %}
|
|
|
|
<!-- Formulario para agregar -->
|
|
<div class="card">
|
|
<h2>Agregar elemento</h2>
|
|
<form method="post" action="/admin/playlists" class="add-form">
|
|
<div class="form-row">
|
|
<input
|
|
type="text"
|
|
name="spotify_url"
|
|
class="input"
|
|
placeholder="URL de Spotify (canción, álbum, artista o playlist)"
|
|
required
|
|
>
|
|
<button type="submit" class="btn-primary">Agregar</button>
|
|
</div>
|
|
<small class="hint">
|
|
Acepta cualquier URL de <strong>open.spotify.com</strong>,
|
|
URI (<code>spotify:track:…</code>, <code>spotify:album:…</code>, etc.)
|
|
o ID de 22 caracteres (se asume playlist).
|
|
</small>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Lista -->
|
|
<div class="card">
|
|
<h2>Elementos configurados ({{ playlists | length }})</h2>
|
|
{% if playlists %}
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Nombre</th>
|
|
<th>Tipo</th>
|
|
<th>Descripción</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for pl in playlists %}
|
|
{% set type_info = type_labels.get(pl.spotify_type, ('🎵', pl.spotify_type)) %}
|
|
<tr>
|
|
<td>
|
|
{% if pl.emoji %}
|
|
<div class="table-thumb-placeholder emoji-thumb">{{ pl.emoji }}</div>
|
|
{% elif pl.image_url %}
|
|
<img src="{{ pl.image_url }}" alt="" class="table-thumb">
|
|
{% else %}
|
|
<div class="table-thumb-placeholder">{{ type_info[0] }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="td-name">{{ pl.name }}</td>
|
|
<td>
|
|
<span class="type-badge type-{{ pl.spotify_type }}">
|
|
{{ type_info[0] }} {{ type_info[1] }}
|
|
</span>
|
|
</td>
|
|
<td class="td-desc">{{ pl.description[:80] }}{% if pl.description | length > 80 %}…{% endif %}</td>
|
|
<td class="td-actions">
|
|
<button class="btn-sm btn-play-now" id="play-btn-{{ pl.id }}"
|
|
onclick="playNow('{{ pl.spotify_id }}', '{{ pl.spotify_type }}', {{ pl.id }})">▶ Reproducir</button>
|
|
<button class="btn-sm" onclick="toggleEdit({{ pl.id }})">✏️ Editar</button>
|
|
<form method="post" action="/admin/playlists/{{ pl.id }}/delete"
|
|
onsubmit="return confirm('¿Eliminar {{ pl.name }}?')">
|
|
<button type="submit" class="btn-sm btn-danger">Eliminar</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<!-- Fila de edición inline -->
|
|
<tr id="edit-row-{{ pl.id }}" class="edit-row" style="display:none">
|
|
<td colspan="5">
|
|
<form method="post" action="/admin/playlists/{{ pl.id }}/edit" class="edit-form">
|
|
<div class="edit-fields">
|
|
<div class="edit-field">
|
|
<label>Nombre</label>
|
|
<input type="text" name="name" class="input"
|
|
value="{{ pl.name }}" required maxlength="200">
|
|
</div>
|
|
<div class="edit-field edit-field-desc">
|
|
<label>Descripción</label>
|
|
<input type="text" name="description" class="input"
|
|
value="{{ pl.description }}" maxlength="300">
|
|
</div>
|
|
<div class="edit-field edit-field-emoji">
|
|
<label>Emoticono <span class="hint-inline">(reemplaza imagen)</span></label>
|
|
<div class="emoji-input-wrap">
|
|
<input type="text" name="emoji" id="emoji-{{ pl.id }}" class="input emoji-input"
|
|
value="{{ pl.emoji }}" maxlength="8" placeholder="😀">
|
|
{% if pl.emoji %}
|
|
<button type="button" class="btn-sm emoji-clear"
|
|
onclick="clearEmoji({{ pl.id }})">✕</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="emoji-picker">
|
|
{% for e in ['🎵','🎶','🎸','🎹','🎺','🎻','🥁','🎤','🎧','🎼','🎷','🪗','🪘','🔥','⚡','🌟','✨','💫','🌈','🎉','🥳','🍕','🍺','🍹','🌙','☀️','🏖️','🌊','💃','🕺','🎭','🎬','🎮','🏆','❤️','🤘'] %}
|
|
<button type="button" class="emoji-opt"
|
|
onclick="pickEmoji({{ pl.id }}, '{{ e }}')">{{ e }}</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="edit-actions">
|
|
<button type="submit" class="btn-primary btn-sm">Guardar</button>
|
|
<button type="button" class="btn-sm" onclick="toggleEdit({{ pl.id }})">Cancelar</button>
|
|
</div>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="empty-msg">No hay elementos configurados todavía.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% include "admin/_system_buttons.html" %}
|
|
</div>
|
|
|
|
<script>
|
|
async function playNow(spotifyId, spotifyType, plId) {
|
|
const btn = document.getElementById('play-btn-' + plId);
|
|
btn.disabled = true;
|
|
btn.textContent = '⏳';
|
|
|
|
const body = spotifyType === 'track'
|
|
? { uris: [`spotify:track:${spotifyId}`] }
|
|
: { context_uri: `spotify:${spotifyType}:${spotifyId}` };
|
|
|
|
try {
|
|
const res = await fetch('/player/play', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
btn.textContent = res.ok ? '✓ Reproduciendo' : '✗ Error';
|
|
} catch (_) {
|
|
btn.textContent = '✗ Error';
|
|
}
|
|
|
|
setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Reproducir'; }, 2500);
|
|
}
|
|
|
|
function toggleEdit(id) {
|
|
const row = document.getElementById('edit-row-' + id);
|
|
const visible = row.style.display !== 'none';
|
|
row.style.display = visible ? 'none' : 'table-row';
|
|
if (!visible) row.querySelector('input[name="name"]').focus();
|
|
}
|
|
function pickEmoji(id, emoji) {
|
|
document.getElementById('emoji-' + id).value = emoji;
|
|
}
|
|
function clearEmoji(id) {
|
|
document.getElementById('emoji-' + id).value = '';
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.td-actions { display: flex; gap: .4rem; align-items: center; }
|
|
.btn-play-now { color: var(--green); border-color: rgba(29,185,84,.4); }
|
|
.btn-play-now:hover { background: rgba(29,185,84,.12); }
|
|
|
|
.edit-row td { background: var(--surface2); padding: .75rem 1rem; }
|
|
.edit-form { display: flex; flex-direction: column; gap: .75rem; }
|
|
.edit-fields { display: flex; gap: .6rem; flex-wrap: wrap; align-items: flex-start; }
|
|
.edit-field { display: flex; flex-direction: column; gap: .25rem; min-width: 140px; }
|
|
.edit-field-desc { flex: 2; }
|
|
.edit-field-emoji { min-width: 180px; }
|
|
.edit-field label { font-size: .75rem; color: var(--text-muted); }
|
|
.hint-inline { font-weight: 400; opacity: .6; }
|
|
.edit-actions { display: flex; gap: .4rem; align-items: center; }
|
|
|
|
.emoji-input-wrap { display: flex; gap: .35rem; align-items: center; }
|
|
.emoji-input { width: 70px; font-size: 1.2rem; text-align: center; padding: .3rem .4rem; }
|
|
.emoji-clear { padding: .3rem .5rem; line-height: 1; }
|
|
|
|
.emoji-picker { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .35rem; }
|
|
.emoji-opt {
|
|
background: var(--surface);
|
|
border: 1px solid #333;
|
|
border-radius: 6px;
|
|
font-size: 1.15rem;
|
|
width: 34px; height: 34px;
|
|
cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: background .12s, transform .1s;
|
|
}
|
|
.emoji-opt:hover { background: #333; transform: scale(1.15); }
|
|
|
|
.emoji-thumb { font-size: 1.6rem; }
|
|
|
|
.type-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .3rem;
|
|
font-size: .75rem;
|
|
padding: .2rem .55rem;
|
|
border-radius: 20px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
.type-playlist { background: rgba(29,185,84,.12); color: #1db954; border: 1px solid rgba(29,185,84,.3); }
|
|
.type-album { background: rgba(168,85,247,.12); color: #a855f7; border: 1px solid rgba(168,85,247,.3); }
|
|
.type-artist { background: rgba(59,130,246,.12); color: #3b82f6; border: 1px solid rgba(59,130,246,.3); }
|
|
.type-track { background: rgba(245,158,11,.12); color: #f59e0b; border: 1px solid rgba(245,158,11,.3); }
|
|
</style>
|
|
{% endblock %}
|