Changeset 7fbb91c


Ignore:
Timestamp:
03/24/26 23:03:39 (3 months ago)
Author:
kikisrbinoska <srbinoskakristina07@…>
Branches:
main
Children:
a7550ca
Parents:
73b69b2
Message:

Added functional collaboration between users

Files:
11 edited

Legend:

Unmodified
Added
Removed
  • ChapterX.API/Controllers/CollaborationsController.cs

    r73b69b2 r7fbb91c  
    11using ChapterX.Application.Collaboration.Commands;
    22using ChapterX.Application.Collaboration.Queries;
     3using ChapterX.Domain.Repositories;
    34using MediatR;
    45using Microsoft.AspNetCore.Authorization;
     
    1314    {
    1415        private readonly IMediator _mediator;
     16        private readonly ICollaborationRepository _collaborationRepository;
    1517        private readonly ILogger<CollaborationsController> _logger;
    1618
    17         public CollaborationsController(IMediator mediator, ILogger<CollaborationsController> logger)
     19        public CollaborationsController(IMediator mediator, ICollaborationRepository collaborationRepository, ILogger<CollaborationsController> logger)
    1820        {
    1921            _mediator = mediator;
     22            _collaborationRepository = collaborationRepository;
    2023            _logger = logger;
    2124        }
     
    2629        {
    2730            _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();
    3051        }
    3152
  • ChapterX.API/Controllers/UsersController.cs

    r73b69b2 r7fbb91c  
    2727            _logger.LogInformation("Fetching all users");
    2828            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);
    3039        }
    3140
  • ChapterX.Domain/Repositories/ICollaborationRepository.cs

    r73b69b2 r7fbb91c  
    55    public interface ICollaborationRepository : IRepository<Collaboration>
    66    {
     7        Task<bool> DeleteByUserAndStoryAsync(int userId, int storyId, CancellationToken cancellationToken = default);
    78    }
    89}
  • ChapterX.Infrastructure/Repositories/CollaborationRepository.cs

    r73b69b2 r7fbb91c  
    22using ChapterX.Domain.Repositories;
    33using 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;
     4using Microsoft.EntityFrameworkCore;
    95
    106namespace ChapterX.Infrastructure.Repositories
     
    1511        {
    1612        }
     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        }
    1727    }
    1828}
  • ChapterX.Infrastructure/Repositories/UserRepository.cs

    r73b69b2 r7fbb91c  
    1212        }
    1313
     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
    1420        public async Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default)
    1521            => await _dbSet
  • chapterx-frontend/src/App.tsx

    r73b69b2 r7fbb91c  
    5252
    5353  if (!currentUser) {
    54     addToast('Please sign in to access this page.', 'warning')
    5554    return <Navigate to="/login" replace />
    5655  }
     
    6867
    6968function App() {
    70   const { fetchStories, fetchChapters, fetchReadingLists } = useStoryStore()
     69  const { fetchStories, fetchChapters, fetchCollaborations, fetchReadingLists } = useStoryStore()
    7170
    7271  useEffect(() => {
    7372    fetchStories()
    7473    fetchChapters()
     74    fetchCollaborations()
    7575    fetchReadingLists()
    7676  }, [])
  • chapterx-frontend/src/components/writer/CollaboratorManager.tsx

    r73b69b2 r7fbb91c  
    3232}) => {
    3333  const { collaborations, addCollaboration, removeCollaboration, updateCollaborationPermission } = useStoryStore()
    34   const { allUsers } = useAuthStore()
     34  const { allUsers, fetchAllUsers } = useAuthStore()
    3535  const { addNotification } = useNotificationStore()
    3636  const { addToast } = useUIStore()
     
    4343  const storyCollabs = collaborations.filter(c => c.story_id === storyId)
    4444  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'))
    4646
    4747  const handleInvite = () => {
     
    6262    addCollaboration(newCollab)
    6363    addNotification({
    64       user_id: user.user_id,
     64      recipientUserId: user.user_id,
    6565      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}.`,
    6867      link: `/story/${storyId}`,
    6968    })
     
    8685          <span className="text-xs text-slate-500">({storyCollabs.length})</span>
    8786        </div>
    88         <Button size="sm" onClick={() => setInviteOpen(true)}>
     87        <Button size="sm" onClick={() => { fetchAllUsers(); setInviteOpen(true) }}>
    8988          <UserPlus size={14} />
    9089          Invite
     
    149148              {availableUsers.map(u => (
    150149                <option key={u.user_id} value={u.username}>
    151                   @{u.username} ({u.role})
     150                  @{u.username}
    152151                </option>
    153152              ))}
  • chapterx-frontend/src/pages/story/StoryDetailPage.tsx

    r73b69b2 r7fbb91c  
    102102
    103103  const isOwner = currentUser?.user_id === story.user_id
     104  const isCollaborator = currentUser ? storyCollabs.some(c => c.user_id === currentUser.user_id) : false
    104105
    105106  return (
     
    162163              </Button>
    163164            )}
    164             {isOwner && (
     165            {(isOwner || isCollaborator) && (
    165166              <Button variant="ghost" size="sm" onClick={() => navigate(`/writer/edit-story/${story.story_id}`)}>
    166167                Edit Story
     
    173174            <div className="flex items-center justify-between mb-4">
    174175              <h2 className="font-serif text-xl font-bold text-white">Chapters</h2>
    175               {isOwner && (
     176              {(isOwner || isCollaborator) && (
    176177                <Button size="sm" onClick={() => navigate(`/writer/create-chapter/${story.story_id}`)}>
    177178                  <Plus size={14} />
  • chapterx-frontend/src/pages/writer/WriterDashboard.tsx

    r73b69b2 r7fbb91c  
    2222  const navigate = useNavigate()
    2323  const { currentUser } = useAuthStore()
    24   const { stories } = useStoryStore()
     24  const { stories, collaborations } = useStoryStore()
    2525  const { notifications } = useNotificationStore()
    2626
    2727  if (!currentUser) return null
    2828
    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]
    3034  const published = myStories.filter(s => s.status === 'published')
    3135  const drafts = myStories.filter(s => s.status === 'draft')
     
    7680            <h2 className="font-serif text-xl font-bold text-white">My Stories</h2>
    7781          </div>
    78           {myStories.length === 0 ? (
     82          {allDashboardStories.length === 0 ? (
    7983            <div className="bg-slate-800 border border-slate-700 rounded-2xl p-12 text-center">
    8084              <BookOpen size={40} className="mx-auto mb-4 text-slate-600" />
     
    8892          ) : (
    8993            <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>
    96115                    </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>
    102119                    </div>
    103120                  </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              })}
    110123            </div>
    111124          )}
  • chapterx-frontend/src/store/authStore.ts

    r73b69b2 r7fbb91c  
    1919  updateUserRole: (userId: number, role: UserRole) => void
    2020  addUser: (user: User) => void
     21  fetchAllUsers: () => Promise<void>
    2122}
    2223
     
    135136      addUser: (user: User) =>
    136137        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      },
    137159    }),
    138160    {
  • chapterx-frontend/src/store/storyStore.ts

    r73b69b2 r7fbb91c  
    7373  fetchStories: () => Promise<void>
    7474  fetchChapters: () => Promise<void>
     75  fetchCollaborations: () => Promise<void>
    7576  fetchReadingLists: () => Promise<void>
    7677  fetchUserReadingLists: (userId: number) => Promise<void>
     
    9899
    99100  // Collaboration actions
    100   addCollaboration: (collab: Collaboration) => void
     101  addCollaboration: (collab: Collaboration) => Promise<void>
    101102  updateCollaborationPermission: (userId: number, storyId: number, level: PermissionLevel) => void
    102   removeCollaboration: (userId: number, storyId: number) => void
     103  removeCollaboration: (userId: number, storyId: number) => Promise<void>
    103104
    104105  // AI Suggestion actions
     
    175176    } catch {
    176177      // 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
    177199    }
    178200  },
     
    364386    get().likedStories.some(l => l.userId === userId && l.storyId === storyId),
    365387
    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  },
    368400
    369401  updateCollaborationPermission: (userId, storyId, level) =>
     
    376408    })),
    377409
    378   removeCollaboration: (userId, storyId) =>
     410  removeCollaboration: async (userId, storyId) => {
    379411    set(state => ({
    380412      collaborations: state.collaborations.filter(
    381413        c => !(c.user_id === userId && c.story_id === storyId)
    382414      ),
    383     })),
     415    }))
     416    try {
     417      await axios.delete(`${API}/collaborations/user/${userId}/story/${storyId}`, { headers: getAuthHeaders() })
     418    } catch {
     419      // keep optimistic
     420    }
     421  },
    384422
    385423  fetchSuggestions: async () => {
Note: See TracChangeset for help on using the changeset viewer.