import { useState, useEffect, useRef } from 'react' import Modal from '../shared/Modal' import { tripsApi, authApi, shareApi } from '../../api/client' import { useToast } from '../shared/Toast' import { useAuthStore } from '../../store/authStore' import { useCanDo } from '../../store/permissionsStore' import { useTripStore } from '../../store/tripStore' import { Crown, UserMinus, UserPlus, Users, LogOut, Link2, Trash2, Copy, Check } from 'lucide-react' import { useTranslation } from '../../i18n' import { getApiErrorMessage } from '../../types' import CustomSelect from '../shared/CustomSelect' interface AvatarProps { username: string avatarUrl: string | null size?: number } function Avatar({ username, avatarUrl, size = 32 }: AvatarProps) { if (avatarUrl) { return } const letter = (username || '?')[0].toUpperCase() const colors = ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b', '#ef4444', '#06b6d4'] const color = colors[letter.charCodeAt(0) % colors.length] return (
{letter}
) } function ShareLinkSection({ tripId, t }: { tripId: number; t: (key: string, params?: Record) => string }) { const [shareToken, setShareToken] = useState(null) const [loading, setLoading] = useState(true) const [copied, setCopied] = useState(false) const [perms, setPerms] = useState({ share_map: true, share_bookings: true, share_packing: false, share_budget: false, share_collab: false }) const toast = useToast() const copyTimerRef = useRef | null>(null) useEffect(() => { return () => { if (copyTimerRef.current) clearTimeout(copyTimerRef.current) } }, []) useEffect(() => { shareApi.getLink(tripId).then(d => { setShareToken(d.token) if (d.token) setPerms({ share_map: d.share_map ?? true, share_bookings: d.share_bookings ?? true, share_packing: d.share_packing ?? false, share_budget: d.share_budget ?? false, share_collab: d.share_collab ?? false }) setLoading(false) }).catch(() => setLoading(false)) }, [tripId]) const shareUrl = shareToken ? `${window.location.origin}/shared/${shareToken}` : null const handleCreate = async () => { try { const d = await shareApi.createLink(tripId, perms) setShareToken(d.token) } catch { toast.error(t('share.createError')) } } const handleUpdatePerms = async (key: string, val: boolean) => { const newPerms = { ...perms, [key]: val } setPerms(newPerms) if (shareToken) { try { await shareApi.createLink(tripId, newPerms) } catch {} } } const handleDelete = async () => { try { await shareApi.deleteLink(tripId) setShareToken(null) } catch {} } const handleCopy = () => { if (shareUrl) { navigator.clipboard.writeText(shareUrl) setCopied(true) if (copyTimerRef.current) clearTimeout(copyTimerRef.current) copyTimerRef.current = setTimeout(() => setCopied(false), 2000) } } if (loading) return null return (
{t('share.linkTitle')}

{t('share.linkHint')}

{/* Permission checkboxes */}
{[ { key: 'share_map', label: t('share.permMap'), always: true }, { key: 'share_bookings', label: t('share.permBookings') }, { key: 'share_packing', label: t('share.permPacking') }, { key: 'share_budget', label: t('share.permBudget') }, { key: 'share_collab', label: t('share.permCollab') }, ].map(opt => ( ))}
{shareUrl ? (
) : ( )}
) } interface TripMembersModalProps { isOpen: boolean onClose: () => void tripId: number tripTitle: string } export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }: TripMembersModalProps) { const [data, setData] = useState(null) const [allUsers, setAllUsers] = useState([]) const [loading, setLoading] = useState(false) const [selectedUserId, setSelectedUserId] = useState('') const [adding, setAdding] = useState(false) const [removingId, setRemovingId] = useState(null) const toast = useToast() const { user } = useAuthStore() const { t } = useTranslation() const can = useCanDo() const trip = useTripStore((s) => s.trip) const canManageMembers = can('member_manage', trip) const canManageShare = can('share_manage', trip) useEffect(() => { if (isOpen && tripId) { loadMembers() loadAllUsers() } }, [isOpen, tripId]) const loadMembers = async () => { setLoading(true) try { const d = await tripsApi.getMembers(tripId) setData(d) } catch { toast.error(t('members.loadError')) } finally { setLoading(false) } } const loadAllUsers = async () => { try { const d = await authApi.listUsers() setAllUsers(d.users) } catch {} } const handleAdd = async () => { if (!selectedUserId) return setAdding(true) try { const target = allUsers.find(u => String(u.id) === String(selectedUserId)) await tripsApi.addMember(tripId, target.username) setSelectedUserId('') await loadMembers() toast.success(`${target.username} ${t('members.added')}`) } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('members.addError'))) } finally { setAdding(false) } } const handleRemove = async (userId, isSelf) => { const msg = isSelf ? t('members.confirmLeave') : t('members.confirmRemove') if (!confirm(msg)) return setRemovingId(userId) try { await tripsApi.removeMember(tripId, userId) if (isSelf) { onClose(); window.location.reload() } else { await loadMembers(); toast.success(t('members.removed')) } } catch { toast.error(t('members.removeError')) } finally { setRemovingId(null) } } // Users not yet in the trip const existingIds = new Set([ data?.owner?.id, ...(data?.members?.map(m => m.id) || []), ]) const availableUsers = allUsers.filter(u => !existingIds.has(u.id)) const isCurrentOwner = data?.owner?.id === user?.id const allMembers = data ? [ { ...data.owner, role: 'owner' }, ...data.members, ] : [] return (
{/* Left column: Members */}
{/* Trip name */}
{t('nav.trip')}
{tripTitle}
{/* Add member dropdown */} {canManageMembers &&
setSelectedUserId(value)} placeholder={t('members.selectUser')} options={[ { value: '', label: t('members.selectUser') }, ...availableUsers.map(u => ({ value: u.id, label: u.username, })), ]} searchable style={{ flex: 1 }} size="sm" />
{availableUsers.length === 0 && allUsers.length > 0 && canManageMembers && (

{t('members.allHaveAccess')}

)}
} {/* Members list */}
{t('members.access')} ({allMembers.length} {allMembers.length === 1 ? t('members.person') : t('members.persons')})
{loading ? (
{[1, 2].map(i => (
))}
) : (
{allMembers.map(member => { const isSelf = member.id === user?.id const canRemove = isSelf || (canManageMembers && member.role !== 'owner') return (
{member.username} {isSelf && ({t('members.you')})} {member.role === 'owner' && ( {t('members.owner')} )}
{canRemove && ( )}
) })}
)}
{/* Right column: Share Link */} {canManageShare &&
}
) }