feat: reemplazar todos los alerts y confirms del navegador por notificaciones y modales internos in-app

This commit is contained in:
urieljareth
2026-08-13 17:51:46 -06:00
parent 72416e46ee
commit ec31f94a21
4 changed files with 238 additions and 50 deletions
+53 -46
View File
@@ -1,36 +1,28 @@
import React, { useState } from 'react';
import { KeyRound, Trash2, Copy, Check, Download, AlertTriangle, ShieldCheck, X } from 'lucide-react';
import { ApiService } from '../services/api';
import { ConfirmModal } from './ConfirmModal';
interface AdminPanelProps {
rawVotes: Record<string, string>;
onVoteResetSuccess: () => Promise<void>;
isOpen?: boolean;
onClose?: () => void;
isOpen: boolean;
onClose: () => void;
}
export const AdminPanel: React.FC<AdminPanelProps> = ({
rawVotes,
onVoteResetSuccess,
isOpen: controlledOpen,
isOpen,
onClose,
}) => {
const [internalOpen, setInternalOpen] = useState(false);
const [selectedVoter, setSelectedVoter] = useState('');
const [password, setPassword] = useState('');
const [adminMsg, setAdminMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [copiedCode, setCopiedCode] = useState(false);
const [copiedSingleHtml, setCopiedSingleHtml] = useState(false);
const isPanelOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
const togglePanel = () => {
if (onClose && controlledOpen !== undefined) {
onClose();
} else {
setInternalOpen(!internalOpen);
}
};
const [showConfirmResetAll, setShowConfirmResetAll] = useState(false);
const registeredVoters = Object.keys(rawVotes).sort();
@@ -68,17 +60,19 @@ export const AdminPanel: React.FC<AdminPanelProps> = ({
}
};
const handleResetAllVotes = async () => {
const handlePromptResetAll = () => {
const trimmedPass = password.trim();
if (!trimmedPass) {
setAdminMsg({ text: 'Ingresa la contraseña de autorización para vaciar todos los votos.', type: 'error' });
return;
}
setAdminMsg(null);
setShowConfirmResetAll(true);
};
if (!window.confirm('¿Confirmas que deseas eliminar TODOS los votos para reiniciar pruebas?')) {
return;
}
const handleExecuteResetAll = async () => {
setShowConfirmResetAll(false);
const trimmedPass = password.trim();
setIsDeleting(true);
setAdminMsg(null);
@@ -246,19 +240,34 @@ const VOTOS_EMBEBIDOS = ${JSON.stringify(rawVotes, null, 2)};`;
} catch (e) { console.error(e); }
}
function showNotify(msg, isErr) {
let el = document.getElementById('standaloneNotify');
if (!el) {
el = document.createElement('div');
el.id = 'standaloneNotify';
el.style.cssText = 'position:fixed;bottom:20px;right:20px;padding:12px 18px;border-radius:12px;font-size:13px;font-weight:600;z-index:9999;transition:all 0.3s;box-shadow:0 10px 30px rgba(0,0,0,0.8);backdrop-filter:blur(10px);';
document.body.appendChild(el);
}
el.style.background = isErr ? 'rgba(239,68,68,0.9)' : 'rgba(0,183,212,0.95)';
el.style.color = isErr ? '#fff' : '#000';
el.innerText = msg;
el.style.display = 'block';
setTimeout(() => { if (el) el.style.display = 'none'; }, 4000);
}
async function submitVote() {
const voter = state.currentVoter.trim().toUpperCase();
const cand = state.selected || state.votes[voter];
if (!voter || voter.length < 2) return alert("Escribe tu nombre antes de votar.");
if (!cand) return alert("Selecciona un candidato.");
if (voter === cand || voter.split(/\\s+/).includes(cand)) return alert("No puedes votar por ti mismo.");
if (!voter || voter.length < 2) return showNotify("Escribe tu nombre antes de votar.", true);
if (!cand) return showNotify("Selecciona un candidato.", true);
if (voter === cand || voter.split(/\\s+/).includes(cand)) return showNotify("No puedes votar por ti mismo.", true);
try {
await callRedis(["HSET", CONFIG.key, voter, cand]);
state.votes[voter] = cand;
localStorage.setItem('e3_voter_app', voter);
render();
alert("¡Voto registrado en la nube con éxito!");
} catch (e) { alert("Error: " + e.message); }
showNotify("¡Voto registrado en la nube con éxito!", false);
} catch (e) { showNotify("Error: " + e.message, true); }
}
function render() {
@@ -331,33 +340,20 @@ const VOTOS_EMBEBIDOS = ${JSON.stringify(rawVotes, null, 2)};`;
URL.revokeObjectURL(url);
};
if (!isOpen) return null;
return (
<div className="w-full mt-6">
{/* Accordion / Toggle trigger if not controlled externally */}
{controlledOpen === undefined && (
<div className="flex justify-end mb-3">
<div className="glass-panel p-6 border border-white/10 shadow-2xl relative animate-in fade-in zoom-in-95 duration-200">
{onClose && (
<button
type="button"
onClick={togglePanel}
className="text-xs font-mono uppercase tracking-widest text-gray-500 hover:text-[#00B7D4] transition-colors flex items-center gap-1.5 cursor-pointer"
onClick={onClose}
className="absolute top-5 right-5 text-gray-400 hover:text-white p-1 cursor-pointer"
>
<KeyRound className="w-3.5 h-3.5" />
<span>{isPanelOpen ? 'Cerrar Panel Admin' : 'Panel de Administración'}</span>
<X className="w-4 h-4" />
</button>
</div>
)}
{isPanelOpen && (
<div className="glass-panel p-6 border border-white/10 shadow-2xl relative animate-in fade-in zoom-in-95 duration-200">
{onClose && (
<button
type="button"
onClick={onClose}
className="absolute top-5 right-5 text-gray-400 hover:text-white p-1 cursor-pointer"
>
<X className="w-4 h-4" />
</button>
)}
)}
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 rounded-lg bg-[#00B7D4]/10 border border-[#00B7D4]/30 flex items-center justify-center text-[#00B7D4]">
@@ -418,7 +414,7 @@ const VOTOS_EMBEBIDOS = ${JSON.stringify(rawVotes, null, 2)};`;
<button
type="button"
onClick={handleResetAllVotes}
onClick={handlePromptResetAll}
disabled={isDeleting || registeredVoters.length === 0}
title="Vaciar todos los votos registrados para pruebas"
className="px-3 py-2.5 bg-amber-950/40 hover:bg-amber-900/60 border border-amber-500/40 text-amber-300 rounded-xl text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer disabled:opacity-30 flex items-center justify-center gap-1"
@@ -482,7 +478,18 @@ const VOTOS_EMBEBIDOS = ${JSON.stringify(rawVotes, null, 2)};`;
</div>
</div>
</div>
)}
{/* Modal Interno de Confirmación */}
<ConfirmModal
isOpen={showConfirmResetAll}
title="¿Vaciar Todos los Votos?"
message="Esta acción eliminará todos los votos registrados en PostgreSQL para reiniciar las pruebas. No se puede deshacer."
confirmLabel="Sí, Vaciar Todo"
cancelLabel="Cancelar"
isDestructive={true}
onConfirm={handleExecuteResetAll}
onCancel={() => setShowConfirmResetAll(false)}
/>
</div>
);
};