"use client"; import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { UserPlus, X, Link2, Copy, Check } from "lucide-react"; import { toast } from "sonner"; import QRCode from "qrcode"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; type Role = "viewer" | "editor"; interface Member { id: string; userId: string; role: Role; createdAt: string; user: { name: string; username: string | null; avatarUrl: string | null; }; } interface Props { weekStart: string; mealPlanId?: string; initialIsPublic?: boolean; } export function ShareMealPlanButton({ weekStart, mealPlanId, initialIsPublic = false }: Props) { const t = useTranslations("mealPlan"); const ts = useTranslations("shareDialog"); const tCommon = useTranslations("common"); const [open, setOpen] = useState(false); const [email, setEmail] = useState(""); const [role, setRole] = useState("viewer"); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(false); const [inviting, setInviting] = useState(false); const [isPublic, setIsPublic] = useState(initialIsPublic); const [planId, setPlanId] = useState(mealPlanId); const [savingPublic, setSavingPublic] = useState(false); const [copied, setCopied] = useState(false); const [qrDataUrl, setQrDataUrl] = useState(null); function shareUrl(id: string): string { return `${window.location.origin}/m/${id}`; } useEffect(() => { if (initialIsPublic && mealPlanId) { QRCode.toDataURL(shareUrl(mealPlanId), { margin: 1, width: 160 }).then(setQrDataUrl).catch(() => {}); } // Only needs to run once per mount with the plan's already-saved state — // subsequent changes are handled by togglePublic itself. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function togglePublic(checked: boolean) { setSavingPublic(true); const previous = isPublic; setIsPublic(checked); try { const res = await fetch(`/api/v1/meal-plans/${weekStart}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isPublic: checked }), }); if (!res.ok) throw new Error(); const data = (await res.json()) as { id: string; isPublic: boolean }; setPlanId(data.id); if (checked) { setQrDataUrl(await QRCode.toDataURL(shareUrl(data.id), { margin: 1, width: 160 })); } else { setQrDataUrl(null); } } catch { setIsPublic(previous); toast.error(ts("publicLinkToggleFailed")); } finally { setSavingPublic(false); } } async function copyLink() { if (!planId) return; try { await navigator.clipboard.writeText(shareUrl(planId)); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast.error(ts("copyLinkFailed")); } } async function fetchMembers() { setLoading(true); try { const res = await fetch(`/api/v1/meal-plans/${weekStart}/members`); if (!res.ok) throw new Error("Failed to load members"); const data = await res.json() as Member[]; setMembers(data); } catch { toast.error(ts("loadMembersFailed")); } finally { setLoading(false); } } function handleOpenChange(next: boolean) { setOpen(next); if (next) { void fetchMembers(); } else { setEmail(""); setRole("viewer"); } } async function handleInvite() { if (!email.trim()) { toast.error(ts("enterEmail")); return; } setInviting(true); try { const res = await fetch(`/api/v1/meal-plans/${weekStart}/members`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), role }), }); if (res.status === 409) { toast.error(ts("alreadyMember")); return; } if (res.status === 404) { toast.error(ts("userNotFound")); return; } if (!res.ok) { toast.error(ts("inviteFailed")); return; } toast.success(ts("invitationSent")); setEmail(""); await fetchMembers(); } catch { toast.error(ts("inviteFailed")); } finally { setInviting(false); } } async function handleRemove(memberId: string) { try { const res = await fetch( `/api/v1/meal-plans/${weekStart}/members?memberId=${memberId}`, { method: "DELETE" }, ); if (!res.ok) { toast.error(ts("removeMemberFailed")); return; } setMembers((prev) => prev.filter((m) => m.id !== memberId)); toast.success(ts("memberRemoved")); } catch { toast.error(ts("removeMemberFailed")); } } return ( <> handleOpenChange(true)} aria-label={tCommon("share")}> } /> {tCommon("share")} {t("shareTitle")} {t("shareDescription")}

{ts("publicLinkTitle")}

{t("publicLinkReadOnlyDescription")}

{ void togglePublic(v); }} />
{isPublic && planId && (
{qrDataUrl && ( // eslint-disable-next-line @next/next/no-img-element -- a data: URL generated client-side, not an optimizable remote image {t("qrCodeAlt")} )}
)}
setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void handleInvite(); }} className="flex-1" />
{loading && (

{ts("loadingMembers")}

)} {!loading && members.length === 0 && (

{ts("noMembers")}

)} {members.map((m) => (
{m.user.name} {m.user.username && ( @{m.user.username} )}
{ts(m.role)}
))}
); }