Changeset 7fbb91c
- Timestamp:
- 03/24/26 23:03:39 (3 months ago)
- Branches:
- main
- Children:
- a7550ca
- Parents:
- 73b69b2
- Files:
-
- 11 edited
-
ChapterX.API/Controllers/CollaborationsController.cs (modified) (3 diffs)
-
ChapterX.API/Controllers/UsersController.cs (modified) (1 diff)
-
ChapterX.Domain/Repositories/ICollaborationRepository.cs (modified) (1 diff)
-
ChapterX.Infrastructure/Repositories/CollaborationRepository.cs (modified) (2 diffs)
-
ChapterX.Infrastructure/Repositories/UserRepository.cs (modified) (1 diff)
-
chapterx-frontend/src/App.tsx (modified) (2 diffs)
-
chapterx-frontend/src/components/writer/CollaboratorManager.tsx (modified) (5 diffs)
-
chapterx-frontend/src/pages/story/StoryDetailPage.tsx (modified) (3 diffs)
-
chapterx-frontend/src/pages/writer/WriterDashboard.tsx (modified) (3 diffs)
-
chapterx-frontend/src/store/authStore.ts (modified) (2 diffs)
-
chapterx-frontend/src/store/storyStore.ts (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
ChapterX.API/Controllers/CollaborationsController.cs
r73b69b2 r7fbb91c 1 1 using ChapterX.Application.Collaboration.Commands; 2 2 using ChapterX.Application.Collaboration.Queries; 3 using ChapterX.Domain.Repositories; 3 4 using MediatR; 4 5 using Microsoft.AspNetCore.Authorization; … … 13 14 { 14 15 private readonly IMediator _mediator; 16 private readonly ICollaborationRepository _collaborationRepository; 15 17 private readonly ILogger<CollaborationsController> _logger; 16 18 17 public CollaborationsController(IMediator mediator, I Logger<CollaborationsController> logger)19 public CollaborationsController(IMediator mediator, ICollaborationRepository collaborationRepository, ILogger<CollaborationsController> logger) 18 20 { 19 21 _mediator = mediator; 22 _collaborationRepository = collaborationRepository; 20 23 _logger = logger; 21 24 } … … 26 29 { 27 30 _logger.LogInformation("Fetching all collaborations"); 28 var response = await _mediator.Send(new GetAllRequest()); 29 return Ok(response); 31 var collabs = await _collaborationRepository.GetAllAsync(); 32 var result = collabs.Select(c => new 33 { 34 id = c.Id, 35 userId = c.UserId, 36 storyId = c.StoryId, 37 username = c.User != null ? c.User.Username : "", 38 name = c.User != null ? c.User.Name : "", 39 createdAt = c.CreatedAt, 40 }); 41 return Ok(result); 42 } 43 44 [HttpDelete("user/{userId:int}/story/{storyId:int}")] 45 [Authorize] 46 public async Task<ActionResult> DeleteByUserAndStory(int userId, int storyId) 47 { 48 var deleted = await _collaborationRepository.DeleteByUserAndStoryAsync(userId, storyId); 49 if (!deleted) return NotFound(); 50 return Ok(); 30 51 } 31 52 -
ChapterX.API/Controllers/UsersController.cs
r73b69b2 r7fbb91c 27 27 _logger.LogInformation("Fetching all users"); 28 28 var response = await _mediator.Send(new GetAllRequest()); 29 return Ok(response); 29 var result = response.Users.Select(u => new 30 { 31 id = u.Id, 32 username = u.Username, 33 name = u.Name, 34 surname = u.Surname, 35 email = u.Email, 36 role = u.Admin != null ? "admin" : u.Writer != null ? "writer" : "regular", 37 }); 38 return Ok(result); 30 39 } 31 40 -
ChapterX.Domain/Repositories/ICollaborationRepository.cs
r73b69b2 r7fbb91c 5 5 public interface ICollaborationRepository : IRepository<Collaboration> 6 6 { 7 Task<bool> DeleteByUserAndStoryAsync(int userId, int storyId, CancellationToken cancellationToken = default); 7 8 } 8 9 } -
ChapterX.Infrastructure/Repositories/CollaborationRepository.cs
r73b69b2 r7fbb91c 2 2 using ChapterX.Domain.Repositories; 3 3 using ChapterX.Infrastructure.Data.DataContext; 4 using System; 5 using System.Collections.Generic; 6 using System.Linq; 7 using System.Text; 8 using System.Threading.Tasks; 4 using Microsoft.EntityFrameworkCore; 9 5 10 6 namespace ChapterX.Infrastructure.Repositories … … 15 11 { 16 12 } 13 14 public override async Task<IEnumerable<Collaboration>> GetAllAsync(CancellationToken cancellationToken = default) 15 => await _dbSet 16 .Include(c => c.User) 17 .ToListAsync(cancellationToken); 18 19 public async Task<bool> DeleteByUserAndStoryAsync(int userId, int storyId, CancellationToken cancellationToken = default) 20 { 21 var collab = await _dbSet.FirstOrDefaultAsync(c => c.UserId == userId && c.StoryId == storyId, cancellationToken); 22 if (collab == null) return false; 23 _dbSet.Remove(collab); 24 await _context.SaveChangesAsync(cancellationToken); 25 return true; 26 } 17 27 } 18 28 } -
ChapterX.Infrastructure/Repositories/UserRepository.cs
r73b69b2 r7fbb91c 12 12 } 13 13 14 public override async Task<IEnumerable<User>> GetAllAsync(CancellationToken cancellationToken = default) 15 => await _dbSet 16 .Include(u => u.Admin) 17 .Include(u => u.Writer) 18 .ToListAsync(cancellationToken); 19 14 20 public async Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default) 15 21 => await _dbSet -
chapterx-frontend/src/App.tsx
r73b69b2 r7fbb91c 52 52 53 53 if (!currentUser) { 54 addToast('Please sign in to access this page.', 'warning')55 54 return <Navigate to="/login" replace /> 56 55 } … … 68 67 69 68 function App() { 70 const { fetchStories, fetchChapters, fetch ReadingLists } = useStoryStore()69 const { fetchStories, fetchChapters, fetchCollaborations, fetchReadingLists } = useStoryStore() 71 70 72 71 useEffect(() => { 73 72 fetchStories() 74 73 fetchChapters() 74 fetchCollaborations() 75 75 fetchReadingLists() 76 76 }, []) -
chapterx-frontend/src/components/writer/CollaboratorManager.tsx
r73b69b2 r7fbb91c 32 32 }) => { 33 33 const { collaborations, addCollaboration, removeCollaboration, updateCollaborationPermission } = useStoryStore() 34 const { allUsers } = useAuthStore()34 const { allUsers, fetchAllUsers } = useAuthStore() 35 35 const { addNotification } = useNotificationStore() 36 36 const { addToast } = useUIStore() … … 43 43 const storyCollabs = collaborations.filter(c => c.story_id === storyId) 44 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) )45 const availableUsers = allUsers.filter(u => u.user_id !== ownerId && !collabUserIds.has(u.user_id) && (u.role === 'writer' || u.role === 'admin')) 46 46 47 47 const handleInvite = () => { … … 62 62 addCollaboration(newCollab) 63 63 addNotification({ 64 user_id: user.user_id,64 recipientUserId: user.user_id, 65 65 type: 'collaboration', 66 title: 'Collaboration Invite', 67 message: `You've been invited to collaborate on "${storyTitle}" as ${selectedRole}.`, 66 content: `You've been invited to collaborate on "${storyTitle}" as ${selectedRole}.`, 68 67 link: `/story/${storyId}`, 69 68 }) … … 86 85 <span className="text-xs text-slate-500">({storyCollabs.length})</span> 87 86 </div> 88 <Button size="sm" onClick={() => setInviteOpen(true)}>87 <Button size="sm" onClick={() => { fetchAllUsers(); setInviteOpen(true) }}> 89 88 <UserPlus size={14} /> 90 89 Invite … … 149 148 {availableUsers.map(u => ( 150 149 <option key={u.user_id} value={u.username}> 151 @{u.username} ({u.role})150 @{u.username} 152 151 </option> 153 152 ))} -
chapterx-frontend/src/pages/story/StoryDetailPage.tsx
r73b69b2 r7fbb91c 102 102 103 103 const isOwner = currentUser?.user_id === story.user_id 104 const isCollaborator = currentUser ? storyCollabs.some(c => c.user_id === currentUser.user_id) : false 104 105 105 106 return ( … … 162 163 </Button> 163 164 )} 164 { isOwner&& (165 {(isOwner || isCollaborator) && ( 165 166 <Button variant="ghost" size="sm" onClick={() => navigate(`/writer/edit-story/${story.story_id}`)}> 166 167 Edit Story … … 173 174 <div className="flex items-center justify-between mb-4"> 174 175 <h2 className="font-serif text-xl font-bold text-white">Chapters</h2> 175 { isOwner&& (176 {(isOwner || isCollaborator) && ( 176 177 <Button size="sm" onClick={() => navigate(`/writer/create-chapter/${story.story_id}`)}> 177 178 <Plus size={14} /> -
chapterx-frontend/src/pages/writer/WriterDashboard.tsx
r73b69b2 r7fbb91c 22 22 const navigate = useNavigate() 23 23 const { currentUser } = useAuthStore() 24 const { stories } = useStoryStore()24 const { stories, collaborations } = useStoryStore() 25 25 const { notifications } = useNotificationStore() 26 26 27 27 if (!currentUser) return null 28 28 29 const myStories = stories.filter(s => s.user_id === currentUser.user_id) 29 const ownedStories = stories.filter(s => s.user_id === currentUser.user_id) 30 const collabStoryIds = new Set(collaborations.filter(c => c.user_id === currentUser.user_id).map(c => c.story_id)) 31 const collabStories = stories.filter(s => collabStoryIds.has(s.story_id)) 32 const myStories = ownedStories 33 const allDashboardStories = [...ownedStories, ...collabStories] 30 34 const published = myStories.filter(s => s.status === 'published') 31 35 const drafts = myStories.filter(s => s.status === 'draft') … … 76 80 <h2 className="font-serif text-xl font-bold text-white">My Stories</h2> 77 81 </div> 78 { myStories.length === 0 ? (82 {allDashboardStories.length === 0 ? ( 79 83 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-12 text-center"> 80 84 <BookOpen size={40} className="mx-auto mb-4 text-slate-600" /> … … 88 92 ) : ( 89 93 <div className="space-y-3"> 90 {myStories.map(story => ( 91 <div key={story.story_id} className="flex items-center gap-4 p-4 bg-slate-800 border border-slate-700 rounded-xl hover:border-indigo-500/40 transition-colors"> 92 <div className="flex-1 min-w-0"> 93 <div className="flex items-center gap-2 mb-1"> 94 <h3 className="text-white font-medium text-sm truncate">{story.title}</h3> 95 <StatusBadge status={story.status} /> 94 {allDashboardStories.map(story => { 95 const isCollab = collabStoryIds.has(story.story_id) 96 return ( 97 <div key={story.story_id} className="flex items-center gap-4 p-4 bg-slate-800 border border-slate-700 rounded-xl hover:border-indigo-500/40 transition-colors"> 98 <div className="flex-1 min-w-0"> 99 <div className="flex items-center gap-2 mb-1"> 100 <h3 className="text-white font-medium text-sm truncate">{story.title}</h3> 101 <StatusBadge status={story.status} /> 102 {isCollab && ( 103 <span className="px-2 py-0.5 text-xs font-medium rounded-full bg-violet-500/20 text-violet-400 border border-violet-500/30"> 104 Collaborator 105 </span> 106 )} 107 </div> 108 <div className="flex items-center gap-3 text-slate-500 text-xs"> 109 {isCollab && <span className="text-slate-500">by {story.author_username}</span>} 110 <span className="flex items-center gap-1"><Eye size={11} /> {story.total_views.toLocaleString()}</span> 111 <span className="flex items-center gap-1"><Heart size={11} /> {story.total_likes}</span> 112 <span className="flex items-center gap-1"><MessageCircle size={11} /> {story.total_comments}</span> 113 <span>{story.total_chapters} chapters</span> 114 </div> 96 115 </div> 97 <div className="flex items-center gap-3 text-slate-500 text-xs"> 98 <span className="flex items-center gap-1"><Eye size={11} /> {story.total_views.toLocaleString()}</span> 99 <span className="flex items-center gap-1"><Heart size={11} /> {story.total_likes}</span> 100 <span className="flex items-center gap-1"><MessageCircle size={11} /> {story.total_comments}</span> 101 <span>{story.total_chapters} chapters</span> 116 <div className="flex gap-2 flex-shrink-0"> 117 <Button size="sm" variant="ghost" onClick={() => navigate(`/story/${story.story_id}`)}>View</Button> 118 <Button size="sm" variant="secondary" onClick={() => navigate(`/writer/edit-story/${story.story_id}`)}>Edit</Button> 102 119 </div> 103 120 </div> 104 <div className="flex gap-2 flex-shrink-0"> 105 <Button size="sm" variant="ghost" onClick={() => navigate(`/story/${story.story_id}`)}>View</Button> 106 <Button size="sm" variant="secondary" onClick={() => navigate(`/writer/edit-story/${story.story_id}`)}>Edit</Button> 107 </div> 108 </div> 109 ))} 121 ) 122 })} 110 123 </div> 111 124 )} -
chapterx-frontend/src/store/authStore.ts
r73b69b2 r7fbb91c 19 19 updateUserRole: (userId: number, role: UserRole) => void 20 20 addUser: (user: User) => void 21 fetchAllUsers: () => Promise<void> 21 22 } 22 23 … … 135 136 addUser: (user: User) => 136 137 set(state => ({ allUsers: [...state.allUsers, user] })), 138 139 fetchAllUsers: async () => { 140 try { 141 const res = await axios.get(`${API_BASE}/users`) 142 const data: any[] = res.data?.users ?? res.data ?? [] 143 const users: User[] = data.map((u: any) => ({ 144 user_id: u.id, 145 username: u.username, 146 email: u.email ?? '', 147 name: u.name ?? u.username, 148 surname: u.surname ?? '', 149 role: (u.role ?? 'regular') as UserRole, 150 created_at: u.createdAt ?? new Date().toISOString(), 151 follower_count: 0, 152 following_count: 0, 153 })) 154 set({ allUsers: users }) 155 } catch { 156 // keep existing 157 } 158 }, 137 159 }), 138 160 { -
chapterx-frontend/src/store/storyStore.ts
r73b69b2 r7fbb91c 73 73 fetchStories: () => Promise<void> 74 74 fetchChapters: () => Promise<void> 75 fetchCollaborations: () => Promise<void> 75 76 fetchReadingLists: () => Promise<void> 76 77 fetchUserReadingLists: (userId: number) => Promise<void> … … 98 99 99 100 // Collaboration actions 100 addCollaboration: (collab: Collaboration) => void101 addCollaboration: (collab: Collaboration) => Promise<void> 101 102 updateCollaborationPermission: (userId: number, storyId: number, level: PermissionLevel) => void 102 removeCollaboration: (userId: number, storyId: number) => void103 removeCollaboration: (userId: number, storyId: number) => Promise<void> 103 104 104 105 // AI Suggestion actions … … 175 176 } catch { 176 177 // keep mock data on failure 178 } 179 }, 180 181 fetchCollaborations: async () => { 182 try { 183 const res = await axios.get(`${API}/collaborations`) 184 const data: any[] = res.data ?? [] 185 const collaborations: Collaboration[] = data.map((c: any) => ({ 186 collab_id: c.id, 187 story_id: c.storyId, 188 user_id: c.userId, 189 username: c.username ?? '', 190 name: c.name ?? c.username ?? '', 191 story_title: '', 192 role: 'editor' as any, 193 permission_level: 3 as any, 194 joined_at: c.createdAt, 195 })) 196 set({ collaborations }) 197 } catch { 198 // keep existing 177 199 } 178 200 }, … … 364 386 get().likedStories.some(l => l.userId === userId && l.storyId === storyId), 365 387 366 addCollaboration: (collab) => 367 set(state => ({ collaborations: [...state.collaborations, collab] })), 388 addCollaboration: async (collab) => { 389 set(state => ({ collaborations: [...state.collaborations, collab] })) 390 try { 391 await axios.post(`${API}/collaborations`, { 392 userId: collab.user_id, 393 storyId: collab.story_id, 394 role: collab.role, 395 }, { headers: getAuthHeaders() }) 396 } catch { 397 // keep optimistic 398 } 399 }, 368 400 369 401 updateCollaborationPermission: (userId, storyId, level) => … … 376 408 })), 377 409 378 removeCollaboration: (userId, storyId) =>410 removeCollaboration: async (userId, storyId) => { 379 411 set(state => ({ 380 412 collaborations: state.collaborations.filter( 381 413 c => !(c.user_id === userId && c.story_id === storyId) 382 414 ), 383 })), 415 })) 416 try { 417 await axios.delete(`${API}/collaborations/user/${userId}/story/${storyId}`, { headers: getAuthHeaders() }) 418 } catch { 419 // keep optimistic 420 } 421 }, 384 422 385 423 fetchSuggestions: async () => {
Note:
See TracChangeset
for help on using the changeset viewer.
