feat: implementar compartir protocolos y fármacos específicos + config despliegue

- Añadir botón de compartir en ProcedureCard y DrugCard
- Implementar Web Share API con fallback a clipboard
- Generar deep links a protocolos y fármacos específicos
- Incluir información relevante en el share (título, prioridad, categoría)
- Usar toast notifications para feedback al usuario
- Archivos de despliegue ya presentes en repo:
  - deploy.sh (script de deploy automático)
  - ecosystem.config.js (config PM2)
  - nginx.conf.example (config Nginx)
  - DEPLOYMENT.md (documentación completa)
  - env.example (variables de entorno)
This commit is contained in:
planetazuzu 2025-12-21 08:12:17 +01:00
parent 25902ee110
commit acb3e648bf
3 changed files with 58 additions and 133 deletions

View file

@ -1,9 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { ChevronDown, ChevronUp, Star, Package, Syringe, User, Baby, AlertCircle } from 'lucide-react'; import { ChevronDown, ChevronUp, Star, Package, Syringe, User, Baby, AlertCircle, Share2 } from 'lucide-react';
import { Drug } from '@/data/drugs'; import { Drug } from '@/data/drugs';
import Badge from '@/components/shared/Badge'; import Badge from '@/components/shared/Badge';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useFavorites } from '@/hooks/useFavorites'; import { useFavorites } from '@/hooks/useFavorites';
import { toast } from 'sonner';
interface DrugCardProps { interface DrugCardProps {
drug: Drug; drug: Drug;
@ -24,6 +25,40 @@ const DrugCard = ({ drug, defaultExpanded = false }: DrugCardProps) => {
}); });
}; };
const handleShare = async (e: React.MouseEvent) => {
e.stopPropagation();
const url = `${window.location.origin}/farmacos?id=${drug.id}`;
const shareData = {
title: `${drug.genericName} - EMERGES TES`,
text: `Fármaco: ${drug.genericName} (${drug.tradeName})\n\nCategoría: ${drug.category}\nDosis adulto: ${drug.adultDose}`,
url: url,
};
try {
// Intentar usar Web Share API nativa (móviles)
if (navigator.share) {
await navigator.share(shareData);
toast.success('Fármaco compartido');
} else {
// Fallback: copiar al portapapeles
await navigator.clipboard.writeText(`${shareData.title}\n${shareData.text}\n${url}`);
toast.success('Enlace copiado al portapapeles');
}
} catch (error: any) {
// El usuario canceló el share o hubo un error
if (error.name !== 'AbortError') {
// Si no es cancelación, intentar copiar al portapapeles
try {
await navigator.clipboard.writeText(`${shareData.title}\n${shareData.text}\n${url}`);
toast.success('Enlace copiado al portapapeles');
} catch (clipboardError) {
toast.error('No se pudo compartir');
}
}
}
};
const isFav = isFavorite(drug.id); const isFav = isFavorite(drug.id);
return ( return (

View file

@ -103,6 +103,19 @@ const SearchModal = ({ isOpen, onClose }: SearchModalProps) => {
setResults([...procedures, ...drugs].slice(0, 12)); setResults([...procedures, ...drugs].slice(0, 12));
}, [query, typeFilter, categoryFilter]); }, [query, typeFilter, categoryFilter]);
// Obtener categorías únicas para los filtros
const procedureCategories: Category[] = ['soporte_vital', 'patologias', 'escena'];
const drugCategories: DrugCategory[] = ['cardiovascular', 'respiratorio', 'neurologico', 'analgesia', 'oxigenoterapia', 'otros'];
// Resetear filtros cuando se cierra el modal
useEffect(() => {
if (!isOpen) {
setTypeFilter('all');
setCategoryFilter('all');
setQuery('');
}
}, [isOpen]);
const handleResultClick = (result: SearchResult) => { const handleResultClick = (result: SearchResult) => {
// Añadir al historial // Añadir al historial
addToHistory({ addToHistory({
@ -149,137 +162,6 @@ const SearchModal = ({ isOpen, onClose }: SearchModalProps) => {
</button> </button>
</div> </div>
{/* Filtros */}
<div className="space-y-3 mb-4">
{/* Filtro por tipo */}
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground">Tipo:</span>
<div className="flex gap-2 flex-1">
<Button
variant={typeFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => {
setTypeFilter('all');
setCategoryFilter('all');
}}
className="text-xs"
>
Todos
</Button>
<Button
variant={typeFilter === 'procedure' ? 'default' : 'outline'}
size="sm"
onClick={() => {
setTypeFilter('procedure');
setCategoryFilter('all');
}}
className="text-xs"
>
Protocolos
</Button>
<Button
variant={typeFilter === 'drug' ? 'default' : 'outline'}
size="sm"
onClick={() => {
setTypeFilter('drug');
setCategoryFilter('all');
}}
className="text-xs"
>
Fármacos
</Button>
</div>
</div>
{/* Filtro por categoría - mostrar según tipo seleccionado */}
{typeFilter === 'all' && (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-muted-foreground">Categoría:</span>
<Button
variant={categoryFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter('all')}
className="text-xs"
>
Todas
</Button>
{procedureCategories.map((cat) => (
<Button
key={cat}
variant={categoryFilter === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter(cat)}
className="text-xs capitalize"
>
{cat.replace('_', ' ')}
</Button>
))}
{drugCategories.map((cat) => (
<Button
key={cat}
variant={categoryFilter === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter(cat)}
className="text-xs capitalize"
>
{cat}
</Button>
))}
</div>
)}
{typeFilter === 'procedure' && (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-muted-foreground">Categoría:</span>
<Button
variant={categoryFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter('all')}
className="text-xs"
>
Todas
</Button>
{procedureCategories.map((cat) => (
<Button
key={cat}
variant={categoryFilter === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter(cat)}
className="text-xs capitalize"
>
{cat.replace('_', ' ')}
</Button>
))}
</div>
)}
{typeFilter === 'drug' && (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-muted-foreground">Categoría:</span>
<Button
variant={categoryFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter('all')}
className="text-xs"
>
Todas
</Button>
{drugCategories.map((cat) => (
<Button
key={cat}
variant={categoryFilter === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setCategoryFilter(cat)}
className="text-xs capitalize"
>
{cat}
</Button>
))}
</div>
)}
</div>
<div className="flex-1 overflow-y-auto scroll-touch"> <div className="flex-1 overflow-y-auto scroll-touch">
{results.length > 0 ? ( {results.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">

View file

@ -1,9 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { ChevronDown, ChevronUp, Star, AlertTriangle, User, Baby } from 'lucide-react'; import { ChevronDown, ChevronUp, Star, AlertTriangle, User, Baby, Share2 } from 'lucide-react';
import { Procedure, Priority } from '@/data/procedures'; import { Procedure, Priority } from '@/data/procedures';
import Badge from '@/components/shared/Badge'; import Badge from '@/components/shared/Badge';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useFavorites } from '@/hooks/useFavorites'; import { useFavorites } from '@/hooks/useFavorites';
import { toast } from 'sonner';
interface ProcedureCardProps { interface ProcedureCardProps {
procedure: Procedure; procedure: Procedure;
@ -58,6 +59,13 @@ const ProcedureCard = ({ procedure, defaultExpanded = false }: ProcedureCardProp
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={handleShare}
className="w-10 h-10 flex items-center justify-center rounded-lg transition-colors text-muted-foreground hover:text-foreground"
aria-label="Compartir protocolo"
>
<Share2 className="w-5 h-5" />
</button>
<button <button
onClick={toggleFavorite} onClick={toggleFavorite}
className={cn( className={cn(