// UploadGate — first screen the user sees when entering the app. // Asks them to "upload" 3 taxonomy books with specific names. The files are // validated only by filename pattern, NEVER read or stored. Once all 3 are // recognized, we simulate analysis for ~3s, then unlock the app. const { useState, useRef, useEffect } = React; const SLOTS = [ { id: 'dx', title: 'Taxonomía de diagnósticos', hint: 'El archivo debe llamarse "taxonomía nanda"', match: /(taxonom[ií]a[\s_-]*nanda|^nanda)/i, tone: 'dx', }, { id: 'ob', title: 'Taxonomía de objetivos', hint: 'El archivo debe llamarse "taxonomía noc"', match: /(taxonom[ií]a[\s_-]*noc|^noc)/i, tone: 'ob', }, { id: 'iv', title: 'Taxonomía de intervenciones', hint: 'El archivo debe llamarse "taxonomía nic"', match: /(taxonom[ií]a[\s_-]*nic|^nic)/i, tone: 'iv', }, ]; const UploadSlot = ({ slot, state, onAccept, onReject }) => { const inputRef = useRef(null); const [dragOver, setDragOver] = useState(false); const handleFile = (file) => { if (!file) return; // We NEVER read the contents. Filename match is the only check. if (slot.match.test(file.name)) { onAccept(slot.id, file.name); } else { onReject(slot.id, `"${file.name}" no coincide con el nombre esperado.`); } // Discard the file reference immediately. }; const onDrop = (e) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files?.[0]; handleFile(f); }; return (
{slot.title}
{state.status === 'accepted' ? (
{state.filename}
Listo · validado
) : ( )}
); }; const GATE_KEY = 'cynamoonurs_books_uploaded'; const isGateAlreadyPassed = () => { try { return localStorage.getItem(GATE_KEY) === '1'; } catch { return false; } }; const markGatePassed = () => { try { localStorage.setItem(GATE_KEY, '1'); } catch {} }; const resetGate = () => { try { localStorage.removeItem(GATE_KEY); } catch {} }; const UploadGate = ({ onUnlock }) => { const [slots, setSlots] = useState({ dx: { status: 'idle' }, ob: { status: 'idle' }, iv: { status: 'idle' }, }); const [analyzing, setAnalyzing] = useState(false); const [progress, setProgress] = useState(0); const [analyzeStep, setAnalyzeStep] = useState(0); // If gate has been passed in a previous session, skip directly. useEffect(() => { if (isGateAlreadyPassed()) { loadAllData().then(() => onUnlock()); } }, []); const accept = (id, filename) => { setSlots(s => ({ ...s, [id]: { status: 'accepted', filename } })); }; const reject = (id, error) => { setSlots(s => ({ ...s, [id]: { status: 'error', error } })); // Auto-clear error after 4s setTimeout(() => setSlots(s => s[id]?.status === 'error' ? ({ ...s, [id]: { status: 'idle' } }) : s), 4000); }; const allReady = ['dx', 'ob', 'iv'].every(k => slots[k].status === 'accepted'); // Once all 3 are accepted, auto-start the simulated analysis. useEffect(() => { if (allReady && !analyzing) { setAnalyzing(true); // Also kick off the real data load in parallel. loadAllData(); } }, [allReady]); useEffect(() => { if (!analyzing) return; const steps = [ 'Validando integridad de los libros…', 'Indexando diagnósticos de enfermería…', 'Indexando objetivos de enfermería…', 'Indexando intervenciones de enfermería…', 'Cruzando relaciones entre taxonomías…', 'Eliminando los archivos del dispositivo…', 'Todo listo.', ]; let p = 0; let s = 0; const id = setInterval(() => { p = Math.min(100, p + 4 + Math.random() * 5); setProgress(p); const nextStep = Math.min(steps.length - 1, Math.floor((p / 100) * steps.length)); if (nextStep !== s) { s = nextStep; setAnalyzeStep(s); } if (p >= 100) { clearInterval(id); markGatePassed(); setTimeout(() => onUnlock(), 700); } }, 180); return () => clearInterval(id); }, [analyzing]); const totalAccepted = ['dx', 'ob', 'iv'].filter(k => slots[k].status === 'accepted').length; return (
Cynamonurs
Compañera de planes de cuidado
{analyzing ? 'Paso 2 de 2 · Analizando' : `Paso 1 de 2 · ${totalAccepted}/3 libros`}
{!analyzing ? ( <>

Sube los libros aquí para poder empezar a organizar tu información.

Cynamonurs es un organizador local: no almacenamos contenido de las taxonomías. Sube los tres libros de referencia en cualquier formato. Solo validamos el nombre del archivo como llave de acceso; el contenido no se guarda y los archivos se eliminan de inmediato.

{SLOTS.map(s => ( ))}
Tus archivos nunca salen de tu dispositivo. No se leen ni se conservan.
) : (

Estamos preparando tu biblioteca…

{[ 'Validando integridad de los libros…', 'Indexando diagnósticos de enfermería…', 'Indexando objetivos de enfermería…', 'Indexando intervenciones de enfermería…', 'Cruzando relaciones entre taxonomías…', 'Eliminando los archivos del dispositivo…', 'Todo listo.' ][analyzeStep]}

{Math.floor(progress)}%
)}
); }; Object.assign(window, { UploadGate, resetGate, isGateAlreadyPassed, markGatePassed });