WebVNWrite/src/components/editor/ShareModal.tsx

225 lines
9.0 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import {
getCollaborators,
inviteCollaborator,
updateCollaboratorRole,
removeCollaborator,
type Collaborator,
} from '@/app/editor/[projectId]/actions'
type ShareModalProps = {
projectId: string
isOwner: boolean
onClose: () => void
}
export default function ShareModal({ projectId, isOwner, onClose }: ShareModalProps) {
const [collaborators, setCollaborators] = useState<Collaborator[]>([])
const [loading, setLoading] = useState(true)
const [email, setEmail] = useState('')
const [role, setRole] = useState<'editor' | 'viewer'>('editor')
const [inviting, setInviting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [successMessage, setSuccessMessage] = useState<string | null>(null)
const fetchCollaborators = useCallback(async () => {
const result = await getCollaborators(projectId)
if (result.success && result.data) {
setCollaborators(result.data)
}
setLoading(false)
}, [projectId])
useEffect(() => {
fetchCollaborators()
}, [fetchCollaborators])
const handleInvite = async (e: React.FormEvent) => {
e.preventDefault()
if (!email.trim()) return
setInviting(true)
setError(null)
setSuccessMessage(null)
const result = await inviteCollaborator(projectId, email.trim(), role)
if (result.success) {
setEmail('')
setSuccessMessage('Collaborator invited successfully')
fetchCollaborators()
} else {
setError(result.error || 'Failed to invite collaborator')
}
setInviting(false)
}
const handleRoleChange = async (collaboratorId: string, newRole: 'editor' | 'viewer') => {
setError(null)
const result = await updateCollaboratorRole(projectId, collaboratorId, newRole)
if (result.success) {
setCollaborators((prev) =>
prev.map((c) => (c.id === collaboratorId ? { ...c, role: newRole } : c))
)
} else {
setError(result.error || 'Failed to update role')
}
}
const handleRemove = async (collaboratorId: string) => {
setError(null)
const result = await removeCollaborator(projectId, collaboratorId)
if (result.success) {
setCollaborators((prev) => prev.filter((c) => c.id !== collaboratorId))
} else {
setError(result.error || 'Failed to remove collaborator')
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50"
onClick={onClose}
aria-hidden="true"
/>
<div className="relative w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-zinc-800">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
Share Project
</h2>
<button
onClick={onClose}
className="rounded p-1 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Invite Form - only for owners */}
{isOwner && (
<form onSubmit={handleInvite} className="mb-6">
<label className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Invite collaborator by email
</label>
<div className="flex gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="user@example.com"
disabled={inviting}
className="flex-1 rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-50 dark:placeholder-zinc-500"
/>
<select
value={role}
onChange={(e) => setRole(e.target.value as 'editor' | 'viewer')}
disabled={inviting}
className="rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-50"
>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
<button
type="submit"
disabled={inviting || !email.trim()}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-offset-zinc-800"
>
{inviting ? 'Inviting...' : 'Invite'}
</button>
</div>
</form>
)}
{/* Messages */}
{error && (
<div className="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
{error}
</div>
)}
{successMessage && (
<div className="mb-4 rounded-md bg-green-50 p-3 text-sm text-green-700 dark:bg-green-900/20 dark:text-green-400">
{successMessage}
</div>
)}
{/* Collaborators List */}
<div>
<h3 className="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-3">
Collaborators
</h3>
{loading ? (
<p className="text-sm text-zinc-500 dark:text-zinc-400">Loading...</p>
) : collaborators.length === 0 ? (
<p className="text-sm text-zinc-500 dark:text-zinc-400">
No collaborators yet. Invite someone to get started.
</p>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{collaborators.map((collab) => (
<div
key={collab.id}
className="flex items-center justify-between rounded-md border border-zinc-200 p-3 dark:border-zinc-700"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-zinc-900 truncate dark:text-zinc-50">
{collab.display_name || collab.email || 'Unknown user'}
</p>
{collab.display_name && collab.email && (
<p className="text-xs text-zinc-500 truncate dark:text-zinc-400">
{collab.email}
</p>
)}
</div>
<div className="flex items-center gap-2 ml-3">
{isOwner ? (
<>
<select
value={collab.role}
onChange={(e) =>
handleRoleChange(collab.id, e.target.value as 'editor' | 'viewer')
}
className="rounded border border-zinc-300 bg-white px-2 py-1 text-xs text-zinc-700 focus:border-blue-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"
>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
<button
onClick={() => handleRemove(collab.id)}
className="rounded p-1 text-zinc-400 hover:text-red-600 dark:hover:text-red-400"
title="Remove collaborator"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</>
) : (
<span className="rounded bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600 dark:bg-zinc-700 dark:text-zinc-400">
{collab.role}
</span>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Close button */}
<div className="mt-6 flex justify-end">
<button
onClick={onClose}
className="rounded-md border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
>
Close
</button>
</div>
</div>
</div>
)
}