// ============================================================================= // useShare Hook - Share dialog state and operations // ============================================================================= import { useCallback, useState } from "react"; import type { Medication } from "../types"; import { withCorrelation } from "../utils/correlation"; import { log } from "../utils/logger"; const SHARE_ALL_VALUE = "all"; export interface UseShareReturn { showShareDialog: boolean; sharePeople: string[]; shareSelectedPerson: string; setShareSelectedPerson: React.Dispatch>; shareSelectedDays: number; setShareSelectedDays: React.Dispatch>; shareGenerating: boolean; shareLink: string | null; setShareLink: React.Dispatch>; shareCopied: boolean; setShareCopied: React.Dispatch>; openShareDialog: (meds: Medication[]) => void; generateShareLink: () => Promise; copyShareLink: () => void; closeShareDialog: () => void; resetShareDialogState: () => void; } export function useShare(): UseShareReturn { const [showShareDialog, setShowShareDialog] = useState(false); const [sharePeople, setSharePeople] = useState([]); const [shareSelectedPerson, setShareSelectedPerson] = useState(""); const [shareSelectedDays, setShareSelectedDays] = useState(30); const [shareGenerating, setShareGenerating] = useState(false); const [shareLink, setShareLink] = useState(null); const [shareCopied, setShareCopied] = useState(false); const openShareDialog = useCallback((meds: Medication[]) => { setShowShareDialog(true); window.history.pushState({ modal: "share" }, ""); setShareLink(null); setShareCopied(false); setShareSelectedPerson(""); setShareSelectedDays(30); // Include both per-intake assignments and legacy medication-level assignments. const uniquePeople = [ ...new Set( meds.flatMap((medication) => [ ...(medication.intakes ?.map((intake) => intake.takenBy) .filter((person): person is string => Boolean(person)) ?? []), ...(medication.takenBy || []), ]) ), ] .filter(Boolean) .sort(); setSharePeople(uniquePeople.length > 0 ? [SHARE_ALL_VALUE, ...uniquePeople] : []); log.info("[ShareDialog] Opened", { medicationCount: meds.length, personCount: uniquePeople.length }); if (uniquePeople.length > 0) { setShareSelectedPerson(uniquePeople[0]); } }, []); const generateShareLink = useCallback(async () => { if (!shareSelectedPerson) { log.warn("[ShareDialog] Attempted to generate link without selected person"); return; } setShareGenerating(true); setShareCopied(false); try { const { correlationId, init } = withCorrelation( { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ takenBy: shareSelectedPerson, scheduleDays: shareSelectedDays, }), }, "fe-share" ); const res = await fetch("/api/share", init); if (res.ok) { const data = await res.json(); const fullUrl = `${window.location.origin}/share/${data.token}`; setShareLink(fullUrl); log.info("[ShareDialog] Share link ready", { person: shareSelectedPerson, days: shareSelectedDays, reused: Boolean(data.reused), correlationId, }); } else { const err = await res.json(); log.error("[ShareDialog] Failed to generate share link", { status: res.status, person: shareSelectedPerson, error: err.error, correlationId, }); alert(err.error || "Failed to generate share link"); } } catch (error) { log.error("[ShareDialog] Share link request threw error", { person: shareSelectedPerson, error }); alert("Failed to generate share link"); } finally { setShareGenerating(false); } }, [shareSelectedPerson, shareSelectedDays]); const copyShareLink = useCallback(() => { if (shareLink) { if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(shareLink).then( () => { setShareCopied(true); log.debug("[ShareDialog] Share link copied to clipboard"); setTimeout(() => setShareCopied(false), 2000); }, () => { // Clipboard API blocked (non-secure context / permissions) fallbackCopyToClipboard(shareLink); } ); } else { fallbackCopyToClipboard(shareLink); } } function fallbackCopyToClipboard(text: string) { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); setShareCopied(true); log.debug("[ShareDialog] Share link copied via fallback"); setTimeout(() => setShareCopied(false), 2000); } catch { log.warn("[ShareDialog] Clipboard copy failed — not in secure context"); } finally { document.body.removeChild(textarea); } } }, [shareLink]); const closeShareDialog = useCallback(() => { if (showShareDialog) { log.debug("[ShareDialog] Closing dialog"); window.history.back(); } }, [showShareDialog]); // Internal function to reset share dialog state (called by popstate handler) const resetShareDialogState = useCallback(() => { log.debug("[ShareDialog] Reset dialog state"); setShowShareDialog(false); setShareLink(null); setShareCopied(false); }, []); return { showShareDialog, sharePeople, shareSelectedPerson, setShareSelectedPerson, shareSelectedDays, setShareSelectedDays, shareGenerating, shareLink, setShareLink, shareCopied, setShareCopied, openShareDialog, generateShareLink, copyShareLink, closeShareDialog, resetShareDialogState, }; }