source: chapterx-frontend/src/components/writer/CollaboratorManager.tsx@ 99c1e45

main
Last change on this file since 99c1e45 was 7fbb91c, checked in by kikisrbinoska <srbinoskakristina07@…>, 3 months ago

Added functional collaboration between users

  • Property mode set to 100644
File size: 7.7 KB
Line 
1import React, { useState } from 'react'
2import { Users, Trash2, UserPlus, Shield } from 'lucide-react'
3import { useStoryStore } from '../../store/storyStore'
4import { useAuthStore } from '../../store/authStore'
5import { useNotificationStore } from '../../store/notificationStore'
6import { useUIStore } from '../../store/uiStore'
7import { Collaboration, CollaboratorRole, PermissionLevel } from '../../types'
8import { Button } from '../ui/Button'
9import { Avatar } from '../ui/Avatar'
10import { Modal } from '../ui/Modal'
11
12interface CollaboratorManagerProps {
13 storyId: number
14 storyTitle: string
15 ownerId: number
16}
17
18const roles: CollaboratorRole[] = ['co-author', 'editor', 'proofreader', 'beta-reader']
19
20const permissionLabels: Record<PermissionLevel, string> = {
21 1: 'View Only',
22 2: 'Comment',
23 3: 'Edit Chapters',
24 4: 'Manage Story',
25 5: 'Full Access',
26}
27
28export const CollaboratorManager: React.FC<CollaboratorManagerProps> = ({
29 storyId,
30 storyTitle,
31 ownerId,
32}) => {
33 const { collaborations, addCollaboration, removeCollaboration, updateCollaborationPermission } = useStoryStore()
34 const { allUsers, fetchAllUsers } = useAuthStore()
35 const { addNotification } = useNotificationStore()
36 const { addToast } = useUIStore()
37
38 const [inviteOpen, setInviteOpen] = useState(false)
39 const [selectedUser, setSelectedUser] = useState('')
40 const [selectedRole, setSelectedRole] = useState<CollaboratorRole>('editor')
41 const [selectedLevel, setSelectedLevel] = useState<PermissionLevel>(3)
42
43 const storyCollabs = collaborations.filter(c => c.story_id === storyId)
44 const collabUserIds = new Set(storyCollabs.map(c => c.user_id))
45 const availableUsers = allUsers.filter(u => u.user_id !== ownerId && !collabUserIds.has(u.user_id) && (u.role === 'writer' || u.role === 'admin'))
46
47 const handleInvite = () => {
48 const user = availableUsers.find(u => u.username === selectedUser)
49 if (!user) { addToast('User not found', 'error'); return }
50
51 const newCollab: Collaboration = {
52 collab_id: Date.now(),
53 story_id: storyId,
54 story_title: storyTitle,
55 user_id: user.user_id,
56 username: user.username,
57 name: user.name,
58 role: selectedRole,
59 permission_level: selectedLevel,
60 joined_at: new Date().toISOString(),
61 }
62 addCollaboration(newCollab)
63 addNotification({
64 recipientUserId: user.user_id,
65 type: 'collaboration',
66 content: `You've been invited to collaborate on "${storyTitle}" as ${selectedRole}.`,
67 link: `/story/${storyId}`,
68 })
69 addToast(`${user.username} added as collaborator!`)
70 setInviteOpen(false)
71 setSelectedUser('')
72 }
73
74 const handleRemove = (collab: Collaboration) => {
75 removeCollaboration(collab.user_id, storyId)
76 addToast(`${collab.username} removed from collaborators`, 'info')
77 }
78
79 return (
80 <div className="bg-slate-900 border border-slate-700 rounded-2xl p-6">
81 <div className="flex items-center justify-between mb-4">
82 <div className="flex items-center gap-2">
83 <Users size={18} className="text-violet-400" />
84 <h3 className="text-white font-semibold">Collaborators</h3>
85 <span className="text-xs text-slate-500">({storyCollabs.length})</span>
86 </div>
87 <Button size="sm" onClick={() => { fetchAllUsers(); setInviteOpen(true) }}>
88 <UserPlus size={14} />
89 Invite
90 </Button>
91 </div>
92
93 {storyCollabs.length === 0 ? (
94 <div className="text-center py-8 text-slate-500 text-sm">
95 <Users size={32} className="mx-auto mb-2 opacity-30" />
96 No collaborators yet
97 </div>
98 ) : (
99 <div className="space-y-3">
100 {storyCollabs.map(collab => (
101 <div key={collab.collab_id} className="flex items-center gap-3 p-3 bg-slate-800 rounded-xl border border-slate-700">
102 <Avatar name={collab.name} size="sm" />
103 <div className="flex-1 min-w-0">
104 <p className="text-sm font-medium text-white">@{collab.username}</p>
105 <div className="flex items-center gap-2 mt-0.5">
106 <span className="text-xs text-violet-400">{collab.role}</span>
107 <span className="text-slate-600">·</span>
108 <Shield size={11} className="text-slate-500" />
109 <span className="text-xs text-slate-500">Level {collab.permission_level}: {permissionLabels[collab.permission_level]}</span>
110 </div>
111 </div>
112 <div className="flex items-center gap-2">
113 <select
114 value={collab.permission_level}
115 onChange={e => {
116 updateCollaborationPermission(collab.user_id, storyId, Number(e.target.value) as PermissionLevel)
117 addToast('Permission updated')
118 }}
119 className="bg-slate-700 border border-slate-600 text-slate-300 text-xs rounded-lg px-2 py-1 focus:outline-none focus:border-indigo-500"
120 >
121 {([1, 2, 3, 4, 5] as PermissionLevel[]).map(l => (
122 <option key={l} value={l}>Level {l}</option>
123 ))}
124 </select>
125 <button
126 onClick={() => handleRemove(collab)}
127 className="text-slate-500 hover:text-rose-400 transition-colors p-1"
128 >
129 <Trash2 size={14} />
130 </button>
131 </div>
132 </div>
133 ))}
134 </div>
135 )}
136
137 {/* Invite Modal */}
138 <Modal isOpen={inviteOpen} onClose={() => setInviteOpen(false)} title="Invite Collaborator">
139 <div className="space-y-4">
140 <div>
141 <label className="block text-sm text-slate-400 mb-1">Select User</label>
142 <select
143 value={selectedUser}
144 onChange={e => setSelectedUser(e.target.value)}
145 className="w-full bg-slate-800 border border-slate-700 text-slate-300 rounded-xl px-4 py-2 focus:outline-none focus:border-indigo-500"
146 >
147 <option value="">-- Choose a user --</option>
148 {availableUsers.map(u => (
149 <option key={u.user_id} value={u.username}>
150 @{u.username}
151 </option>
152 ))}
153 </select>
154 </div>
155 <div>
156 <label className="block text-sm text-slate-400 mb-1">Role</label>
157 <select
158 value={selectedRole}
159 onChange={e => setSelectedRole(e.target.value as CollaboratorRole)}
160 className="w-full bg-slate-800 border border-slate-700 text-slate-300 rounded-xl px-4 py-2 focus:outline-none focus:border-indigo-500"
161 >
162 {roles.map(r => <option key={r} value={r}>{r}</option>)}
163 </select>
164 </div>
165 <div>
166 <label className="block text-sm text-slate-400 mb-1">Permission Level</label>
167 <select
168 value={selectedLevel}
169 onChange={e => setSelectedLevel(Number(e.target.value) as PermissionLevel)}
170 className="w-full bg-slate-800 border border-slate-700 text-slate-300 rounded-xl px-4 py-2 focus:outline-none focus:border-indigo-500"
171 >
172 {([1, 2, 3, 4, 5] as PermissionLevel[]).map(l => (
173 <option key={l} value={l}>Level {l} – {permissionLabels[l]}</option>
174 ))}
175 </select>
176 </div>
177 <div className="flex gap-3 pt-2">
178 <Button variant="secondary" className="flex-1" onClick={() => setInviteOpen(false)}>
179 Cancel
180 </Button>
181 <Button className="flex-1" onClick={handleInvite} disabled={!selectedUser}>
182 Send Invite
183 </Button>
184 </div>
185 </div>
186 </Modal>
187 </div>
188 )
189}
Note: See TracBrowser for help on using the repository browser.