import { useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Share2, GitFork, Lock, Users, MessageSquare, ChevronDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import type { CanvasPage } from '../types/canvas'; import { listVelocityUsers, type VelocityActiveUser } from '@/lib/velocityPlatformClient'; interface ShareModalProps { page: CanvasPage | null; isOpen: boolean; onClose: () => void; currentUserId?: string | null; onShare: (params: { recipientUserId: string; visibility: 'private' | 'team'; message: string; sourceRevision: number; }) => Promise; } function getDisplayName(member: VelocityActiveUser): string { return member.full_name?.trim() || member.email?.trim() || member.user_id; } function getRoleLabel(member: VelocityActiveUser): string { return member.role .toLowerCase() .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); } function getInitials(member: VelocityActiveUser): string { const basis = getDisplayName(member); return basis .split(/[\s@._-]+/) .filter(Boolean) .slice(0, 2) .map((part) => part.charAt(0).toUpperCase()) .join('') || 'U'; } export function ShareModal({ page, isOpen, onClose, currentUserId, onShare }: ShareModalProps) { const [mounted, setMounted] = useState(false); const [teamMembers, setTeamMembers] = useState([]); const [loadingMembers, setLoadingMembers] = useState(false); const [membersError, setMembersError] = useState(null); const [recipient, setRecipient] = useState(null); const [visibility, setVisibility] = useState<'private' | 'team'>('private'); const [message, setMessage] = useState(''); const [submitting, setSubmitting] = useState(false); const [success, setSuccess] = useState(false); const [submitError, setSubmitError] = useState(null); const [memberDropOpen, setMemberDropOpen] = useState(false); useEffect(() => setMounted(true), []); useEffect(() => { if (!isOpen) { setMemberDropOpen(false); setSubmitError(null); return; } let cancelled = false; setLoadingMembers(true); setMembersError(null); void listVelocityUsers() .then((users) => { if (cancelled) return; setTeamMembers(users); }) .catch((error) => { if (cancelled) return; setMembersError(error instanceof Error ? error.message : 'Failed to load team members.'); setTeamMembers([]); }) .finally(() => { if (!cancelled) setLoadingMembers(false); }); return () => { cancelled = true; }; }, [isOpen]); const availableMembers = useMemo( () => teamMembers.filter((member) => member.user_id !== currentUserId), [teamMembers, currentUserId], ); useEffect(() => { if (recipient && recipient.user_id === currentUserId) { setRecipient(null); } }, [recipient, currentUserId]); const selectedRecipientLabel = useMemo( () => (recipient ? getDisplayName(recipient) : 'Select verified teammate...'), [recipient], ); const handleShare = async () => { if (!recipient || !page) return; setSubmitting(true); setSubmitError(null); try { await onShare({ recipientUserId: recipient.user_id, visibility, message, sourceRevision: page.headRevision, }); setSuccess(true); setTimeout(() => { setSuccess(false); onClose(); setRecipient(null); setMessage(''); }, 1800); } catch (error) { setSubmitError(error instanceof Error ? error.message : 'Share failed.'); } finally { setSubmitting(false); } }; const content = ( {isOpen && ( <>

Share Canvas

{page?.title ?? 'Oracle Canvas'} · rev.{page?.headRevision}

The recipient gets a fork of this canvas at the selected revision. They can edit their copy and later open a merge request back into the source branch.

{success ? (

Fork created successfully.

{recipient ? getDisplayName(recipient) : 'Recipient'} can access the shared copy.

) : (
{submitError && (
{submitError}
)}
{memberDropOpen && ( {loadingMembers && (
Loading verified accounts...
)} {!loadingMembers && membersError && (
{membersError}
)} {!loadingMembers && !membersError && availableMembers.length === 0 && (
No verified users available.
)} {!loadingMembers && !membersError && availableMembers.map((member) => ( ))}
)}
{([ { value: 'private', icon: Lock, label: 'Private', desc: 'Only recipient' }, { value: 'team', icon: Users, label: 'Team', desc: 'Whole team' }, ] as const).map(({ value, icon: Icon, label, desc }) => ( ))}