Files
DocNest/frontend/src/components/Share/ShareModal.tsx
2026-03-15 09:45:17 +00:00

319 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { shareApi } from '../../api/share';
import type { DocumentShareWithUser, ShareLink } from '../../types/share';
import PixelIcon from '../PixelIcon/PixelIcon';
import './ShareModal.css';
interface ShareModalProps {
documentId: string;
documentType?: 'editor' | 'kanban';
onClose: () => void;
currentPermission?: string;
currentRole?: string;
}
function ShareModal({
documentId,
documentType = 'editor',
onClose,
currentPermission,
currentRole,
}: ShareModalProps) {
const [activeTab, setActiveTab] = useState<'users' | 'link'>('users');
const [shares, setShares] = useState<DocumentShareWithUser[]>([]);
const [shareLink, setShareLink] = useState<ShareLink | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
// Form state for user sharing
const [userEmail, setUserEmail] = useState('');
const [permission, setPermission] = useState<'view' | 'edit'>('view');
// Form state for link sharing
const [linkPermission, setLinkPermission] = useState<'view' | 'edit'>('edit');
const [copied, setCopied] = useState(false);
// Load shares on mount
useEffect(() => {
loadShares();
loadShareLink();
}, [documentId]);
const loadShares = async () => {
try {
const data = await shareApi.listShares(documentId);
setShares(data);
} catch (err) {
console.error('Failed to load shares:', err);
}
};
const loadShareLink = async () => {
try {
const link = await shareApi.getShareLink(documentId);
setShareLink(link);
if (link) {
setLinkPermission(link.permission);
}
} catch (err) {
console.error('Failed to load share link:', err);
}
};
const handleAddUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!userEmail.trim()) {
setError('Please enter an email address');
return;
}
setLoading(true);
setError(null);
setSuccess(null);
try {
await shareApi.createShare(documentId, {
user_email: userEmail,
permission,
});
setSuccess('User added successfully');
setUserEmail('');
await loadShares();
} catch (err) {
setError('Failed to add user. Make sure the email is registered.');
} finally {
setLoading(false);
}
};
const handleRemoveUser = async (userId: string) => {
if (!confirm('Remove access for this user?')) {
return;
}
setLoading(true);
setError(null);
try {
await shareApi.deleteShare(documentId, userId);
setSuccess('User removed successfully');
await loadShares();
} catch (err) {
setError('Failed to remove user');
} finally {
setLoading(false);
}
};
const handleGenerateLink = async () => {
setLoading(true);
setError(null);
try {
const link = await shareApi.createShareLink(documentId, linkPermission);
setShareLink(link);
setSuccess('Share link created');
} catch (err) {
setError('Failed to create share link');
} finally {
setLoading(false);
}
};
const handleRevokeLink = async () => {
if (!confirm('Revoke this share link? Anyone with the link will lose access.')) {
return;
}
setLoading(true);
setError(null);
try {
await shareApi.revokeShareLink(documentId);
setShareLink(null);
setSuccess('Share link revoked');
} catch (err) {
setError('Failed to revoke share link');
} finally {
setLoading(false);
}
};
const handleCopyLink = () => {
if (!shareLink) return;
const url = `${window.location.origin}/${documentType}/${documentId}?share=${shareLink.token}`;
navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Share Document</h2>
<button className="close-button" onClick={onClose}>×</button>
</div>
{currentRole && (
<div className={`role-banner ${currentRole}`}>
<span className="role-icon">
{currentRole === 'owner' ? '👑' : currentPermission === 'edit' ? '✏️' : '👁️'}
</span>
<span>
{currentRole === 'owner' && 'You are the owner'}
{currentRole === 'editor' && 'You have edit access'}
{currentRole === 'viewer' && 'You have view-only access'}
</span>
</div>
)}
<div className="tabs">
<button
className={`tab ${activeTab === 'users' ? 'active' : ''}`}
onClick={() => setActiveTab('users')}
>
Share with Users
</button>
<button
className={`tab ${activeTab === 'link' ? 'active' : ''}`}
onClick={() => setActiveTab('link')}
>
Public Link
</button>
</div>
{error && <div className="message error">{error}</div>}
{success && <div className="message success">{success}</div>}
{activeTab === 'users' && (
<div className="tab-content">
<form onSubmit={handleAddUser} className="share-form">
<input
type="email"
placeholder="Email address"
value={userEmail}
onChange={(e) => setUserEmail(e.target.value)}
className="share-input"
disabled={loading}
/>
<select
value={permission}
onChange={(e) => setPermission(e.target.value as 'view' | 'edit')}
className="share-select"
disabled={loading}
>
<option value="view">Can view</option>
<option value="edit">Can edit</option>
</select>
<button type="submit" className="share-button" disabled={loading}>
{loading ? 'Adding...' : 'Add User'}
</button>
</form>
<div className="shares-list">
<h3>People with access</h3>
{shares.length === 0 ? (
<div className="empty-state">
<PixelIcon name="gem" size={18} color="hsl(var(--brand-teal))" />
<p>No users have been given access yet.</p>
</div>
) : (
shares.map((share) => (
<div key={share.id} className="share-item">
<div className="share-user">
{share.user.avatar_url && (
<img
src={share.user.avatar_url}
alt={share.user.name}
className="share-avatar"
/>
)}
<div className="share-info">
<div className="share-name">{share.user.name}</div>
<div className="share-email">{share.user.email}</div>
</div>
</div>
<div className="share-actions">
<span className="permission-badge">{share.permission}</span>
<button
onClick={() => handleRemoveUser(share.user_id)}
className="remove-button"
disabled={loading}
>
Remove
</button>
</div>
</div>
))
)}
</div>
</div>
)}
{activeTab === 'link' && (
<div className="tab-content">
{!shareLink ? (
<div className="link-creation">
<p>Create a public link that anyone can use to access this document.</p>
<div className="link-form">
<select
value={linkPermission}
onChange={(e) => setLinkPermission(e.target.value as 'view' | 'edit')}
className="share-select"
disabled={loading}
>
<option value="view">Can view</option>
<option value="edit">Can edit</option>
</select>
<button
onClick={handleGenerateLink}
className="share-button"
disabled={loading}
>
{loading ? 'Creating...' : 'Generate Link'}
</button>
</div>
</div>
) : (
<div className="link-display">
<p>Anyone with this link can {shareLink.permission} this document.</p>
<div className="link-box">
<input
type="text"
value={`${window.location.origin}/${documentType}/${documentId}?share=${shareLink.token}`}
readOnly
className="link-input"
/>
<button onClick={handleCopyLink} className="copy-button">
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
<div className="link-meta">
<span className="permission-badge">{shareLink.permission}</span>
<span className="link-date">
Created {new Date(shareLink.created_at).toLocaleDateString()}
</span>
</div>
<button
onClick={handleRevokeLink}
className="revoke-button"
disabled={loading}
>
{loading ? 'Revoking...' : 'Revoke Link'}
</button>
</div>
)}
</div>
)}
</div>
</div>
);
}
export default ShareModal;