commit inicial

This commit is contained in:
2026-04-23 00:39:58 -04:00
commit a1b9c0139d
32 changed files with 2836 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
{% extends "base.html" %}
{% block title %}Votación — Admin{% endblock %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h1>Control de Votación</h1>
<div style="display:flex;gap:.5rem">
<a href="/admin/playlists" class="btn-sm">Playlists</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 %}
<!-- Configuración de rango horario -->
<div class="card">
<h2>Configurar votación</h2>
<form method="post" action="/admin/voting" class="voting-form">
<div class="time-row">
<div class="time-field">
<label>Hora de inicio</label>
<input type="time" name="start_time" class="input"
value="{{ config.start_time }}" required>
</div>
<div class="time-sep"></div>
<div class="time-field">
<label>Hora de fin</label>
<input type="time" name="end_time" class="input"
value="{{ config.end_time }}" required>
</div>
</div>
<label class="toggle-label">
<input type="checkbox" name="is_active"
{% if config.is_active %}checked{% endif %}>
<span class="toggle-text">Votación activa</span>
<span class="toggle-hint">
{% if config.is_active %}
Los usuarios pueden votar dentro del rango horario
{% else %}
La votación está desactivada
{% endif %}
</span>
</label>
<button type="submit" class="btn-primary">Guardar configuración</button>
</form>
<div class="status-badge {% if config.is_active %}status-active{% else %}status-inactive{% endif %}">
{% if config.is_active %}
🟢 Activa · Ventana: {{ config.start_time }} {{ config.end_time }}
{% else %}
🔴 Inactiva
{% endif %}
</div>
</div>
<!-- Resultados -->
<div class="card">
<div class="results-header">
<h2>Resultados ({{ total_votes }} votos)</h2>
<div style="display:flex;gap:.5rem;align-items:center">
{% if results and results[0].votes > 0 %}
<form method="post" action="/admin/voting/play-winner">
<button type="submit" class="btn-primary btn-sm">
▶ Reproducir ganador
</button>
</form>
{% endif %}
<form method="post" action="/admin/voting/reset"
onsubmit="return confirm('¿Reiniciar todos los votos?')">
<button type="submit" class="btn-sm btn-danger">Reiniciar votos</button>
</form>
</div>
</div>
{% if results %}
<div class="results-list">
{% for item in results %}
{% set pct = (item.votes / total_votes * 100) | round(1) if total_votes > 0 else 0 %}
<div class="result-row {% if loop.first and item.votes > 0 %}result-winner{% endif %}">
<div class="result-rank">{{ loop.index }}</div>
{% if item.playlist.image_url %}
<img src="{{ item.playlist.image_url }}" alt="" class="table-thumb">
{% else %}
<div class="table-thumb-placeholder">🎵</div>
{% endif %}
<div class="result-info">
<div class="result-name">
{{ item.playlist.name }}
{% if loop.first and item.votes > 0 %}
<span class="winner-badge">🏆 Ganador</span>
{% endif %}
</div>
<div class="result-bar-wrap">
<div class="result-bar" style="width: {{ pct }}%"></div>
</div>
</div>
<div class="result-count">
<strong>{{ item.votes }}</strong>
<span class="pct-label">{{ pct }}%</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-msg">No hay playlists configuradas.</p>
{% endif %}
</div>
</div>
<style>
.voting-form { display: flex; flex-direction: column; gap: 1rem; }
.time-row { display: flex; align-items: flex-end; gap: 1rem; }
.time-field { display: flex; flex-direction: column; gap: .3rem; flex: 1; }
.time-field label { font-size: .8rem; color: var(--text-muted); }
.time-sep { padding-bottom: .6rem; color: var(--text-muted); }
.toggle-label { display: flex; align-items: center; gap: .6rem; cursor: pointer; }
.toggle-label input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--green); }
.toggle-text { font-weight: 600; font-size: .9rem; }
.toggle-hint { font-size: .8rem; color: var(--text-muted); }
.status-badge {
display: inline-flex;
padding: .4rem .8rem;
border-radius: 20px;
font-size: .82rem;
font-weight: 500;
align-self: flex-start;
}
.status-active { background: rgba(29,185,84,.15); color: var(--green); border: 1px solid rgba(29,185,84,.3); }
.status-inactive { background: rgba(136,136,136,.1); color: var(--text-muted); border: 1px solid #333; }
.results-header { display: flex; justify-content: space-between; align-items: center; }
.results-list { display: flex; flex-direction: column; gap: .5rem; margin-top: .25rem; }
.result-row {
display: flex;
align-items: center;
gap: .75rem;
padding: .6rem;
border-radius: 8px;
background: var(--surface2);
}
.result-winner { border: 1px solid rgba(29,185,84,.4); background: rgba(29,185,84,.06); }
.result-rank { width: 20px; text-align: center; font-size: .82rem; color: var(--text-muted); font-weight: 600; }
.result-info { flex: 1; display: flex; flex-direction: column; gap: .3rem; }
.result-name { font-size: .9rem; font-weight: 500; display: flex; align-items: center; gap: .5rem; }
.winner-badge { background: rgba(255,215,0,.15); color: gold; font-size: .75rem; padding: 1px 6px; border-radius: 10px; border: 1px solid rgba(255,215,0,.3); }
.result-bar-wrap { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
.result-bar { height: 100%; background: var(--green); border-radius: 3px; transition: width .4s; }
.result-count { display: flex; flex-direction: column; align-items: flex-end; min-width: 50px; }
.result-count strong { font-size: 1.1rem; }
.pct-label { font-size: .75rem; color: var(--text-muted); }
</style>
{% endblock %}