import React, { useId, type ReactNode } from 'react' import { avatarSrc } from '../../utils/avatarSrc' import { UserPlus, UserMinus, UserX, Loader2, Clock, Crown, LogOut, Share2 } from 'lucide-react' import type { CollectionMember, CollectionRole } from '@trek/shared' import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' import ConfirmDialog from '../shared/ConfirmDialog' import CustomSelect from '../shared/CustomSelect' import { Tooltip } from '../shared/Tooltip' import { TripMemberAvatar } from '../Trips/TripMemberAvatar' import type { TranslationFn } from '../../types' import { useCollectionSharing } from './useCollectionSharing' interface ShareCollectionModalProps { isOpen: boolean onClose: () => void collectionId: number collectionName: string isOwner: boolean members: CollectionMember[] /** Called after the current (member) user successfully leaves the list. */ onAfterLeave: () => void t: TranslationFn } const ROLE_ORDER: CollectionRole[] = ['viewer', 'editor', 'admin'] // The trip share dialog's roster, so a shared list and a shared trip read alike. const COUNT = 'rounded-full bg-surface-tertiary px-2 py-[2px] font-geist font-bold text-content-muted' const ROWS = 'flex flex-col gap-0.5 rounded-[14px] border border-edge-faint bg-surface-secondary p-1.5' const ROW = 'flex min-h-[48px] items-center gap-3 rounded-[10px] px-2.5 py-1.5 hover:bg-surface-card' const BADGE = 'inline-flex flex-none items-center gap-1 rounded-full px-2 py-[2px] font-semibold' const DANGER_BUTTON = 'inline-flex items-center gap-1.5 rounded-[10px] bg-surface-card px-3.5 py-2 font-medium text-danger shadow-sm ring-1 ring-edge-faint hover:bg-danger-soft disabled:cursor-default disabled:opacity-50' /** A round icon button of a row, named by its tooltip; faint until the pointer is on it. */ function RowAction({ label, onClick, disabled, children }: { label: string; onClick: () => void; disabled?: boolean; children: ReactNode }) { return ( {children} ) } /** * Fusion-share surface for a single list (blueprint 4.4 / 4.8). The OWNER sees the * member roster with accepted/pending status, can invite a user from * GET /:id/available-users and cancel a pending invite. A non-owner MEMBER sees the * roster read-only plus a "Leave shared list" action (the server blocks the owner * from leaving). Incoming invites are accepted/declined from the lists rail, not here. */ export default function ShareCollectionModal({ isOpen, onClose, collectionId, collectionName, isOwner, members, onAfterLeave, t, }: ShareCollectionModalProps): React.ReactElement | null { const labelId = useId() const { currentUserId, availableUsers, sortedMembers, selectedUserId, setSelectedUserId, inviteRole, setInviteRole, inviting, confirmLeave, setConfirmLeave, leaving, busyUserId, setRole, cancel, remove, handleInvite, handleLeave, } = useCollectionSharing({ open: isOpen, collectionId, isOwner, members, onAfterLeave, t }) const settingRoleId = busyUserId('role') const cancellingId = busyUserId('cancel') const removingId = busyUserId('remove') if (!isOpen) return null const roleOptions = ROLE_ORDER.map(r => ({ value: r, label: t(`collections.role.${r}`) })) return ( <> } tint={NEUTRAL_TINT} labelId={labelId} onClose={onClose} eyebrow={t('collections.share.title')} title={collectionName} /> )} // A member's way out sits where an editor keeps its delete: bottom left. footer={isOwner ? undefined : ( setConfirmLeave(true)} disabled={leaving} className={DANGER_BUTTON} style={fs(13, 'body')}> {leaving ? : } {t('collections.share.leave')} )} > {isOwner && ( {t('collections.share.inviteHint')} {availableUsers.length === 0 ? ( {t('collections.share.noUsers')} ) : ( setSelectedUserId(v === '' ? null : Number(v))} options={availableUsers.map(u => ({ value: u.id, label: u.username }))} placeholder={t('collections.share.inviteUser')} searchable size="sm" /> setInviteRole(v as CollectionRole)} options={roleOptions} /> void handleInvite()} disabled={selectedUserId == null || inviting} icon={inviting ? : } > {t('collections.share.sendInvite')} )} )} {t('collections.share.members')} {sortedMembers.length} )}> {sortedMembers.map(member => { const isSelf = member.user_id === currentUserId const pending = member.status === 'pending' let standing: ReactNode if (member.is_owner) { standing = {t('collections.share.owner')} } else if (pending) { standing = {t('collections.share.pending')} } else if (isOwner) { standing = ( setRole(member.user_id, v as CollectionRole)} options={roleOptions} disabled={settingRoleId === member.user_id} /> ) } else { standing = {t(`collections.role.${member.role ?? 'editor'}`)} } return ( {member.username} {isSelf && ({t('collections.share.you')})} {member.email && !pending && ( {member.email} )} {standing} {isOwner && pending && ( cancel(member.user_id)} disabled={cancellingId === member.user_id}> {cancellingId === member.user_id ? : } )} {isOwner && !member.is_owner && member.status === 'accepted' && ( remove(member.user_id)} disabled={removingId === member.user_id}> {removingId === member.user_id ? : } )} ) })} {!isOwner && {t('collections.share.memberHint')}} setConfirmLeave(false)} onConfirm={() => void handleLeave()} title={t('collections.share.leave')} message={t('collections.share.leaveConfirm')} confirmLabel={t('collections.share.leave')} /> > ) }
{t('collections.share.inviteHint')}
{t('collections.share.noUsers')}
{t('collections.share.memberHint')}