diff --git a/src/components/AedDetailModal.tsx b/src/components/AedDetailModal.tsx
index 4290ae6..b01a34e 100644
--- a/src/components/AedDetailModal.tsx
+++ b/src/components/AedDetailModal.tsx
@@ -19,9 +19,17 @@ interface AedDetailModalProps {
aed: Aed | null;
isOpen: boolean;
onClose: () => void;
+ onDirectionsClick?: () => void;
+ onViewDuration?: () => void;
}
-export default function AedDetailModal({ aed, isOpen, onClose }: AedDetailModalProps) {
+export default function AedDetailModal({
+ aed,
+ isOpen,
+ onClose,
+ onDirectionsClick,
+ onViewDuration,
+}: AedDetailModalProps) {
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const { trackDeaImageView, trackDeaPhoneClick, trackExternalLink, trackButtonClick } =
useAnalytics();
@@ -31,6 +39,14 @@ export default function AedDetailModal({ aed, isOpen, onClose }: AedDetailModalP
setSelectedImageIndex(0);
}, [aed?.id]);
+ // Trigger app download prompt after viewing for 5 seconds
+ const aedId = aed?.id;
+ useEffect(() => {
+ if (!isOpen || !aedId || !onViewDuration) return;
+ const timer = setTimeout(onViewDuration, 5000);
+ return () => clearTimeout(timer);
+ }, [isOpen, aedId, onViewDuration]);
+
const handleImageChange = useCallback(
(index: number) => {
if (aed) {
@@ -57,8 +73,9 @@ export default function AedDetailModal({ aed, isOpen, onClose }: AedDetailModalP
"Cómo llegar",
"dea_modal"
);
+ onDirectionsClick?.();
}
- }, [aed, trackExternalLink]);
+ }, [aed, trackExternalLink, onDirectionsClick]);
const handleEmailClick = useCallback(
(_email: string) => {
@@ -82,7 +99,7 @@ export default function AedDetailModal({ aed, isOpen, onClose }: AedDetailModalP
return (
("desktop");
+
+ useEffect(() => {
+ const ua = navigator.userAgent.toLowerCase();
+ if (/iphone|ipad|ipod/.test(ua)) {
+ setPlatform("ios");
+ } else if (/android/.test(ua)) {
+ setPlatform("android");
+ }
+ }, []);
+
+ return platform;
+}
+
+/* ================================================================
+ Contextual App Download Prompt — hook + component
+ ================================================================ */
+
+export type PromptContext = "search_results" | "dea_detail" | "directions" | "geolocation";
+
+const PROMPT_MESSAGES: Record<
+ PromptContext,
+ { title: string; subtitle: string; icon: typeof Download }
+> = {
+ search_results: {
+ title: "Encuentra DEAs mas rapido",
+ subtitle: "Busca desfibriladores al instante con la app",
+ icon: MapPin,
+ },
+ dea_detail: {
+ title: "Navega hasta este DEA",
+ subtitle: "La app te guia paso a paso hasta el desfibrilador",
+ icon: NavigationIcon,
+ },
+ directions: {
+ title: "Mejor navegacion con la app",
+ subtitle: "Te lleva directamente al DEA mas cercano",
+ icon: NavigationIcon,
+ },
+ geolocation: {
+ title: "Tu ubicacion, siempre lista",
+ subtitle: "GPS rapido y preciso con la app nativa",
+ icon: Wifi,
+ },
+};
+
+const COOLDOWN_24H = 24 * 60 * 60 * 1000;
+const COOLDOWN_30D = 30 * 24 * 60 * 60 * 1000;
+
+// Session-level flag: only show 1 prompt per page session
+let sessionPromptShown = false;
+
+function isCoolingDown(context: PromptContext): boolean {
+ try {
+ // Permanent dismiss check
+ const permanent = localStorage.getItem("app-prompt-permanent-dismiss");
+ if (permanent) {
+ const ts = parseInt(permanent, 10);
+ if (Date.now() - ts < COOLDOWN_30D) return true;
+ }
+ // Per-context 24h cooldown
+ const contextTs = localStorage.getItem(`app-prompt-dismiss-${context}`);
+ if (contextTs) {
+ const ts = parseInt(contextTs, 10);
+ if (Date.now() - ts < COOLDOWN_24H) return true;
+ }
+ // Smart banner dismiss doesn't block contextual prompts
+ } catch {
+ return false;
+ }
+ return false;
+}
+
+/**
+ * Hook to manage contextual app download prompts.
+ * Returns a trigger function and the component to render.
+ */
+export function useAppDownloadPrompt() {
+ const [visible, setVisible] = useState(false);
+ const [context, setContext] = useState
(null);
+ const platform = useDevicePlatform();
+ const timerRef = useRef | null>(null);
+
+ const trigger = useCallback(
+ (ctx: PromptContext, delayMs = 0) => {
+ if (platform === "desktop") return;
+ if (sessionPromptShown) return;
+ if (isCoolingDown(ctx)) return;
+
+ const show = () => {
+ // Re-check in case something changed during delay
+ if (sessionPromptShown) return;
+ sessionPromptShown = true;
+ setContext(ctx);
+ setVisible(true);
+ };
+
+ if (delayMs > 0) {
+ timerRef.current = setTimeout(show, delayMs);
+ } else {
+ show();
+ }
+ },
+ [platform]
+ );
+
+ const dismiss = useCallback(
+ (permanent = false) => {
+ setVisible(false);
+ if (context) {
+ localStorage.setItem(`app-prompt-dismiss-${context}`, String(Date.now()));
+ }
+ if (permanent) {
+ localStorage.setItem("app-prompt-permanent-dismiss", String(Date.now()));
+ }
+ },
+ [context]
+ );
+
+ const cancelPending = useCallback(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ }, []);
+
+ // Cleanup timer on unmount
+ useEffect(() => {
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ };
+ }, []);
+
+ return { trigger, dismiss, cancelPending, visible, context, platform };
+}
+
+/**
+ * Bottom-sheet style prompt that appears over content on mobile.
+ */
+export function AppDownloadPrompt({
+ visible,
+ context,
+ platform,
+ onDismiss,
+}: {
+ visible: boolean;
+ context: PromptContext | null;
+ platform: "ios" | "android" | "desktop";
+ onDismiss: (permanent?: boolean) => void;
+}) {
+ const { trackExternalLink, trackButtonClick } = useAnalytics();
+
+ if (!visible || !context || platform === "desktop") return null;
+
+ const msg = PROMPT_MESSAGES[context];
+ const Icon = msg.icon;
+ const storeUrl = platform === "ios" ? APP_STORE_URL : PLAY_STORE_URL;
+ const storeName = platform === "ios" ? "App Store" : "Google Play";
+
+ return (
+
+ {/* Backdrop */}
+
{
+ trackButtonClick("app_prompt_backdrop_dismiss", context);
+ onDismiss();
+ }}
+ />
+
+ {/* Bottom sheet */}
+
+
+ {/* Drag handle */}
+
+
+ {/* Close button */}
+
{
+ trackButtonClick("app_prompt_close", context);
+ onDismiss();
+ }}
+ className="absolute top-3 right-3 p-1.5 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
+ aria-label="Cerrar"
+ >
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Sticky smart banner for mobile users — appears after 3s with app store styling.
+ * Dismissible with localStorage persistence.
+ */
+export function AppSmartBanner() {
+ const [show, setShow] = useState(false);
+ const platform = useDevicePlatform();
+ const { trackExternalLink, trackButtonClick } = useAnalytics();
+
+ useEffect(() => {
+ if (platform === "desktop") return;
+ const dismissedAt = localStorage.getItem("app-banner-dismissed");
+ if (dismissedAt) {
+ const ts = parseInt(dismissedAt, 10);
+ if (Date.now() - ts < COOLDOWN_24H) return;
+ }
+
+ const timer = setTimeout(() => setShow(true), 3000);
+ return () => clearTimeout(timer);
+ }, [platform]);
+
+ if (!show || platform === "desktop") return null;
+
+ const storeUrl = platform === "ios" ? APP_STORE_URL : PLAY_STORE_URL;
+ const storeName = platform === "ios" ? "App Store" : "Google Play";
+ const isIos = platform === "ios";
+
+ const handleDismiss = () => {
+ setShow(false);
+ localStorage.setItem("app-banner-dismissed", String(Date.now()));
+ trackButtonClick("smart_banner_dismiss", "smart_banner");
+ };
+
+ return (
+
+ {/* Store-style banner */}
+
+
+ );
+}
+
+/**
+ * Full promotional section with store badges — for the home page info area.
+ */
+export function AppDownloadSection() {
+ const { trackExternalLink } = useAnalytics();
+
+ return (
+
+
+ {/* Decorative background elements */}
+
+
+
+
+ {/* Text content */}
+
+
+
+ DISPONIBLE EN TU MOVIL
+
+
+
+ Lleva DeaMap en tu bolsillo
+
+
+
+ Encuentra el desfibrilador mas cercano al instante.
+
+
+
+
+
+ Busqueda por GPS en tiempo real
+
+
+
+ Navegacion hasta el DEA mas cercano
+
+
+
+ Funciona sin conexion
+
+
+
+ {/* Store badges */}
+
+
+
+ {/* Phone mockup */}
+
+
+ {/* Phone frame */}
+
+ {/* Notch */}
+
+ {/* Screen */}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Compact store links for the footer.
+ */
+export function AppStoreFooterLinks() {
+ const { trackExternalLink } = useAnalytics();
+
+ return (
+
+
App Movil
+
Descarga DeaMap gratis
+
+
+ );
+}
+
+/* ---------- SVG Store Badges ---------- */
+
+function AppStoreBadge({ className = "h-11" }: { className?: string }) {
+ return (
+
+
+
+
+ Disponible en
+
+
+ App Store
+
+ {/* Apple logo simplified */}
+
+
+
+
+ );
+}
+
+function GooglePlayBadge({ className = "h-11" }: { className?: string }) {
+ return (
+
+
+
+
+ DISPONIBLE EN
+
+
+ Google Play
+
+ {/* Play triangle */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index c303217..fa3535d 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -3,6 +3,7 @@
import { ExternalLink, MapPin, PlusCircle } from "lucide-react";
import Link from "next/link";
+import { AppStoreFooterLinks } from "@/components/AppDownloadBanner";
import { useAnalytics } from "@/hooks/useAnalytics";
export default function Footer() {
@@ -11,7 +12,7 @@ export default function Footer() {
return (
-
+
{/* DeaMap Info */}
DeaMap
@@ -42,6 +43,9 @@ export default function Footer() {
Mejorando la respuesta ante emergencias
+ {/* App Móvil */}
+
+
{/* Enlaces rápidos */}
Enlaces