e0fb77d494
Closes #424
192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
// =============================================================================
|
|
// 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<React.SetStateAction<string>>;
|
|
shareSelectedDays: number;
|
|
setShareSelectedDays: React.Dispatch<React.SetStateAction<number>>;
|
|
shareGenerating: boolean;
|
|
shareLink: string | null;
|
|
setShareLink: React.Dispatch<React.SetStateAction<string | null>>;
|
|
shareCopied: boolean;
|
|
setShareCopied: React.Dispatch<React.SetStateAction<boolean>>;
|
|
openShareDialog: (meds: Medication[]) => void;
|
|
generateShareLink: () => Promise<void>;
|
|
copyShareLink: () => void;
|
|
closeShareDialog: () => void;
|
|
resetShareDialogState: () => void;
|
|
}
|
|
|
|
export function useShare(): UseShareReturn {
|
|
const [showShareDialog, setShowShareDialog] = useState(false);
|
|
const [sharePeople, setSharePeople] = useState<string[]>([]);
|
|
const [shareSelectedPerson, setShareSelectedPerson] = useState<string>("");
|
|
const [shareSelectedDays, setShareSelectedDays] = useState<number>(30);
|
|
const [shareGenerating, setShareGenerating] = useState(false);
|
|
const [shareLink, setShareLink] = useState<string | null>(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,
|
|
};
|
|
}
|