import { useState, useEffect, useCallback } from 'react' import { X, Copy, Check, Globe, Users, Clock, Trash2, Link2 } from 'lucide-react' import type { SessionShare, SessionShareVisibility } from '@/types' import { sessionsApi } from '@/api/sessions' import { buildSessionShareUrl, filterSharesForSession } from '@/lib/sessionShare' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' interface ShareSessionModalProps { sessionId: string sessionLabel: string // e.g. ticket number or "Session Details" isOpen: boolean onClose: () => void } type ExpirationPreset = 'never' | '1day' | '7days' | '30days' | 'custom' function getRelativeTime(dateString: string): string { const now = Date.now() const date = new Date(dateString).getTime() const diffMs = now - date const diffSeconds = Math.floor(diffMs / 1000) const diffMinutes = Math.floor(diffSeconds / 60) const diffHours = Math.floor(diffMinutes / 60) const diffDays = Math.floor(diffHours / 24) if (diffSeconds < 60) return 'just now' if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago` if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago` if (diffDays < 30) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago` const diffMonths = Math.floor(diffDays / 30) return `${diffMonths} month${diffMonths === 1 ? '' : 's'} ago` } function getExpirationLabel(expiresAt: string | null): { text: string; isExpired: boolean } { if (!expiresAt) return { text: 'No expiration', isExpired: false } const now = Date.now() const expiry = new Date(expiresAt).getTime() if (expiry <= now) return { text: 'Expired', isExpired: true } const diffMs = expiry - now const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) const diffDays = Math.floor(diffHours / 24) if (diffDays > 0) return { text: `Expires in ${diffDays} day${diffDays === 1 ? '' : 's'}`, isExpired: false } if (diffHours > 0) return { text: `Expires in ${diffHours} hour${diffHours === 1 ? '' : 's'}`, isExpired: false } return { text: 'Expires soon', isExpired: false } } function computeExpiresAt(preset: ExpirationPreset, customDatetime: string): string | undefined { if (preset === 'never') return undefined if (preset === 'custom') { if (!customDatetime) return undefined return new Date(customDatetime).toISOString() } const now = new Date() switch (preset) { case '1day': now.setDate(now.getDate() + 1) break case '7days': now.setDate(now.getDate() + 7) break case '30days': now.setDate(now.getDate() + 30) break } return now.toISOString() } export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }: ShareSessionModalProps) { const [shares, setShares] = useState([]) const [isLoadingShares, setIsLoadingShares] = useState(false) const [isGenerating, setIsGenerating] = useState(false) const [copiedShareId, setCopiedShareId] = useState(null) // Form state const [visibility, setVisibility] = useState('account') const [shareName, setShareName] = useState('') const [expirationPreset, setExpirationPreset] = useState('never') const [customDatetime, setCustomDatetime] = useState('') const [visibilityError, setVisibilityError] = useState(null) const loadShares = useCallback(async () => { setIsLoadingShares(true) try { const allShares = await sessionsApi.listMyShares() const sessionShares = filterSharesForSession(allShares, sessionId) // Sort newest first sessionShares.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) setShares(sessionShares) } catch (err) { console.error('Failed to load shares:', err) } finally { setIsLoadingShares(false) } }, [sessionId]) useEffect(() => { if (isOpen) { loadShares() // Reset form state setVisibility('account') setShareName('') setExpirationPreset('never') setCustomDatetime('') setVisibilityError(null) setCopiedShareId(null) } }, [isOpen, sessionId, loadShares]) const handleGenerateLink = async () => { setIsGenerating(true) setVisibilityError(null) try { const expires_at = computeExpiresAt(expirationPreset, customDatetime) const newShare = await sessionsApi.createShare(sessionId, { visibility, share_name: shareName.trim() || undefined, expires_at, }) setShares([newShare, ...shares]) toast.success('Share link generated') // Reset form setShareName('') setExpirationPreset('never') setCustomDatetime('') } catch (err: unknown) { const error = err as { response?: { status?: number; data?: { detail?: string } } } if ( error.response?.status === 403 && error.response?.data?.detail?.toLowerCase().includes('public session sharing') ) { setVisibilityError(error.response.data.detail ?? 'Organization does not allow public session sharing') } else { console.error('Failed to generate share link:', err) toast.error('Failed to generate share link') } } finally { setIsGenerating(false) } } const handleCopyUrl = async (share: SessionShare) => { try { const url = buildSessionShareUrl(share) await navigator.clipboard.writeText(url) setCopiedShareId(share.id) toast.success('Link copied to clipboard') setTimeout(() => setCopiedShareId(null), 2000) } catch (err) { console.error('Failed to copy link:', err) toast.error('Failed to copy link') } } const handleRevoke = async (shareId: string) => { try { await sessionsApi.revokeShare(shareId) setShares(shares.filter((s) => s.id !== shareId)) toast.success('Share link revoked') } catch (err) { console.error('Failed to revoke share:', err) toast.error('Failed to revoke share') } } if (!isOpen) return null const presetButtons: { value: ExpirationPreset; label: string }[] = [ { value: 'never', label: 'Never' }, { value: '1day', label: '1 day' }, { value: '7days', label: '7 days' }, { value: '30days', label: '30 days' }, { value: 'custom', label: 'Custom' }, ] return (
{/* Backdrop */}
{/* Modal */}
{/* Header */}

Share Session

{sessionLabel}

{/* Body */}
{/* Create Share Form */}
{/* Visibility */}
{visibilityError && (

{visibilityError}

)}
{/* Share Name */}
setShareName(e.target.value.slice(0, 100))} placeholder="e.g. Training link, Customer escalation" className={cn( 'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder-muted-foreground', 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20' )} maxLength={100} />
{/* Expiration */}
{presetButtons.map((preset) => ( ))}
{expirationPreset === 'custom' && ( setCustomDatetime(e.target.value)} className={cn( 'mt-2 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20', '[color-scheme:dark]' )} /> )}
{/* Generate Button */}
{/* Existing Shares */} {shares.length > 0 && (

Active Shares ({shares.length})

{shares.map((share) => { const expiration = getExpirationLabel(share.expires_at) const isCopied = copiedShareId === share.id return (
{share.visibility === 'public' ? ( ) : ( )} {share.visibility === 'public' ? 'Public' : 'Account'} {share.share_name || 'Untitled share'}
{getRelativeTime(share.created_at)} {share.view_count > 0 ? `${share.view_count} view${share.view_count === 1 ? '' : 's'}` : 'Not viewed yet'} {expiration.text}
) })}
)} {/* Loading state */} {isLoadingShares && shares.length === 0 && (
)}
{/* Footer */}
) } export default ShareSessionModal