"use client"; /** * ShareDialog — page-level sharing (core PageShare model): * PUT /pages/:id/share { level: view|edit|public, password?, expiresAt? } * → { url, slug, level, expiresAt } * POST /pages/:id/invites { email, role } * * Level semantics (core share page): view = read-only, edit = editable, * public = anyone-with-link. Password + expiry are optional hardening. * * Reusable from the top bar / page menu; reads + writes the shared link * state under ["pages", pageId, "share"] (same cache key the /share route * page uses). */ import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Copy, Link2, Loader2, Lock, Mail } from "lucide-react"; import { apiFetch, pageApi } from "@/lib/api"; import { toast } from "@/stores/ui-store"; import { User, WorkspaceRole } from "@/lib/types"; import { Dialog } from "@/components/dialogs/dialog"; const LEVELS: { value: "view" | "edit" | "public"; label: string; description: string }[] = [ { value: "view", label: "Can view", description: "Read-only access to this page." }, { value: "edit", label: "Can edit", description: "Can edit this page and its sub-pages." }, { value: "public", label: "Anyone with the link", description: "No sign-in required." } ]; const ROLES: { value: WorkspaceRole; label: string }[] = [ { value: "member", label: "Member" }, { value: "guest", label: "Guest (read-only)" } ]; export interface ShareDialogProps { open: boolean; pageId: string; pageTitle?: string; /** For the invite-by-email affordance (members list is not needed here). */ members?: User[]; onClose: () => void; } export function ShareDialog({ open, pageId, pageTitle, members, onClose }: ShareDialogProps) { const qc = useQueryClient(); const [level, setLevel] = useState<"view" | "edit" | "public">("view"); const [password, setPassword] = useState(""); const [copied, setCopied] = useState(false); const [email, setEmail] = useState(""); const [role, setRole] = useState("member"); const share = useQuery({ queryKey: ["pages", pageId, "share"], queryFn: async () => { try { return await pageApi.share(pageId, { level: "view" }); } catch { return null; } }, enabled: open && !!pageId, retry: false }); const saveShare = useMutation({ mutationFn: (input: { level: "view" | "edit" | "public"; password?: string | null }) => pageApi.share(pageId, input), onSuccess: (data) => { qc.setQueryData(["pages", pageId, "share"], data); setLevel(data.level); toast("success", "Share settings saved"); }, onError: (e) => toast("error", "Save failed", (e as Error).message) }); const invite = useMutation({ mutationFn: (input: { email: string; role: WorkspaceRole }) => apiFetch<{ id: string }>(`/pages/${pageId}/invites`, { method: "POST", body: input }), onSuccess: () => { toast("success", "Invitation sent", email); setEmail(""); }, onError: (e) => toast("error", "Invite failed", (e as Error).message) }); const shareUrl = useMemo(() => { if (share.data?.url) return share.data.url; if (typeof window === "undefined" || !pageId) return ""; return `${window.location.origin}/share/${pageId}`; }, [share.data, pageId]); const copyLink = async () => { try { await navigator.clipboard.writeText(shareUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast("error", "Copy failed", "Select the link text and copy manually."); } }; const levelChanged = share.data ? share.data.level !== level : true; const passwordChanged = share.data ? (share.data.password ?? "") !== password : !!password; return (
{/* link */}
{shareUrl || "No link yet"}
{/* level */}
{LEVELS.map((l) => ( ))}
{/* password (optional) */}
setPassword(e.target.value)} />
{/* invite by email */}
setEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && email.trim()) invite.mutate({ email: email.trim(), role }); }} />
{members && members.length > 0 && (

Already a member: {members.slice(0, 4).map((m) => m.name).join(", ")} {members.length > 4 ? ` +${members.length - 4} more` : ""}

)}
); }