This commit is contained in:
gpt-engineer-app[bot] 2025-12-13 11:55:24 +00:00
parent bebc3a2029
commit 69dacbe188
23 changed files with 2543 additions and 116 deletions

View file

@ -1,22 +1,24 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="es">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!-- TODO: Set the document title to the name of your application --> <title>EMERGES TES - Guía de Protocolos de Emergencias</title>
<title>Lovable App</title> <meta name="description" content="Guía rápida de protocolos médicos de emergencias para Técnicos de Emergencias Sanitarias (TES). RCP, fármacos, calculadoras y más." />
<meta name="description" content="Lovable Generated Project" /> <meta name="author" content="EMERGES TES" />
<meta name="author" content="Lovable" /> <meta name="theme-color" content="#1a1f2e" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- TODO: Update og:title to match your application name --> <meta property="og:title" content="EMERGES TES - Guía de Protocolos" />
<meta property="og:title" content="Lovable App" /> <meta property="og:description" content="Guía rápida de protocolos médicos de emergencias para TES" />
<meta property="og:description" content="Lovable Generated Project" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@Lovable" /> <meta name="twitter:title" content="EMERGES TES" />
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" /> <meta name="twitter:description" content="Protocolos de emergencias para TES" />
<link rel="canonical" href="https://emerges-tes.lovable.app/" />
</head> </head>
<body> <body>

View file

@ -1,27 +1,72 @@
import { useState } from 'react';
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index"; import Header from "@/components/layout/Header";
import BottomNav from "@/components/layout/BottomNav";
import SearchModal from "@/components/layout/SearchModal";
import MenuSheet from "@/components/layout/MenuSheet";
import Home from "./pages/Index";
import SoporteVital from "./pages/SoporteVital";
import Patologias from "./pages/Patologias";
import Escena from "./pages/Escena";
import Farmacos from "./pages/Farmacos";
import Herramientas from "./pages/Herramientas";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const App = () => ( const App = () => {
<QueryClientProvider client={queryClient}> const [isSearchOpen, setIsSearchOpen] = useState(false);
<TooltipProvider> const [isMenuOpen, setIsMenuOpen] = useState(false);
<Toaster />
<Sonner /> return (
<BrowserRouter> <QueryClientProvider client={queryClient}>
<Routes> <TooltipProvider>
<Route path="/" element={<Index />} /> <Toaster />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} <Sonner />
<Route path="*" element={<NotFound />} /> <BrowserRouter>
</Routes> <div className="min-h-screen bg-background">
</BrowserRouter> <Header
</TooltipProvider> onSearchClick={() => setIsSearchOpen(true)}
</QueryClientProvider> onMenuClick={() => setIsMenuOpen(true)}
); />
<main className="pt-14 pb-safe">
<div className="container max-w-2xl py-4">
<Routes>
<Route
path="/"
element={<Home onSearchClick={() => setIsSearchOpen(true)} />}
/>
<Route path="/soporte-vital" element={<SoporteVital />} />
<Route path="/patologias" element={<Patologias />} />
<Route path="/escena" element={<Escena />} />
<Route path="/farmacos" element={<Farmacos />} />
<Route path="/herramientas" element={<Herramientas />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</main>
<BottomNav />
<SearchModal
isOpen={isSearchOpen}
onClose={() => setIsSearchOpen(false)}
/>
<MenuSheet
isOpen={isMenuOpen}
onClose={() => setIsMenuOpen(false)}
/>
</div>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
);
};
export default App; export default App;

View file

@ -0,0 +1,173 @@
import { useState } from 'react';
import { ChevronDown, ChevronUp, Star, Package, Syringe, User, Baby, AlertCircle } from 'lucide-react';
import { Drug } from '@/data/drugs';
import Badge from '@/components/shared/Badge';
import { cn } from '@/lib/utils';
interface DrugCardProps {
drug: Drug;
defaultExpanded?: boolean;
}
const DrugCard = ({ drug, defaultExpanded = false }: DrugCardProps) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [isFavorite, setIsFavorite] = useState(false);
const toggleFavorite = (e: React.MouseEvent) => {
e.stopPropagation();
setIsFavorite(!isFavorite);
};
return (
<div className="card-procedure">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full text-left"
aria-expanded={isExpanded}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-2xl">💊</span>
<h3 className="font-bold text-foreground text-lg">
{drug.genericName.toUpperCase()}
</h3>
</div>
<p className="text-muted-foreground text-sm">({drug.tradeName})</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={toggleFavorite}
className={cn(
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
isFavorite ? 'text-warning' : 'text-muted-foreground hover:text-foreground'
)}
aria-label={isFavorite ? 'Quitar de favoritos' : 'Añadir a favoritos'}
>
<Star className={cn('w-5 h-5', isFavorite && 'fill-current')} />
</button>
<div className="w-10 h-10 flex items-center justify-center">
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-muted-foreground" />
) : (
<ChevronDown className="w-5 h-5 text-muted-foreground" />
)}
</div>
</div>
</div>
</button>
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border space-y-4">
{/* Presentation */}
<div className="flex items-start gap-3">
<Package className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-muted-foreground">Presentación</p>
<p className="text-foreground font-medium">{drug.presentation}</p>
</div>
</div>
{/* Adult Dose */}
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-info flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-muted-foreground">Dosis Adulto</p>
<p className="text-foreground font-medium">{drug.adultDose}</p>
</div>
</div>
{/* Pediatric Dose */}
{drug.pediatricDose && (
<div className="flex items-start gap-3">
<Baby className="w-5 h-5 text-info flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-muted-foreground">Dosis Pediátrica</p>
<p className="text-foreground font-medium">{drug.pediatricDose}</p>
</div>
</div>
)}
{/* Routes */}
<div className="flex items-start gap-3">
<Syringe className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-muted-foreground">Vías de Administración</p>
<div className="flex flex-wrap gap-1 mt-1">
{drug.routes.map((route) => (
<Badge key={route} variant="info">
{route}
</Badge>
))}
</div>
</div>
</div>
{/* Dilution */}
{drug.dilution && (
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground mb-1">Dilución</p>
<p className="text-foreground">{drug.dilution}</p>
</div>
)}
{/* Indications */}
<div>
<p className="text-sm text-muted-foreground mb-2 flex items-center gap-1">
<span className="text-success"></span> Indicaciones
</p>
<ul className="space-y-1">
{drug.indications.map((indication, index) => (
<li key={index} className="text-foreground text-sm flex items-start gap-2">
<span className="text-success"></span>
<span>{indication}</span>
</li>
))}
</ul>
</div>
{/* Contraindications */}
<div className="warning-box">
<p className="text-sm text-warning mb-2 flex items-center gap-1 font-semibold">
<AlertCircle className="w-4 h-4" /> Contraindicaciones
</p>
<ul className="space-y-1">
{drug.contraindications.map((contraindication, index) => (
<li key={index} className="text-foreground text-sm flex items-start gap-2">
<span className="text-warning"></span>
<span>{contraindication}</span>
</li>
))}
</ul>
</div>
{/* Antidote */}
{drug.antidote && (
<div className="p-3 bg-primary/10 border border-primary/30 rounded-lg">
<p className="text-sm text-primary font-semibold">
Antídoto: {drug.antidote}
</p>
</div>
)}
{/* Notes */}
{drug.notes && drug.notes.length > 0 && (
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Notas</p>
<ul className="space-y-1">
{drug.notes.map((note, index) => (
<li key={index} className="text-foreground text-sm">
{note}
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
);
};
export default DrugCard;

View file

@ -0,0 +1,40 @@
import { NavLink } from 'react-router-dom';
import { Home, AlertTriangle, Stethoscope, Video, Pill, Wrench } from 'lucide-react';
interface NavItem {
path: string;
icon: React.ReactNode;
label: string;
}
const navItems: NavItem[] = [
{ path: '/', icon: <Home className="w-5 h-5" />, label: 'Home' },
{ path: '/soporte-vital', icon: <AlertTriangle className="w-5 h-5" />, label: 'Soporte' },
{ path: '/patologias', icon: <Stethoscope className="w-5 h-5" />, label: 'Patologías' },
{ path: '/escena', icon: <Video className="w-5 h-5" />, label: 'Escena' },
{ path: '/farmacos', icon: <Pill className="w-5 h-5" />, label: 'Fármacos' },
{ path: '/herramientas', icon: <Wrench className="w-5 h-5" />, label: 'Herram.' },
];
const BottomNav = () => {
return (
<nav className="bottom-nav">
<div className="flex items-stretch justify-around max-w-lg mx-auto">
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`bottom-nav-item flex-1 ${isActive ? 'active' : ''}`
}
>
{item.icon}
<span className="text-2xs mt-1 font-medium">{item.label}</span>
</NavLink>
))}
</div>
</nav>
);
};
export default BottomNav;

View file

