'use client'; import { useState, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import PasswordInput from '@/components/PasswordInput'; import { Label } from '@/components/ui/label'; import { ShareIcon, LockClosedIcon, ClockIcon, EyeIcon, CheckIcon, ClipboardIcon, GlobeAltIcon, UsersIcon, LinkIcon, PencilSquareIcon, } from '@heroicons/react/24/outline'; import { createShare, getShareableUsers } from '@/app/(authenticated)/shared/lib/actions'; import { useTranslations } from 'next-intl'; import UserAvatar from '@/components/UserAvatar'; interface ShareDialogProps { entityType: 'NOTE' | 'BOARD'; entityId: number; trigger?: React.ReactNode; } type ShareScope = 'PUBLIC' | 'ALL' | 'SPECIFIC'; interface ShareableUser { id: number; name: string | null; email: string; profileImage: string | null; } /** * Renders share creation for a note or board, with an optional custom trigger. * Public links support a password, expiration, and one-time viewing; specific * recipients can receive edit access to notes. Closing resets the share options. */ export default function ShareDialog({ entityType, entityId, trigger }: ShareDialogProps) { const t = useTranslations('ShareDialog'); const [isOpen, setIsOpen] = useState(false); const [scope, setScope] = useState('PUBLIC'); const [password, setPassword] = useState(''); const [expiresAt, setExpiresAt] = useState(''); const [oneTime, setOneTime] = useState(false); const [isLoading, setIsLoading] = useState(false); const [shareLink, setShareLink] = useState(null); const [shareScope, setShareScope] = useState(null); const [copied, setCopied] = useState(false); const [users, setUsers] = useState([]); const [selectedUserIds, setSelectedUserIds] = useState([]); const [userSearch, setUserSearch] = useState(''); const [loadingUsers, setLoadingUsers] = useState(false); const [canEdit, setCanEdit] = useState(false); useEffect(() => { if (scope === 'SPECIFIC' && users.length === 0) { setLoadingUsers(true); getShareableUsers() .then(setUsers) .catch(console.error) .finally(() => setLoadingUsers(false)); } }, [scope, users.length]); const handleCreate = async () => { setIsLoading(true); try { const share = await createShare({ entityType, entityId, password: scope === 'PUBLIC' ? password || undefined : undefined, expiresAt: scope === 'PUBLIC' && expiresAt ? new Date(expiresAt) : null, oneTime: scope === 'PUBLIC' ? oneTime : false, scope, recipientIds: scope === 'SPECIFIC' ? selectedUserIds : undefined, canEdit: scope === 'SPECIFIC' && entityType === 'NOTE' ? canEdit : false, }); if (scope === 'PUBLIC') { setShareLink(`${window.location.origin}/share/${share.id}`); } setShareScope(scope); } catch (error) { console.error('Failed to create share:', error); } finally { setIsLoading(false); } }; const copyToClipboard = () => { if (shareLink) { navigator.clipboard .writeText(shareLink) .then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }) .catch(console.error); } }; const handleClose = () => { setIsOpen(false); setShareLink(null); setShareScope(null); setPassword(''); setExpiresAt(''); setOneTime(false); setScope('PUBLIC'); setSelectedUserIds([]); setUserSearch(''); setCanEdit(false); }; const toggleUser = (id: number) => { setSelectedUserIds((prev) => prev.includes(id) ? prev.filter((u) => u !== id) : [...prev, id] ); }; const filteredUsers = users.filter( (u) => u.name?.toLowerCase().includes(userSearch.toLowerCase()) || u.email.toLowerCase().includes(userSearch.toLowerCase()) ); const entityLabel = entityType === 'NOTE' ? t('entities.note') : t('entities.board'); const scopeOptions: { value: ShareScope; label: string; description: string; icon: React.ElementType }[] = [ { value: 'PUBLIC', label: t('scopePublic'), description: t('scopePublicDesc'), icon: LinkIcon }, { value: 'ALL', label: t('scopeAll'), description: t('scopeAllDesc'), icon: GlobeAltIcon }, { value: 'SPECIFIC', label: t('scopeSpecific'), description: t('scopeSpecificDesc'), icon: UsersIcon }, ]; const isSuccess = shareScope !== null; return ( { if (!val) handleClose(); else setIsOpen(true); }}> {trigger || ( )} {/* Header */}
{t('title', { entity: entityLabel })}

{t('subtitle')}

{!isSuccess ? ( <> {/* Scope selector — vertical radio list */}
{scopeOptions.map((opt) => { const isSelected = scope === opt.value; return ( ); })}
{/* PUBLIC: password, expiry, one-time */} {scope === 'PUBLIC' && (
setPassword(e.target.value)} className="h-10 rounded-xl border-border bg-muted/30 focus:bg-background transition-all text-sm" />
{ if (!expiresAt) setExpiresAt(`${new Date().toLocaleDateString('sv-SE')}T23:59`); }} onChange={(e) => setExpiresAt(e.target.value)} className="h-10 rounded-xl border-border bg-muted/30 focus:bg-background transition-all text-sm" />
{/* One-time toggle */}
)} {/* SPECIFIC: user picker */} {scope === 'SPECIFIC' && (
setUserSearch(e.target.value)} className="h-10 rounded-xl border-border bg-muted/30 focus:bg-background transition-all" />
{loadingUsers ? (
{t('loadingUsers')}
) : filteredUsers.length === 0 ? (
{t('noUsers')}
) : ( filteredUsers.map((u) => { const isSelected = selectedUserIds.includes(u.id); return ( ); }) )}
{selectedUserIds.length > 0 && (

{t('selectedCount', { count: selectedUserIds.length })}

)} {/* Allow editing — only for notes */} {entityType === 'NOTE' && ( )}
)} ) : ( /* Success state */

{shareScope === 'PUBLIC' ? t('successTitle') : shareScope === 'ALL' ? t('successAllTitle') : t('successSpecificTitle')}

{shareScope === 'PUBLIC' ? t('successDescription') : shareScope === 'ALL' ? t('successAllDescription') : t('successSpecificDescription')}

{shareScope === 'PUBLIC' && shareLink && (
)}
)}
); }