// 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 => (
))}
>
) : (
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]}