@ -0,0 +1,80 @@
import { Search, Menu, Wifi, WifiOff, Star } from 'lucide-react';
import { useState, useEffect } from 'react';
interface HeaderProps {
onSearchClick: () => void;
onMenuClick: () => void;
}
const Header = ({ onSearchClick, onMenuClick }: HeaderProps) => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-card border-b border-border">
<div className="flex items-center justify-between h-14 px-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
<span className="text-primary-foreground font-bold text-sm">TES</span>
</div>
<div className="hidden sm:block">
<h1 className="font-semibold text-foreground text-sm">EMERGES TES</h1>
<p className="text-2xs text-muted-foreground">Guía de Protocolos</p>
</div>
</div>
<div className="flex items-center gap-2">
<div
className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-2xs font-medium ${
isOnline
? 'bg-success/20 text-success'
: 'bg-warning/20 text-warning'
}`}
>
{isOnline ? (
<>
<Wifi className="w-3 h-3" />
<span className="hidden sm:inline">Online</span>
</>
) : (
<>
<WifiOff className="w-3 h-3" />
<span className="hidden sm:inline">Offline</span>
</>
)}
</div>
<button
onClick={onSearchClick}
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
aria-label="Buscar"
>
<Search className="w-5 h-5 text-muted-foreground" />
</button>
<button
onClick={onMenuClick}
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
aria-label="Menú"
>
<Menu className="w-5 h-5 text-muted-foreground" />
</button>
</div>
</div>
</header>
);
};
export default Header;

View file

@ -0,0 +1,63 @@
import { X, Star, History, Settings, Info, Share2 } from 'lucide-react';
interface MenuSheetProps {
isOpen: boolean;
onClose: () => void;
}
const MenuSheet = ({ isOpen, onClose }: MenuSheetProps) => {
if (!isOpen) return null;
const menuItems = [
{ icon: <Star className="w-5 h-5" />, label: 'Favoritos', onClick: () => {} },
{ icon: <History className="w-5 h-5" />, label: 'Historial', onClick: () => {} },
{ icon: <Share2 className="w-5 h-5" />, label: 'Compartir App', onClick: () => {} },
{ icon: <Settings className="w-5 h-5" />, label: 'Ajustes', onClick: () => {} },
{ icon: <Info className="w-5 h-5" />, label: 'Acerca de', onClick: () => {} },
];
return (
<>
<div
className="fixed inset-0 z-[90] bg-background/80 backdrop-blur-sm"
onClick={onClose}
/>
<div className="fixed top-0 right-0 bottom-0 z-[95] w-80 max-w-[85vw] bg-card border-l border-border shadow-xl">
<div className="flex items-center justify-between h-14 px-4 border-b border-border">
<h2 className="font-semibold text-foreground">Menú</h2>
<button
onClick={onClose}
className="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-muted transition-colors"
aria-label="Cerrar menú"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 space-y-2">
{menuItems.map((item, index) => (
<button
key={index}
onClick={item.onClick}
className="w-full flex items-center gap-4 p-4 rounded-xl hover:bg-muted transition-colors text-left"
>
<span className="text-muted-foreground">{item.icon}</span>
<span className="font-medium text-foreground">{item.label}</span>
</button>
))}
</div>
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-border">
<p className="text-2xs text-muted-foreground text-center">
EMERGES TES v1.0.0
</p>
<p className="text-2xs text-muted-foreground text-center mt-1">
Guía de Protocolos para TES
</p>
</div>
</div>
</>
);
};
export default MenuSheet;

View file

@ -0,0 +1,137 @@
import { useState, useEffect, useRef } from 'react';
import { Search, X, FileText, Pill, ArrowRight } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { searchProcedures, Procedure } from '@/data/procedures';
import { searchDrugs, Drug } from '@/data/drugs';
interface SearchModalProps {
isOpen: boolean;
onClose: () => void;
}
type SearchResult = {
type: 'procedure' | 'drug';
id: string;
title: string;
subtitle?: string;
};
const SearchModal = ({ isOpen, onClose }: SearchModalProps) => {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
useEffect(() => {
if (query.length < 2) {
setResults([]);
return;
}
const procedures = searchProcedures(query).map((p): SearchResult => ({
type: 'procedure',
id: p.id,
title: p.shortTitle,
subtitle: p.category.replace('_', ' '),
}));
const drugs = searchDrugs(query).map((d): SearchResult => ({
type: 'drug',
id: d.id,
title: d.genericName,
subtitle: d.tradeName,
}));
setResults([...procedures, ...drugs].slice(0, 8));
}, [query]);
const handleResultClick = (result: SearchResult) => {
if (result.type === 'procedure') {
navigate(`/soporte-vital?id=${result.id}`);
} else {
navigate(`/farmacos?id=${result.id}`);
}
onClose();
setQuery('');
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[100] bg-background/95 backdrop-blur-sm">
<div className="flex flex-col h-full max-w-2xl mx-auto p-4">
<div className="flex items-center gap-3 mb-4">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar protocolo, fármaco, calculadora..."
className="w-full h-14 pl-12 pr-4 bg-card border border-border rounded-xl text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<button
onClick={onClose}
className="w-14 h-14 flex items-center justify-center rounded-xl bg-card border border-border hover:bg-muted transition-colors"
aria-label="Cerrar búsqueda"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto scroll-touch">
{results.length > 0 ? (
<div className="space-y-2">
{results.map((result) => (
<button
key={`${result.type}-${result.id}`}
onClick={() => handleResultClick(result)}
className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors text-left"
>
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
{result.type === 'procedure' ? (
<FileText className="w-5 h-5 text-primary" />
) : (
<Pill className="w-5 h-5 text-secondary" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground truncate">{result.title}</p>
{result.subtitle && (
<p className="text-sm text-muted-foreground capitalize truncate">
{result.subtitle}
</p>
)}
</div>
<ArrowRight className="w-5 h-5 text-muted-foreground flex-shrink-0" />
</button>
))}
</div>
) : query.length >= 2 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="w-12 h-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">No se encontraron resultados</p>
<p className="text-sm text-muted-foreground">Intenta con otros términos</p>
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="w-12 h-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">Escribe para buscar</p>
<p className="text-sm text-muted-foreground">Protocolos, fármacos, calculadoras...</p>
</div>
)}
</div>
</div>
</div>
);
};
export default SearchModal;

View file

@ -0,0 +1,147 @@
import { useState } from 'react';
import { ChevronDown, ChevronUp, Star, AlertTriangle, User, Baby } from 'lucide-react';
import { Procedure, Priority } from '@/data/procedures';
import Badge from '@/components/shared/Badge';
import { cn } from '@/lib/utils';
interface ProcedureCardProps {
procedure: Procedure;
defaultExpanded?: boolean;
}
const priorityToBadgeVariant: Record<Priority, 'critical' | 'high' | 'medium' | 'low'> = {
critico: 'critical',
alto: 'high',
medio: 'medium',
bajo: 'low',
};
const ProcedureCard = ({ procedure, defaultExpanded = false }: ProcedureCardProps) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [isFavorite, setIsFavorite] = useState(false);
const toggleFavorite = (e: React.MouseEvent) => {
e.stopPropagation();
setIsFavorite(!isFavorite);
};
return (
<div className="card-procedure">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full text-left"
aria-expanded={isExpanded}
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2 flex-wrap">
<Badge variant={priorityToBadgeVariant[procedure.priority]}>
{procedure.priority}
</Badge>
<Badge variant="info">
{procedure.ageGroup === 'adulto' && <User className="w-3 h-3 mr-1" />}
{procedure.ageGroup === 'pediatrico' && <Baby className="w-3 h-3 mr-1" />}
{procedure.ageGroup}
</Badge>
</div>
<h3 className="font-semibold text-foreground text-lg leading-tight">
{procedure.shortTitle}
</h3>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={toggleFavorite}
className={cn(
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
isFavorite ? 'text-warning' : 'text-muted-foreground hover:text-foreground'
)}
aria-label={isFavorite ? 'Quitar de favoritos' : 'Añadir a favoritos'}
>
<Star className={cn('w-5 h-5', isFavorite && 'fill-current')} />
</button>
<div className="w-10 h-10 flex items-center justify-center">
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-muted-foreground" />
) : (
<ChevronDown className="w-5 h-5 text-muted-foreground" />
)}
</div>
</div>
</div>
</button>
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border">
<div className="space-y-3">
<h4 className="font-semibold text-foreground text-sm uppercase tracking-wide">
Pasos
</h4>
<ol className="space-y-3">
{procedure.steps.map((step, index) => (
<li key={index} className="flex items-start gap-3">
<span className="step-number">{index + 1}</span>
<span className="text-foreground pt-1 flex-1">{step}</span>
</li>
))}
</ol>
</div>
{procedure.warnings.length > 0 && (
<div className="mt-6">
<div className="warning-box">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-warning" />
<h4 className="font-semibold text-warning text-sm uppercase tracking-wide">
Puntos Clave
</h4>
</div>
<ul className="space-y-1">
{procedure.warnings.map((warning, index) => (
<li key={index} className="text-foreground text-sm flex items-start gap-2">
<span className="text-warning"></span>
<span>{warning}</span>
</li>
))}
</ul>
</div>
</div>
)}
{procedure.keyPoints && procedure.keyPoints.length > 0 && (
<div className="mt-4 p-3 bg-muted rounded-lg">
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-2">
Recuerda
</h4>
<ul className="space-y-1">
{procedure.keyPoints.map((point, index) => (
<li key={index} className="text-foreground text-sm flex items-start gap-2">
<span className="text-info"></span>
<span>{point}</span>
</li>
))}
</ul>
</div>
)}
{procedure.equipment && procedure.equipment.length > 0 && (
<div className="mt-4">
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-2">
Material
</h4>
<div className="flex flex-wrap gap-2">
{procedure.equipment.map((item, index) => (
<Badge key={index} variant="default">
{item}
</Badge>
))}
</div>
</div>
)}
</div>
)}
</div>
);
};
export default ProcedureCard;

View file

@ -0,0 +1,34 @@
import { cn } from '@/lib/utils';
type BadgeVariant = 'critical' | 'high' | 'medium' | 'low' | 'info' | 'default';
interface BadgeProps {
variant?: BadgeVariant;
children: React.ReactNode;
className?: string;
}
const variantClasses: Record<BadgeVariant, string> = {
critical: 'badge-critical',
high: 'badge-high',
medium: 'badge-medium',
low: 'bg-success/20 text-success border border-success/30',
info: 'badge-info',
default: 'bg-muted text-muted-foreground border border-border',
};
const Badge = ({ variant = 'default', children, className }: BadgeProps) => {
return (
<span
className={cn(
'inline-flex items-center px-2 py-0.5 rounded-md text-2xs font-semibold uppercase tracking-wide',
variantClasses[variant],
className
)}
>
{children}
</span>
);
};
export default Badge;

View file

@ -0,0 +1,48 @@
import { Link } from 'react-router-dom';
import { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
type ButtonVariant = 'critical' | 'high' | 'medium';
interface EmergencyButtonProps {
to: string;
icon: LucideIcon;
title: string;
subtitle?: string;
variant?: ButtonVariant;
className?: string;
}
const variantClasses: Record<ButtonVariant, string> = {
critical: 'btn-emergency-critical',
high: 'btn-emergency-high',
medium: 'btn-emergency-medium',
};
const EmergencyButton = ({
to,
icon: Icon,
title,
subtitle,
variant = 'critical',
className,
}: EmergencyButtonProps) => {
return (
<Link
to={to}
className={cn(
'btn-emergency flex flex-col items-center justify-center gap-1 p-4',
variantClasses[variant],
className
)}
>
<Icon className="w-8 h-8" />
<span className="text-sm font-semibold text-center leading-tight">{title}</span>
{subtitle && (
<span className="text-2xs opacity-80 text-center">{subtitle}</span>
)}
</Link>
);
};
export default EmergencyButton;

View file

@ -0,0 +1,83 @@
import { useState } from 'react';
import { glasgowScale, getGlasgowInterpretation } from '@/data/calculators';
import Badge from '@/components/shared/Badge';
import { cn } from '@/lib/utils';
const GlasgowCalculator = () => {
const [scores, setScores] = useState<Record<string, number>>({
'Apertura Ocular': 4,
'Respuesta Verbal': 5,
'Respuesta Motora': 6,
});
const totalScore = Object.values(scores).reduce((sum, score) => sum + score, 0);
const interpretation = getGlasgowInterpretation(totalScore);
const handleScoreChange = (category: string, value: number) => {
setScores((prev) => ({ ...prev, [category]: value }));
};
return (
<div className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-4">
🧠 Escala de Glasgow (GCS)
</h3>
<div className="space-y-6">
{glasgowScale.map((category) => (
<div key={category.name}>
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-foreground">{category.name}</h4>
<span className="text-xl font-bold text-primary">
{scores[category.name]}
</span>
</div>
<div className="space-y-2">
{category.options.map((option) => (
<button
key={option.value}
onClick={() => handleScoreChange(category.name, option.value)}
className={cn(
'w-full flex items-center justify-between p-3 rounded-lg border transition-colors text-left',
scores[category.name] === option.value
? 'bg-primary/10 border-primary text-foreground'
: 'bg-muted border-border text-muted-foreground hover:border-primary/50'
)}
>
<span className="text-sm">{option.label}</span>
<span className="font-bold">{option.value}</span>
</button>
))}
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-card border-2 border-primary rounded-xl text-center">
<p className="text-muted-foreground text-sm mb-1">Puntuación Total</p>
<p className="text-5xl font-bold text-foreground mb-2">{totalScore}</p>
<Badge
variant={
interpretation.color === 'critical'
? 'critical'
: interpretation.color === 'high'
? 'high'
: 'low'
}
className="text-sm px-3 py-1"
>
{interpretation.severity}
</Badge>
</div>
<div className="mt-4 text-center text-sm text-muted-foreground">
<p>Rango: 3 (mínimo) - 15 (máximo)</p>
<p className="mt-1">
8: Grave (IOT) | 9-12: Moderado | 13-15: Leve
</p>
</div>
</div>
);
};
export default GlasgowCalculator;

View file

@ -0,0 +1,54 @@
import { InfusionTable } from '@/data/calculators';
interface InfusionTableViewProps {
table: InfusionTable;
}
const InfusionTableView = ({ table }: InfusionTableViewProps) => {
return (
<div className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-2">{table.name}</h3>
<p className="text-sm text-muted-foreground mb-4">{table.preparation}</p>
<div className="overflow-x-auto -mx-4 px-4">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2 font-semibold text-muted-foreground">
Peso (kg)
</th>
{table.columns.map((col) => (
<th
key={col}
className="text-center py-3 px-2 font-semibold text-muted-foreground"
>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{table.rows.map((row) => (
<tr key={row.weight} className="border-b border-border/50">
<td className="py-3 px-2 font-bold text-foreground">
{row.weight}
</td>
{table.columns.map((col) => (
<td key={col} className="text-center py-3 px-2 text-foreground">
{row.doses[col]} {table.unit}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<p className="text-2xs text-muted-foreground mt-4 text-center">
Rango de dosis: {table.doseRange}
</p>
</div>
);
};
export default InfusionTableView;

101
src/data/calculators.ts Normal file
View file

@ -0,0 +1,101 @@
export interface GlasgowOption {
label: string;
value: number;
}
export interface GlasgowCategory {
name: string;
options: GlasgowOption[];
}
export const glasgowScale: GlasgowCategory[] = [
{
name: 'Apertura Ocular',
options: [
{ label: 'Espontánea', value: 4 },
{ label: 'A la orden verbal', value: 3 },
{ label: 'Al dolor', value: 2 },
{ label: 'No abre', value: 1 },
],
},
{
name: 'Respuesta Verbal',
options: [
{ label: 'Orientado', value: 5 },
{ label: 'Confuso', value: 4 },
{ label: 'Palabras inapropiadas', value: 3 },
{ label: 'Sonidos incomprensibles', value: 2 },
{ label: 'Sin respuesta', value: 1 },
],
},
{
name: 'Respuesta Motora',
options: [
{ label: 'Obedece órdenes', value: 6 },
{ label: 'Localiza el dolor', value: 5 },
{ label: 'Retirada al dolor', value: 4 },
{ label: 'Flexión anormal (decorticación)', value: 3 },
{ label: 'Extensión anormal (descerebración)', value: 2 },
{ label: 'Sin respuesta', value: 1 },
],
},
];
export const getGlasgowInterpretation = (score: number): { severity: string; color: string } => {
if (score <= 8) return { severity: 'TCE Grave - IOT', color: 'critical' };
if (score <= 12) return { severity: 'TCE Moderado', color: 'high' };
return { severity: 'TCE Leve', color: 'low' };
};
export interface InfusionRow {
weight: number;
doses: { [key: string]: string };
}
export interface InfusionTable {
id: string;
name: string;
drugName: string;
preparation: string;
unit: string;
doseRange: string;
columns: string[];
rows: InfusionRow[];
}
export const infusionTables: InfusionTable[] = [
{
id: 'dopamina',
name: 'Perfusión Dopamina',
drugName: 'Dopamina',
preparation: '200mg en 100ml SG5% = 2000 mcg/ml',
unit: 'ml/h',
doseRange: '2-20 mcg/kg/min',
columns: ['5 mcg/kg/min', '10 mcg/kg/min', '15 mcg/kg/min', '20 mcg/kg/min'],
rows: [
{ weight: 50, doses: { '5 mcg/kg/min': '7.5', '10 mcg/kg/min': '15', '15 mcg/kg/min': '22.5', '20 mcg/kg/min': '30' } },
{ weight: 60, doses: { '5 mcg/kg/min': '9', '10 mcg/kg/min': '18', '15 mcg/kg/min': '27', '20 mcg/kg/min': '36' } },
{ weight: 70, doses: { '5 mcg/kg/min': '10.5', '10 mcg/kg/min': '21', '15 mcg/kg/min': '31.5', '20 mcg/kg/min': '42' } },
{ weight: 80, doses: { '5 mcg/kg/min': '12', '10 mcg/kg/min': '24', '15 mcg/kg/min': '36', '20 mcg/kg/min': '48' } },
{ weight: 90, doses: { '5 mcg/kg/min': '13.5', '10 mcg/kg/min': '27', '15 mcg/kg/min': '40.5', '20 mcg/kg/min': '54' } },
{ weight: 100, doses: { '5 mcg/kg/min': '15', '10 mcg/kg/min': '30', '15 mcg/kg/min': '45', '20 mcg/kg/min': '60' } },
],
},
{
id: 'noradrenalina',
name: 'Perfusión Noradrenalina',
drugName: 'Noradrenalina',
preparation: '8mg en 100ml SG5% = 80 mcg/ml',
unit: 'ml/h',
doseRange: '0.05-1 mcg/kg/min',
columns: ['0.1 mcg/kg/min', '0.2 mcg/kg/min', '0.3 mcg/kg/min', '0.5 mcg/kg/min'],
rows: [
{ weight: 50, doses: { '0.1 mcg/kg/min': '3.75', '0.2 mcg/kg/min': '7.5', '0.3 mcg/kg/min': '11.25', '0.5 mcg/kg/min': '18.75' } },
{ weight: 60, doses: { '0.1 mcg/kg/min': '4.5', '0.2 mcg/kg/min': '9', '0.3 mcg/kg/min': '13.5', '0.5 mcg/kg/min': '22.5' } },
{ weight: 70, doses: { '0.1 mcg/kg/min': '5.25', '0.2 mcg/kg/min': '10.5', '0.3 mcg/kg/min': '15.75', '0.5 mcg/kg/min': '26.25' } },
{ weight: 80, doses: { '0.1 mcg/kg/min': '6', '0.2 mcg/kg/min': '12', '0.3 mcg/kg/min': '18', '0.5 mcg/kg/min': '30' } },
{ weight: 90, doses: { '0.1 mcg/kg/min': '6.75', '0.2 mcg/kg/min': '13.5', '0.3 mcg/kg/min': '20.25', '0.5 mcg/kg/min': '33.75' } },
{ weight: 100, doses: { '0.1 mcg/kg/min': '7.5', '0.2 mcg/kg/min': '15', '0.3 mcg/kg/min': '22.5', '0.5 mcg/kg/min': '37.5' } },
],
},
];

174
src/data/drugs.ts Normal file
View file

@ -0,0 +1,174 @@
export type DrugCategory = 'cardiovascular' | 'respiratorio' | 'neurologico' | 'analgesia' | 'otros';
export type AdministrationRoute = 'IV' | 'IM' | 'SC' | 'IO' | 'Nebulizado' | 'SL' | 'Rectal' | 'Nasal';
export interface Drug {
id: string;
genericName: string;
tradeName: string;
category: DrugCategory;
presentation: string;
adultDose: string;
pediatricDose?: string;
routes: AdministrationRoute[];
dilution?: string;
indications: string[];
contraindications: string[];
sideEffects?: string[];
antidote?: string;
notes?: string[];
}
export const drugs: Drug[] = [
{
id: 'adrenalina',
genericName: 'Adrenalina',
tradeName: 'Adrenalina Braun®',
category: 'cardiovascular',
presentation: '1 mg/1 ml (ampolla)',
adultDose: 'PCR: 1mg IV/IO cada 3-5 min | Anafilaxia: 0.3-0.5mg IM',
pediatricDose: 'PCR: 0.01 mg/kg IV/IO | Anafilaxia: 0.01 mg/kg IM (máx 0.3mg)',
routes: ['IV', 'IO', 'IM'],
dilution: 'PCR: sin diluir | Perfusión: 1mg en 100ml SSF (10 mcg/ml)',
indications: [
'Parada cardiorrespiratoria',
'Anafilaxia',
'Shock séptico refractario',
'Bradicardia sintomática',
'Crisis asmática severa',
],
contraindications: [
'No hay contraindicaciones absolutas en emergencias vitales',
],
sideEffects: ['Taquicardia', 'HTA', 'Arritmias', 'Ansiedad'],
notes: [
'En anafilaxia: vía IM en cara anterolateral del muslo',
'Repetir cada 5-15 min si persisten síntomas',
],
},
{
id: 'amiodarona',
genericName: 'Amiodarona',
tradeName: 'Trangorex®',
category: 'cardiovascular',
presentation: '150 mg/3 ml (ampolla)',
adultDose: 'FV/TVSP: 300mg IV en bolo | Mantenimiento: 900mg/24h',
pediatricDose: '5 mg/kg IV/IO (máx 300mg)',
routes: ['IV', 'IO'],
dilution: 'Diluir en SG5% (precipita con SSF)',
indications: [
'FV/TVSP refractaria a descargas',
'Taquicardia ventricular estable',
'Control de frecuencia en FA',
],
contraindications: [
'Bradicardia sinusal',
'Bloqueo AV 2º-3º grado sin marcapasos',
'Disfunción tiroidea severa',
'Hipersensibilidad al yodo',
],
sideEffects: ['Hipotensión', 'Bradicardia', 'Flebitis'],
notes: [
'Segunda dosis en PCR: 150mg si persiste FV/TVSP',
'Usar vía central si es posible',
],
},
{
id: 'atropina',
genericName: 'Atropina',
tradeName: 'Atropina Braun®',
category: 'cardiovascular',
presentation: '1 mg/1 ml (ampolla)',
adultDose: 'Bradicardia: 0.5mg IV cada 3-5 min (máx 3mg)',
pediatricDose: '0.02 mg/kg IV (mín 0.1mg, máx 0.5mg)',
routes: ['IV', 'IO', 'IM'],
indications: [
'Bradicardia sintomática',
'Intoxicación por organofosforados',
'Premedicación antes de IOT',
],
contraindications: [
'Glaucoma de ángulo estrecho',
'Retención urinaria',
'Íleo paralítico',
],
sideEffects: ['Taquicardia', 'Sequedad de boca', 'Retención urinaria', 'Midriasis'],
antidote: 'Fisostigmina',
notes: [
'Dosis <0.5mg pueden causar bradicardia paradójica',
'Ya NO se usa en PCR según guías actuales',
],
},
{
id: 'midazolam',
genericName: 'Midazolam',
tradeName: 'Dormicum®',
category: 'neurologico',
presentation: '15 mg/3 ml o 5 mg/5 ml (ampolla)',
adultDose: 'Sedación: 1-2.5mg IV titulando | Crisis: 10mg IM/Intranasal',
pediatricDose: 'Crisis: 0.2-0.3 mg/kg intranasal/bucal (máx 10mg)',
routes: ['IV', 'IM', 'Nasal', 'Rectal'],
dilution: 'Puede administrarse sin diluir IV lento',
indications: [
'Status epiléptico',
'Sedación para procedimientos',
'Premedicación anestésica',
'Agitación severa',
],
contraindications: [
'Miastenia gravis',
'Insuficiencia respiratoria severa',
'Shock',
],
sideEffects: ['Depresión respiratoria', 'Hipotensión', 'Amnesia'],
antidote: 'Flumazenilo',
notes: [
'En crisis: vía intranasal con atomizador (MAD)',
'Inicio de acción IV: 1-2 min, IM/IN: 5-10 min',
],
},
{
id: 'salbutamol',
genericName: 'Salbutamol',
tradeName: 'Ventolin®',
category: 'respiratorio',
presentation: 'Nebulización: 5 mg/ml | MDI: 100 mcg/puff',
adultDose: 'Nebulización: 2.5-5mg | MDI: 4-8 puffs con cámara',
pediatricDose: 'Nebulización: 2.5mg (<20kg) o 5mg (>20kg) | MDI: 4-6 puffs',
routes: ['Nebulizado'],
dilution: 'Diluir en 3ml de SSF para nebulizar',
indications: [
'Crisis asmática',
'EPOC reagudizado',
'Broncoespasmo',
'Hiperpotasemia (coadyuvante)',
],
contraindications: [
'Hipersensibilidad conocida',
'Usar con precaución en cardiopatía isquémica',
],
sideEffects: ['Taquicardia', 'Temblor', 'Hipopotasemia', 'Nerviosismo'],
notes: [
'En crisis grave: nebulización continua',
'Puede repetirse cada 20 min las primeras horas',
'Asociar ipratropio en crisis moderada-grave',
],
},
];
export const getDrugsByCategory = (category: DrugCategory): Drug[] => {
return drugs.filter((d) => d.category === category);
};
export const getDrugById = (id: string): Drug | undefined => {
return drugs.find((d) => d.id === id);
};
export const searchDrugs = (query: string): Drug[] => {
const lowerQuery = query.toLowerCase();
return drugs.filter(
(d) =>
d.genericName.toLowerCase().includes(lowerQuery) ||
d.tradeName.toLowerCase().includes(lowerQuery) ||
d.indications.some((i) => i.toLowerCase().includes(lowerQuery))
);
};

200
src/data/procedures.ts Normal file
View file

@ -0,0 +1,200 @@
export type Priority = 'critico' | 'alto' | 'medio' | 'bajo';
export type AgeGroup = 'adulto' | 'pediatrico' | 'neonatal' | 'todos';
export type Category = 'soporte_vital' | 'patologias' | 'escena';
export interface Procedure {
id: string;
title: string;
shortTitle: string;
category: Category;
subcategory?: string;
priority: Priority;
ageGroup: AgeGroup;
steps: string[];
warnings: string[];
keyPoints?: string[];
equipment?: string[];
drugs?: string[];
}
export const procedures: Procedure[] = [
{
id: 'rcp-adulto-svb',
title: 'RCP Adulto - Soporte Vital Básico',
shortTitle: 'RCP Adulto SVB',
category: 'soporte_vital',
subcategory: 'rcp',
priority: 'critico',
ageGroup: 'adulto',
steps: [
'Garantizar seguridad de la escena',
'Comprobar consciencia: estimular y preguntar "¿Se encuentra bien?"',
'Si no responde, gritar pidiendo ayuda',
'Abrir vía aérea: maniobra frente-mentón',
'Comprobar respiración: VER-OÍR-SENTIR (máx. 10 segundos)',
'Si no respira normal: llamar 112 y pedir DEA',
'Iniciar compresiones torácicas: 30 compresiones',
'Dar 2 ventilaciones de rescate',
'Continuar ciclos 30:2 sin interrupción',
'Cuando llegue DEA: encenderlo y seguir instrucciones',
],
warnings: [
'Profundidad compresiones: 5-6 cm',
'Frecuencia: 100-120 compresiones/min',
'Permitir descompresión completa',
'Minimizar interrupciones (<10 seg)',
'Cambiar reanimador cada 2 min',
],
keyPoints: [
'Compresiones de calidad salvan vidas',
'No interrumpir para pulso hasta que haya signos de vida',
'La desfibrilación precoz aumenta supervivencia',
],
equipment: ['DEA', 'Bolsa-mascarilla', 'Cánula orofaríngea'],
drugs: ['Adrenalina 1mg'],
},
{
id: 'rcp-adulto-sva',
title: 'RCP Adulto - Soporte Vital Avanzado',
shortTitle: 'RCP Adulto SVA',
category: 'soporte_vital',
subcategory: 'rcp',
priority: 'critico',
ageGroup: 'adulto',
steps: [
'Continuar RCP 30:2 mientras se prepara monitorización',
'Colocar monitor/desfibrilador y analizar ritmo',
'Ritmo desfibrilable (FV/TVSP): descarga 150-200J bifásico',
'Reiniciar RCP inmediatamente 2 minutos',
'Obtener acceso IV/IO',
'Administrar adrenalina 1mg IV cada 3-5 min (tras 3ª descarga si DF)',
'Considerar amiodarona 300mg IV si FV/TVSP refractaria',
'Asegurar vía aérea avanzada cuando sea posible',
'Buscar y tratar causas reversibles (4H y 4T)',
'Si ROSC: cuidados post-parada',
],
warnings: [
'Minimizar interrupciones de compresiones',
'Adrenalina en ritmos no DF: lo antes posible',
'Amiodarona: 150mg adicionales si persiste FV/TVSP',
'Capnografía: objetivo ETCO2 >10 mmHg',
],
keyPoints: [
'4H: Hipoxia, Hipovolemia, Hipo/Hiperpotasemia, Hipotermia',
'4T: Neumotórax a Tensión, Taponamiento, Tóxicos, TEP',
],
equipment: ['Monitor/Desfibrilador', 'Material IOT', 'Acceso venoso'],
drugs: ['Adrenalina', 'Amiodarona', 'Atropina'],
},
{
id: 'rcp-pediatrico',
title: 'RCP Pediátrico - SVB',
shortTitle: 'RCP Pediátrico',
category: 'soporte_vital',
subcategory: 'rcp',
priority: 'critico',
ageGroup: 'pediatrico',
steps: [
'Garantizar seguridad',
'Comprobar consciencia',
'Abrir vía aérea: maniobra frente-mentón',
'Comprobar respiración (máx. 10 segundos)',
'Dar 5 ventilaciones de rescate iniciales',
'Comprobar signos de vida/pulso (máx. 10 seg)',
'Si no hay signos de vida: 15 compresiones torácicas',
'Continuar con ciclos 15:2',
'Si está solo: RCP 1 minuto antes de llamar 112',
],
warnings: [
'Lactante (<1 año): compresiones con 2 dedos',
'Niño (1-8 años): talón de una mano',
'Profundidad: 1/3 del tórax (4cm lactante, 5cm niño)',
'Frecuencia: 100-120/min',
],
keyPoints: [
'La causa más frecuente es respiratoria',
'Las 5 ventilaciones iniciales son cruciales',
'Ratio 15:2 para profesionales',
],
},
{
id: 'obstruccion-via-aerea',
title: 'Obstrucción de Vía Aérea - OVACE',
shortTitle: 'OVACE',
category: 'soporte_vital',
subcategory: 'via_aerea',
priority: 'critico',
ageGroup: 'todos',
steps: [
'Valorar gravedad: ¿Puede toser, hablar, respirar?',
'OBSTRUCCIÓN LEVE: animar a toser, vigilar',
'OBSTRUCCIÓN GRAVE consciente: 5 golpes interescapulares',
'Si no se resuelve: 5 compresiones abdominales (Heimlich)',
'Alternar 5 golpes + 5 compresiones hasta resolución',
'Si pierde consciencia: iniciar RCP',
'Antes de ventilar: revisar boca y extraer objeto visible',
],
warnings: [
'En embarazadas y obesos: compresiones torácicas',
'Lactantes: 5 golpes en espalda + 5 compresiones torácicas',
'NO hacer barrido digital a ciegas',
'Derivar siempre tras maniobras de Heimlich',
],
keyPoints: [
'La tos es el mecanismo más efectivo',
'No interferir si la tos es efectiva',
],
},
{
id: 'shock-hemorragico',
title: 'Shock Hemorrágico',
shortTitle: 'Shock Hemorrágico',
category: 'soporte_vital',
subcategory: 'shock',
priority: 'critico',
ageGroup: 'adulto',
steps: [
'Control de hemorragia externa: presión directa',
'Torniquete si hemorragia en extremidad no controlable',
'Oxigenoterapia alto flujo',
'Canalizar 2 vías IV gruesas (14-16G)',
'Fluidos: cristaloides tibios (objetivo TAS 80-90 mmHg)',
'Posición Trendelenburg si no hay TCE',
'Evitar hipotermia: mantas térmicas',
'Traslado urgente a hospital útil',
'Considerar ácido tranexámico 1g IV',
],
warnings: [
'Hipotensión permisiva: TAS 80-90 mmHg',
'Excepto en TCE: mantener TAS >90 mmHg',
'Evitar sobrecarga de fluidos',
'Torniquete: anotar hora de colocación',
],
keyPoints: [
'Clase I: <15% pérdida, FC normal, TA normal',
'Clase II: 15-30%, taquicardia, TA normal',
'Clase III: 30-40%, taquicardia, hipotensión',
'Clase IV: >40%, bradicardia, shock severo',
],
equipment: ['Torniquete', 'Agentes hemostáticos', 'Mantas térmicas'],
drugs: ['Ácido tranexámico', 'Cristaloides'],
},
];
export const getProceduresByCategory = (category: Category): Procedure[] => {
return procedures.filter((p) => p.category === category);
};
export const getProcedureById = (id: string): Procedure | undefined => {
return procedures.find((p) => p.id === id);
};
export const searchProcedures = (query: string): Procedure[] => {
const lowerQuery = query.toLowerCase();
return procedures.filter(
(p) =>
p.title.toLowerCase().includes(lowerQuery) ||
p.shortTitle.toLowerCase().includes(lowerQuery) ||
p.steps.some((s) => s.toLowerCase().includes(lowerQuery))
);
};

View file

@ -2,95 +2,95 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here. /* EMERGES TES - Design System for Emergency Medical Technicians */
All colors MUST be HSL. /* Dark theme optimized for ambulance night use */
*/
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap');
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; /* Dark theme by default - optimized for night use */
--foreground: 222.2 84% 4.9%; --background: 220 20% 12%;
--foreground: 0 0% 98%;
--card: 0 0% 100%; --card: 220 18% 16%;
--card-foreground: 222.2 84% 4.9%; --card-foreground: 0 0% 98%;
--popover: 0 0% 100%; --popover: 220 18% 14%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 0 0% 98%;
--primary: 222.2 47.4% 11.2%; /* Emergency Red - Primary action color */
--primary-foreground: 210 40% 98%; --primary: 0 84% 50%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%; /* Medical Blue - Secondary elements */
--secondary-foreground: 222.2 47.4% 11.2%; --secondary: 217 91% 52%;
--secondary-foreground: 0 0% 100%;
--muted: 210 40% 96.1%; --muted: 220 16% 22%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 220 10% 65%;
--accent: 210 40% 96.1%; --accent: 220 16% 24%;
--accent-foreground: 222.2 47.4% 11.2%; --accent-foreground: 0 0% 98%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84% 50%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 100%;
--border: 214.3 31.8% 91.4%; --border: 220 16% 26%;
--input: 214.3 31.8% 91.4%; --input: 220 16% 20%;
--ring: 222.2 84% 4.9%; --ring: 0 84% 50%;
--radius: 0.5rem; --radius: 0.75rem;
--sidebar-background: 0 0% 98%; /* Custom emergency colors */
--emergency-critical: 0 84% 50%;
--emergency-high: 25 95% 53%;
--emergency-medium: 45 93% 47%;
--emergency-low: 142 71% 45%;
--sidebar-foreground: 240 5.3% 26.1%; /* Functional colors */
--success: 142 71% 45%;
--warning: 45 93% 47%;
--info: 217 91% 52%;
--sidebar-primary: 240 5.9% 10%; --sidebar-background: 220 20% 10%;
--sidebar-foreground: 0 0% 98%;
--sidebar-primary-foreground: 0 0% 98%; --sidebar-primary: 0 84% 50%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 4.8% 95.9%; --sidebar-accent: 220 16% 18%;
--sidebar-accent-foreground: 0 0% 98%;
--sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 16% 22%;
--sidebar-ring: 0 84% 50%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
.dark { .light {
--background: 222.2 84% 4.9%; --background: 0 0% 100%;
--foreground: 210 40% 98%; --foreground: 220 20% 12%;
--card: 222.2 84% 4.9%; --card: 0 0% 100%;
--card-foreground: 210 40% 98%; --card-foreground: 220 20% 12%;
--popover: 222.2 84% 4.9%; --popover: 0 0% 100%;
--popover-foreground: 210 40% 98%; --popover-foreground: 220 20% 12%;
--primary: 210 40% 98%; --primary: 0 84% 50%;
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%; --secondary: 217 91% 52%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 0 0% 100%;
--muted: 217.2 32.6% 17.5%; --muted: 220 14% 96%;
--muted-foreground: 215 20.2% 65.1%; --muted-foreground: 220 10% 40%;
--accent: 217.2 32.6% 17.5%; --accent: 220 14% 96%;
--accent-foreground: 210 40% 98%; --accent-foreground: 220 20% 12%;
--destructive: 0 62.8% 30.6%; --destructive: 0 84% 50%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 0 0% 100%;
--border: 217.2 32.6% 17.5%; --border: 220 13% 91%;
--input: 217.2 32.6% 17.5%; --input: 220 13% 91%;
--ring: 212.7 26.8% 83.9%; --ring: 0 84% 50%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
} }
} }
@ -99,7 +99,134 @@ All colors MUST be HSL.
@apply border-border; @apply border-border;
} }
html {
font-family: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
-webkit-tap-highlight-color: transparent;
}
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground antialiased;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
}
/* Touch-friendly for gloved hands */
button, a, input, select, textarea {
min-height: 48px;
}
/* High contrast focus states */
*:focus-visible {
@apply outline-none ring-2 ring-primary ring-offset-2 ring-offset-background;
}
}
@layer components {
/* Emergency button variants */
.btn-emergency {
@apply min-h-[60px] font-semibold text-lg rounded-lg transition-all active:scale-95;
}
.btn-emergency-critical {
@apply bg-[hsl(var(--emergency-critical))] text-white hover:bg-[hsl(var(--emergency-critical))]/90;
}
.btn-emergency-high {
@apply bg-[hsl(var(--emergency-high))] text-white hover:bg-[hsl(var(--emergency-high))]/90;
}
.btn-emergency-medium {
@apply bg-[hsl(var(--emergency-medium))] text-black hover:bg-[hsl(var(--emergency-medium))]/90;
}
/* Badge priorities */
.badge-critical {
@apply bg-[hsl(var(--emergency-critical))]/20 text-[hsl(var(--emergency-critical))] border border-[hsl(var(--emergency-critical))]/30;
}
.badge-high {
@apply bg-[hsl(var(--emergency-high))]/20 text-[hsl(var(--emergency-high))] border border-[hsl(var(--emergency-high))]/30;
}
.badge-medium {
@apply bg-[hsl(var(--emergency-medium))]/20 text-[hsl(var(--emergency-medium))] border border-[hsl(var(--emergency-medium))]/30;
}
.badge-info {
@apply bg-[hsl(var(--info))]/20 text-[hsl(var(--info))] border border-[hsl(var(--info))]/30;
}
/* Card styles */
.card-procedure {
@apply bg-card border border-border rounded-lg p-4 transition-colors hover:border-primary/50;
}
/* Step numbers */
.step-number {
@apply w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm flex-shrink-0;
}
/* Warning box */
.warning-box {
@apply bg-[hsl(var(--emergency-medium))]/10 border-l-4 border-[hsl(var(--emergency-medium))] p-3 rounded-r-lg;
}
/* Bottom navigation */
.bottom-nav {
@apply fixed bottom-0 left-0 right-0 bg-card border-t border-border z-50;
padding-bottom: env(safe-area-inset-bottom);
}
.bottom-nav-item {
@apply flex flex-col items-center justify-center py-2 px-1 min-h-[64px] text-muted-foreground transition-colors;
}
.bottom-nav-item.active {
@apply text-primary;
}
.bottom-nav-item:hover {
@apply text-foreground;
}
}
@layer utilities {
/* Safe area padding for mobile */
.pb-safe {
padding-bottom: calc(80px + env(safe-area-inset-bottom));
}
/* Touch-friendly scrolling */
.scroll-touch {
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
/* Hide scrollbar but keep functionality */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
/* Print styles for protocols */
@media print {
body {
background: white !important;
color: black !important;
}
.bottom-nav,
header,
.no-print {
display: none !important;
}
.card-procedure {
break-inside: avoid;
border: 1px solid #ccc;
} }
} }

290
src/pages/Escena.tsx Normal file
View file

@ -0,0 +1,290 @@
import { useState } from 'react';
import { Shield, Activity, Users, Move, Truck, Check, Square } from 'lucide-react';
import Badge from '@/components/shared/Badge';
const tabs = [
{ id: 'seguridad', label: 'Seguridad', icon: Shield },
{ id: 'abcde', label: 'ABCDE', icon: Activity },
{ id: 'triage', label: 'Triage', icon: Users },
{ id: 'inmovilizacion', label: 'Inmovil.', icon: Move },
{ id: 'extricacion', label: 'Extric.', icon: Truck },
];
const seguridadChecklist = [
'Valorar mecanismo lesional',
'Identificar riesgos: tráfico, fuego, químicos, electricidad',
'Usar EPI adecuado',
'Señalizar/balizar la zona',
'Solicitar recursos si es necesario',
'Establecer zona de seguridad',
'Acceso seguro a la víctima',
];
const abcdeContent = [
{
letter: 'A',
title: 'Airway - Vía Aérea',
points: [
'Permeabilidad vía aérea',
'Control cervical si trauma',
'Aspirar secreciones',
'Cánula orofaríngea si inconsciente',
],
},
{
letter: 'B',
title: 'Breathing - Respiración',
points: [
'FR, profundidad, simetría',
'SpO2',
'Auscultación',
'Oxigenoterapia si precisa',
],
},
{
letter: 'C',
title: 'Circulation - Circulación',
points: [
'FC, TA, relleno capilar',
'Control de hemorragias',
'Acceso venoso',
'Fluidoterapia si shock',
],
},
{
letter: 'D',
title: 'Disability - Neurológico',
points: [
'Nivel consciencia (AVDN/Glasgow)',
'Pupilas',
'Glucemia',
'Movilidad extremidades',
],
},
{
letter: 'E',
title: 'Exposure - Exposición',
points: [
'Desvestir para explorar',
'Prevenir hipotermia',
'Inspección completa',
'Buscar lesiones ocultas',
],
},
];
const triageStart = [
{ color: 'Negro', criteria: 'No respira tras apertura vía aérea', action: 'Fallecido / Expectante', colorClass: 'bg-foreground text-background' },
{ color: 'Rojo', criteria: 'FR >30 o <10, TRC >2s, no obedece órdenes', action: 'Prioridad 1 - Inmediato', colorClass: 'bg-primary text-primary-foreground' },
{ color: 'Amarillo', criteria: 'No puede caminar, pero estable', action: 'Prioridad 2 - Urgente', colorClass: 'bg-warning text-background' },
{ color: 'Verde', criteria: 'Puede caminar', action: 'Prioridad 3 - Demorado', colorClass: 'bg-success text-background' },
];
const Escena = () => {
const [activeTab, setActiveTab] = useState('seguridad');
const [checkedItems, setCheckedItems] = useState<Set<number>>(new Set());
const toggleCheck = (index: number) => {
const newChecked = new Set(checkedItems);
if (newChecked.has(index)) {
newChecked.delete(index);
} else {
newChecked.add(index);
}
setCheckedItems(newChecked);
};
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-foreground mb-1">
Actuación en Escena
</h1>
<p className="text-muted-foreground text-sm">
Seguridad, valoración y triage
</p>
</div>
{/* Tabs */}
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === tab.id
? 'bg-secondary text-secondary-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</div>
{/* Content */}
{activeTab === 'seguridad' && (
<div className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-4">
🛡 Checklist Seguridad Escena
</h3>
<div className="space-y-2">
{seguridadChecklist.map((item, index) => (
<button
key={index}
onClick={() => toggleCheck(index)}
className="w-full flex items-center gap-3 p-3 rounded-lg bg-muted hover:bg-accent transition-colors text-left"
>
<div
className={`w-6 h-6 rounded border-2 flex items-center justify-center transition-colors ${
checkedItems.has(index)
? 'bg-success border-success'
: 'border-muted-foreground'
}`}
>
{checkedItems.has(index) && (
<Check className="w-4 h-4 text-background" />
)}
</div>
<span
className={`text-foreground ${
checkedItems.has(index) ? 'line-through opacity-60' : ''
}`}
>
{item}
</span>
</button>
))}
</div>
<button
onClick={() => setCheckedItems(new Set())}
className="mt-4 text-sm text-muted-foreground hover:text-foreground"
>
Reiniciar checklist
</button>
</div>
)}
{activeTab === 'abcde' && (
<div className="space-y-4">
{abcdeContent.map((section) => (
<div key={section.letter} className="card-procedure">
<div className="flex items-center gap-3 mb-3">
<span className="step-number text-lg">{section.letter}</span>
<h3 className="font-bold text-foreground">{section.title}</h3>
</div>
<ul className="space-y-2 ml-11">
{section.points.map((point, i) => (
<li key={i} className="text-foreground flex items-start gap-2">
<span className="text-primary"></span>
<span>{point}</span>
</li>
))}
</ul>
</div>
))}
</div>
)}
{activeTab === 'triage' && (
<div className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-4">
🏥 Triage START (Adultos)
</h3>
<div className="space-y-3">
{triageStart.map((level) => (
<div
key={level.color}
className="flex items-stretch gap-3 rounded-lg overflow-hidden border border-border"
>
<div
className={`w-20 flex items-center justify-center font-bold text-sm ${level.colorClass}`}
>
{level.color}
</div>
<div className="flex-1 p-3">
<p className="text-foreground text-sm font-medium">
{level.criteria}
</p>
<p className="text-muted-foreground text-2xs mt-1">
{level.action}
</p>
</div>
</div>
))}
</div>
<p className="text-2xs text-muted-foreground mt-4">
JumpSTART para pediátricos: ajustar FR (15-45 normal en lactantes)
</p>
</div>
)}
{activeTab === 'inmovilizacion' && (
<div className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-4">
🦴 Inmovilización
</h3>
<div className="space-y-4">
<div>
<h4 className="font-semibold text-foreground mb-2">
Indicaciones de inmovilización espinal
</h4>
<ul className="space-y-1 text-foreground text-sm">
<li> Mecanismo lesional de riesgo</li>
<li> Dolor cervical o dorsolumbar</li>
<li> Déficit neurológico</li>
<li> Alteración nivel consciencia</li>
<li> Intoxicación que impide valoración</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-foreground mb-2">Material</h4>
<div className="flex flex-wrap gap-2">
<Badge variant="default">Collarín cervical</Badge>
<Badge variant="default">Tabla espinal</Badge>
<Badge variant="default">Dama de Elche</Badge>
<Badge variant="default">Férula de tracción</Badge>
<Badge variant="default">Férulas de vacío</Badge>
</div>
</div>
</div>
</div>
)}
{activeTab === 'extricacion' && (
<div className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-4">
🚗 Extricación Vehicular
</h3>
<div className="space-y-4">
<div>
<h4 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-2">
Maniobra de Rautek
</h4>
<ol className="space-y-2">
{[
'Aproximarse por detrás de la víctima',
'Pasar brazos bajo axilas',
'Sujetar antebrazo de víctima contra su pecho',
'Extraer arrastrando hacia atrás',
'Mantener alineación espinal',
].map((step, i) => (
<li key={i} className="flex items-start gap-3">
<span className="step-number">{i + 1}</span>
<span className="text-foreground pt-1">{step}</span>
</li>
))}
</ol>
</div>
</div>
</div>
)}
</div>
);
};
export default Escena;

114
src/pages/Farmacos.tsx Normal file
View file

@ -0,0 +1,114 @@
import { useState, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Search } from 'lucide-react';
import { drugs, Drug, DrugCategory } from '@/data/drugs';
import DrugCard from '@/components/drugs/DrugCard';
const categories: { id: DrugCategory | 'todos'; label: string }[] = [
{ id: 'todos', label: 'Todos' },
{ id: 'cardiovascular', label: 'Cardiovascular' },
{ id: 'respiratorio', label: 'Respiratorio' },
{ id: 'neurologico', label: 'Neurológico' },
{ id: 'analgesia', label: 'Analgesia' },
{ id: 'otros', label: 'Otros' },
];
const Farmacos = () => {
const [searchParams] = useSearchParams();
const highlightId = searchParams.get('id');
const [searchQuery, setSearchQuery] = useState('');
const [activeCategory, setActiveCategory] = useState<DrugCategory | 'todos'>('todos');
const filteredDrugs = useMemo(() => {
let result = [...drugs];
// Filter by category
if (activeCategory !== 'todos') {
result = result.filter((d) => d.category === activeCategory);
}
// Filter by search
if (searchQuery.length >= 2) {
const query = searchQuery.toLowerCase();
result = result.filter(
(d) =>
d.genericName.toLowerCase().includes(query) ||
d.tradeName.toLowerCase().includes(query) ||
d.indications.some((i) => i.toLowerCase().includes(query))
);
}
// Sort highlighted to top
if (highlightId) {
result.sort((a, b) => {
if (a.id === highlightId) return -1;
if (b.id === highlightId) return 1;
return 0;
});
}
return result;
}, [activeCategory, searchQuery, highlightId]);
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-foreground mb-1">Fármacos</h1>
<p className="text-muted-foreground text-sm">
Vademécum de emergencias
</p>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Buscar fármaco..."
className="w-full h-12 pl-12 pr-4 bg-card border border-border rounded-xl text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
{/* Category Tabs */}
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
activeCategory === cat.id
? 'bg-secondary text-secondary-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
{cat.label}
</button>
))}
</div>
{/* Drugs List */}
<div className="space-y-4">
{filteredDrugs.map((drug) => (
<DrugCard
key={drug.id}
drug={drug}
defaultExpanded={drug.id === highlightId}
/>
))}
</div>
{filteredDrugs.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground">
No se encontraron fármacos
</p>
</div>
)}
</div>
);
};
export default Farmacos;

137
src/pages/Herramientas.tsx Normal file
View file

@ -0,0 +1,137 @@
import { useState } from 'react';
import { Calculator, Table, AlertCircle } from 'lucide-react';
import GlasgowCalculator from '@/components/tools/GlasgowCalculator';
import InfusionTableView from '@/components/tools/InfusionTableView';
import { infusionTables } from '@/data/calculators';
import { Link } from 'react-router-dom';
const tabs = [
{ id: 'calculadoras', label: 'Calculadoras', icon: Calculator },
{ id: 'perfusiones', label: 'Perfusiones', icon: Table },
{ id: 'codigos', label: 'Códigos', icon: AlertCircle },
];
const codigosProtocolo = [
{
name: 'Código Ictus',
description: 'Activación ante sospecha de ictus agudo',
path: '/patologias?tab=neurologicas',
color: 'bg-secondary',
},
{
name: 'Código IAM',
description: 'SCACEST - Infarto con elevación ST',
path: '/patologias?tab=circulatorias',
color: 'bg-primary',
},
{
name: 'Código Sepsis',
description: 'Sospecha de sepsis severa / shock séptico',
path: '/soporte-vital',
color: 'bg-emergency-high',
},
{
name: 'Código Parada',
description: 'PCR - Parada cardiorrespiratoria',
path: '/soporte-vital?id=rcp-adulto-svb',
color: 'bg-primary',
},
];
const Herramientas = () => {
const [activeTab, setActiveTab] = useState('calculadoras');
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-foreground mb-1">Herramientas</h1>
<p className="text-muted-foreground text-sm">
Calculadoras, tablas y códigos
</p>
</div>
{/* Tabs */}
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === tab.id
? 'bg-secondary text-secondary-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</div>
{/* Content */}
{activeTab === 'calculadoras' && (
<div className="space-y-4">
<GlasgowCalculator />
{/* Placeholder for more calculators */}
<div className="card-procedure opacity-60">
<h3 className="font-bold text-foreground text-lg mb-2">
🔥 Fórmula de Parkland (Quemados)
</h3>
<p className="text-muted-foreground text-sm">
Próximamente disponible
</p>
</div>
<div className="card-procedure opacity-60">
<h3 className="font-bold text-foreground text-lg mb-2">
Dosis Pediátricas por Peso
</h3>
<p className="text-muted-foreground text-sm">
Próximamente disponible
</p>
</div>
</div>
)}
{activeTab === 'perfusiones' && (
<div className="space-y-4">
{infusionTables.map((table) => (
<InfusionTableView key={table.id} table={table} />
))}
</div>
)}
{activeTab === 'codigos' && (
<div className="space-y-3">
{codigosProtocolo.map((codigo) => (
<Link
key={codigo.name}
to={codigo.path}
className="block card-procedure hover:border-primary/50"
>
<div className="flex items-center gap-4">
<div
className={`w-12 h-12 rounded-lg ${codigo.color} flex items-center justify-center`}
>
<AlertCircle className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-bold text-foreground">{codigo.name}</h3>
<p className="text-muted-foreground text-sm">
{codigo.description}
</p>
</div>
</div>
</Link>
))}
</div>
)}
</div>
);
};
export default Herramientas;

View file

@ -1,14 +1,140 @@
// Update this page (the content is just a fallback if you fail to update the page) import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Search,
Heart,
Brain,
Zap,
Wind,
Clock,
ChevronRight,
AlertTriangle,
} from 'lucide-react';
import EmergencyButton from '@/components/shared/EmergencyButton';
const Index = () => { const recentSearches = [
{ id: 'rcp-adulto-svb', title: 'RCP Adulto SVB', type: 'procedure' },
{ id: 'adrenalina', title: 'Adrenalina', type: 'drug' },
{ id: 'shock-hemorragico', title: 'Shock Hemorrágico', type: 'procedure' },
];
const quickAccess = [
{ label: 'OVACE', path: '/soporte-vital?id=obstruccion-via-aerea' },
{ label: 'Glasgow', path: '/herramientas' },
{ label: 'Triage', path: '/escena' },
{ label: 'Código Ictus', path: '/patologias' },
{ label: 'Dopamina', path: '/herramientas' },
{ label: 'Politrauma', path: '/soporte-vital' },
];
interface HomeProps {
onSearchClick: () => void;
}
const Home = ({ onSearchClick }: HomeProps) => {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="space-y-6">
<div className="text-center"> {/* Search Bar */}
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1> <button
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p> onClick={onSearchClick}
</div> className="w-full flex items-center gap-4 p-4 bg-card border border-border rounded-xl hover:border-primary/50 transition-colors"
>
<Search className="w-6 h-6 text-muted-foreground" />
<span className="text-muted-foreground">
Buscar protocolo, fármaco, calculadora...
</span>
</button>
{/* Emergency Buttons Grid */}
<section>
<h2 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-3">
Emergencias Críticas
</h2>
<div className="grid grid-cols-2 gap-3">
<EmergencyButton
to="/soporte-vital?id=rcp-adulto-svb"
icon={Heart}
title="RCP / Parada"
subtitle="Adulto y Pediátrico"
variant="critical"
/>
<EmergencyButton
to="/patologias?tab=neurologicas"
icon={Brain}
title="Código Ictus"
variant="high"
/>
<EmergencyButton
to="/soporte-vital?id=shock-hemorragico"
icon={Zap}
title="Shock"
subtitle="Hemorrágico"
variant="medium"
/>
<EmergencyButton
to="/soporte-vital?id=obstruccion-via-aerea"
icon={Wind}
title="Vía Aérea"
subtitle="OVACE / IOT"
variant="critical"
/>
</div>
</section>
{/* Quick Access Chips */}
<section>
<h2 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide mb-3">
Accesos Rápidos
</h2>
<div className="flex flex-wrap gap-2">
{quickAccess.map((item) => (
<Link
key={item.label}
to={item.path}
className="px-4 py-2 bg-muted hover:bg-accent text-foreground rounded-full text-sm font-medium transition-colors"
>
{item.label}
</Link>
))}
</div>
</section>
{/* Recent Searches */}
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-muted-foreground text-sm uppercase tracking-wide">
Últimas Consultas
</h2>
<Clock className="w-4 h-4 text-muted-foreground" />
</div>
<div className="space-y-2">
{recentSearches.map((item) => (
<Link
key={item.id}
to={
item.type === 'procedure'
? `/soporte-vital?id=${item.id}`
: `/farmacos?id=${item.id}`
}
className="flex items-center justify-between p-3 bg-card border border-border rounded-lg hover:border-primary/50 transition-colors"
>
<span className="text-foreground">{item.title}</span>
<ChevronRight className="w-5 h-5 text-muted-foreground" />
</Link>
))}
</div>
</section>
{/* Floating Emergency Button */}
<Link
to="/soporte-vital?id=rcp-adulto-svb"
className="fixed bottom-24 right-4 z-40 w-16 h-16 rounded-full bg-primary flex items-center justify-center shadow-lg animate-pulse-ring"
aria-label="Emergencia - RCP"
>
<AlertTriangle className="w-8 h-8 text-primary-foreground" />
</Link>
</div> </div>
); );
}; };
export default Index; export default Home;

147
src/pages/Patologias.tsx Normal file
View file

@ -0,0 +1,147 @@
import { useState } from 'react';
import { Wind, Heart, Brain, FlaskConical, Skull } from 'lucide-react';
const tabs = [
{ id: 'respiratorias', label: 'Respiratorias', icon: Wind },
{ id: 'circulatorias', label: 'Circulatorias', icon: Heart },
{ id: 'neurologicas', label: 'Neurológicas', icon: Brain },
{ id: 'endocrinas', label: 'Endocrinas', icon: FlaskConical },
{ id: 'intoxicaciones', label: 'Intoxicaciones', icon: Skull },
];
const patologias = {
respiratorias: [
{
title: 'Crisis Asmática',
clinica: 'Disnea, sibilancias, uso de musculatura accesoria, habla entrecortada',
actuacion: ['O2 alto flujo', 'Salbutamol nebulizado', 'Corticoides IV', 'Valorar adrenalina si severa'],
},
{
title: 'EPOC Reagudizado',
clinica: 'Aumento de disnea, cambio en esputo, aumento de tos',
actuacion: ['O2 controlado (SpO2 88-92%)', 'Broncodilatadores', 'Corticoides', 'ATB si sospecha infección'],
},
],
circulatorias: [
{
title: 'SCA / IAM',
clinica: 'Dolor torácico opresivo, irradiado a brazo/mandíbula, sudoración, náuseas',
actuacion: ['ECG 12 derivaciones', 'O2 si SpO2 <94%', 'AAS 300mg', 'Nitroglicerina SL', 'Morfina si dolor intenso'],
},
{
title: 'Edema Agudo de Pulmón',
clinica: 'Disnea súbita, ortopnea, esputo rosado, crepitantes',
actuacion: ['Posición semisentado', 'O2 alto flujo / CPAP', 'Furosemida IV', 'Nitroglicerina', 'Morfina'],
},
],
neurologicas: [
{
title: 'Ictus - Código Ictus',
clinica: 'Déficit neurológico súbito: paresia facial, debilidad brazo, alteración habla (FAST)',
actuacion: ['Hora de inicio síntomas', 'Glucemia', 'TA (no bajar si <220/120)', 'Código Ictus', 'Traslado urgente'],
},
{
title: 'Crisis Convulsiva',
clinica: 'Movimientos tónico-clónicos, pérdida consciencia, relajación esfínteres',
actuacion: ['Proteger de traumatismos', 'Posición lateral si cede', 'Midazolam si >5min', 'O2', 'Glucemia'],
},
],
endocrinas: [
{
title: 'Hipoglucemia',
clinica: 'Glucemia <70 mg/dl, sudoración, temblor, confusión, taquicardia',
actuacion: ['Si consciente: glucosa oral', 'Si inconsciente: Glucosmon IV', 'Glucagón IM si no vía', 'Buscar causa'],
},
{
title: 'Cetoacidosis Diabética',
clinica: 'Hiperglucemia >250, aliento cetósico, náuseas, dolor abdominal',
actuacion: ['Fluidoterapia SSF', 'Insulina rápida', 'Monitorización K+', 'Buscar desencadenante'],
},
],
intoxicaciones: [
{
title: 'Intoxicación por Opioides',
clinica: 'Miosis puntiforme, depresión respiratoria, bajo nivel consciencia',
actuacion: ['Vía aérea', 'Ventilación', 'Naloxona 0.4-2mg IV', 'Repetir cada 2-3 min si precisa'],
},
{
title: 'Intoxicación por Benzodiacepinas',
clinica: 'Somnolencia, ataxia, habla farfullante, depresión respiratoria',
actuacion: ['Vía aérea', 'Flumazenilo 0.2mg IV', 'Repetir hasta 1mg', 'Monitorización prolongada'],
},
],
};
const Patologias = () => {
const [activeTab, setActiveTab] = useState('respiratorias');
const currentPatologias = patologias[activeTab as keyof typeof patologias] || [];
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-foreground mb-1">Patologías</h1>
<p className="text-muted-foreground text-sm">
Clínica y actuación por sistemas
</p>
</div>
{/* System Tabs */}
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === tab.id
? 'bg-secondary text-secondary-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</div>
{/* Patologies List */}
<div className="space-y-4">
{currentPatologias.map((patologia, index) => (
<div key={index} className="card-procedure">
<h3 className="font-bold text-foreground text-lg mb-3">
{patologia.title}
</h3>
<div className="space-y-4">
<div>
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
Clínica
</h4>
<p className="text-foreground">{patologia.clinica}</p>
</div>
<div>
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
Actuación
</h4>
<ol className="space-y-2">
{patologia.actuacion.map((paso, i) => (
<li key={i} className="flex items-start gap-3">
<span className="step-number">{i + 1}</span>
<span className="text-foreground pt-1">{paso}</span>
</li>
))}
</ol>
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default Patologias;

View file

@ -0,0 +1,80 @@
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { procedures, getProceduresByCategory, Procedure } from '@/data/procedures';
import ProcedureCard from '@/components/procedures/ProcedureCard';
const subcategories = [
{ id: 'todos', label: 'Todos' },
{ id: 'rcp', label: 'RCP' },
{ id: 'via_aerea', label: 'Vía Aérea' },
{ id: 'shock', label: 'Shock' },
];
const SoporteVital = () => {
const [searchParams] = useSearchParams();
const highlightId = searchParams.get('id');
const [activeTab, setActiveTab] = useState('todos');
const soporteVitalProcedures = getProceduresByCategory('soporte_vital');
const filteredProcedures =
activeTab === 'todos'
? soporteVitalProcedures
: soporteVitalProcedures.filter((p) => p.subcategory === activeTab);
// Sort to put highlighted procedure first
const sortedProcedures = [...filteredProcedures].sort((a, b) => {
if (a.id === highlightId) return -1;
if (b.id === highlightId) return 1;
return 0;
});
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-foreground mb-1">Soporte Vital</h1>
<p className="text-muted-foreground text-sm">
Protocolos de emergencia y reanimación
</p>
</div>
{/* Subcategory Tabs */}
<div className="flex gap-2 overflow-x-auto scrollbar-hide -mx-4 px-4">
{subcategories.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Procedures List */}
<div className="space-y-4">
{sortedProcedures.map((procedure) => (
<ProcedureCard
key={procedure.id}
procedure={procedure}
defaultExpanded={procedure.id === highlightId}
/>
))}
</div>
{sortedProcedures.length === 0 && (
<div className="text-center py-12">
<p className="text-muted-foreground">
No hay protocolos en esta categoría
</p>
</div>
)}
</div>
);
};
export default SoporteVital;

View file

@ -7,7 +7,7 @@ export default {
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: "1rem",
screens: { screens: {
"2xl": "1400px", "2xl": "1400px",
}, },
@ -57,33 +57,58 @@ export default {
border: "hsl(var(--sidebar-border))", border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))", ring: "hsl(var(--sidebar-ring))",
}, },
emergency: {
critical: "hsl(var(--emergency-critical))",
high: "hsl(var(--emergency-high))",
medium: "hsl(var(--emergency-medium))",
low: "hsl(var(--emergency-low))",
},
success: "hsl(var(--success))",
warning: "hsl(var(--warning))",
info: "hsl(var(--info))",
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 4px)",
}, },
fontFamily: {
sans: ["IBM Plex Sans", "system-ui", "-apple-system", "sans-serif"],
},
fontSize: {
"2xs": ["0.625rem", { lineHeight: "0.875rem" }],
},
minHeight: {
touch: "48px",
"touch-lg": "60px",
},
spacing: {
safe: "env(safe-area-inset-bottom)",
},
keyframes: { keyframes: {
"accordion-down": { "accordion-down": {
from: { from: { height: "0" },
height: "0", to: { height: "var(--radix-accordion-content-height)" },
},
to: {
height: "var(--radix-accordion-content-height)",
},
}, },
"accordion-up": { "accordion-up": {
from: { from: { height: "var(--radix-accordion-content-height)" },
height: "var(--radix-accordion-content-height)", to: { height: "0" },
}, },
to: { pulse: {
height: "0", "0%, 100%": { opacity: "1" },
}, "50%": { opacity: "0.5" },
},
"pulse-ring": {
"0%": { transform: "scale(0.95)", boxShadow: "0 0 0 0 hsl(var(--emergency-critical) / 0.7)" },
"70%": { transform: "scale(1)", boxShadow: "0 0 0 10px hsl(var(--emergency-critical) / 0)" },
"100%": { transform: "scale(0.95)", boxShadow: "0 0 0 0 hsl(var(--emergency-critical) / 0)" },
}, },
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
"pulse-ring": "pulse-ring 2s infinite",
}, },
}, },
}, },