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

main
Last change on this file since acf690c was b62cefc, checked in by kikisrbinoska <srbinoskakristina07@…>, 4 months ago

Added frontend and imporoved AI Suggestions service

  • Property mode set to 100644
File size: 7.6 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 } = 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))
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 user_id: user.user_id,
65 type: 'collaboration',
66 title: 'Collaboration Invite',
67 message: `You've been invited to collaborate on "${storyTitle}" as ${selectedRole}.`,
68 link: `/story/${storyId}`,
69 })
70 addToast(`${user.username} added as collaborator!`)
71 setInviteOpen(false)
72 setSelectedUser('')
73 }
74
75 const handleRemove = (collab: Collaboration) => {
76 removeCollaboration(collab.user_id, storyId)
77 addToast(`${collab.username} removed from collaborators`, 'info')
78 }
79
80 return (
81 <div className="bg-slate-900 border border-slate-700 rounded-2xl p-6">
82 <div className="flex items-center justify-between mb-4">
83 <div className="flex items-center gap-2">
84 <Users size={18} className="text-violet-400" />
85 <h3 className="text-white font-semibold">Collaborators</h3>
86 <span className="text-xs text-slate-500">({storyCollabs.length})</span>
87 </div>
88 <Button size="sm" onClick={() => setInviteOpen(true)}>
89 <UserPlus size={14} />
90 Invite
91 </Button>
92 </div>
93
94 {storyCollabs.length === 0 ? (
95 <div className="text-center py-8 text-slate-500 text-sm">
96 <Users size={32} className="mx-auto mb-2 opacity-30" />
97 No collaborators yet
98 </div>
99 ) : (
100 <div className="space-y-3">
101 {storyCollabs.map(collab => (
102 <div key={collab.collab_id} className="flex items-center gap-3 p-3 bg-slate-800 rounded-xl border border-slate-700">
103 <Avatar name={collab.name} size="sm" />
104 <div className="flex-1 min-w-0">
105 <p className="text-sm font-medium text-white">@{collab.username}</p>
106 <div className="flex items-center gap-2 mt-0.5">
107 <span className="text-xs text-violet-400">{collab.role}</span>
108 <span className="text-slate-600">·</span>
109 <Shield size={11} className="text-slate-500" />
110 <span className="text-xs text-slate-500">Level {collab.permission_level}: {permissionLabels[collab.permission_level]}</span>
111 </div>
112 </div>
113 <div className="flex items-center gap-2">
114 <select
115 value={collab.permission_level}
116 onChange={e => {
117 updateCollaborationPermission(collab.user_id, storyId, Number(e.target.value) as PermissionLevel)
118 addToast('Permission updated')
119 }}
120 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"
121 >
122 {([1, 2, 3, 4, 5] as PermissionLevel[]).map(l => (
123 <option key={l} value={l}>Level {l}</option>
124 ))}
125 </select>
126 <button
127 onClick={() => handleRemove(collab)}
128 className="text-slate-500 hover:text-rose-400 transition-colors p-1"
129 >
130 <Trash2 size={14} />
131 </button>
132 </div>
133 </div>
134 ))}
135 </div>
136 )}
137
138 {/* Invite Modal */}
139 <Modal isOpen={inviteOpen} onClose={() => setInviteOpen(false)} title="Invite Collaborator">
140 <div className="space-y-4">
141 <div>
142 <label className="block text-sm text-slate-400 mb-1">Select User</label>
143 <select
144 value={selectedUser}
145 onChange={e => setSelectedUser(e.target.value)}
146 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"
147 >
148 <option value="">-- Choose a user --</option>
149 {availableUsers.map(u => (
150 <option key={u.user_id} value={u.username}>
151 @{u.username} ({u.role})
152 </option>
153 ))}
154 </select>
155 </div>
156 <div>
157 <label className="block text-sm text-slate-400 mb-1">Role</label>
158 <select
159 value={selectedRole}
160 onChange={e => setSelectedRole(e.target.value as CollaboratorRole)}
161 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"
162 >
163 {roles.map(r => <option key={r} value={r}>{r}</option>)}
164 </select>
165 </div>
166 <div>
167 <label className="block text-sm text-slate-400 mb-1">Permission Level</label>
168 <select
169 value={selectedLevel}
170 onChange={e => setSelectedLevel(Number(e.target.value) as PermissionLevel)}
171 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"
172 >
173 {([1, 2, 3, 4, 5] as PermissionLevel[]).map(l => (
174 <option key={l} value={l}>Level {l} – {permissionLabels[l]}</option>
175 ))}
176 </select>
177 </div>
178 <div className="flex gap-3 pt-2">
179 <Button variant="secondary" className="flex-1" onClick={() => setInviteOpen(false)}>
180 Cancel
181 </Button>
182 <Button className="flex-1" onClick={handleInvite} disabled={!selectedUser}>
183 Send Invite
184 </Button>
185 </div>
186 </div>
187 </Modal>
188 </div>
189 )
190}
Note: See TracBrowser for help on using the repository browser.