feat: reemplazar todos los alerts y confirms del navegador por notificaciones y modales internos in-app
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user