Changeset 0b502c2


Ignore:
Timestamp:
06/23/26 17:20:47 (12 days ago)
Author:
kikisrbinoska <srbinoskakristina07@…>
Branches:
main
Children:
99c1e45
Parents:
b373fea
Message:

Fixed user profile and reading lists

Files:
19 edited

Legend:

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

    rb373fea r0b502c2  
    55using Microsoft.AspNetCore.Mvc;
    66using Microsoft.Extensions.Logging;
    7 using System.IdentityModel.Tokens.Jwt;
    87using System.Security.Claims;
    98
     
    6059            }
    6160
    62             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     61            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    6362            var response = await _mediator.Send(request with { CallerId = callerId });
    6463            return Ok(response);
     
    7069        {
    7170            _logger.LogInformation("Deleting chapter with ID: {ChapterId}", id);
    72             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     71            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    7372            var response = await _mediator.Send(new DeleteRequest(id, callerId));
    7473            return Ok(response);
  • ChapterX.API/Controllers/CommentsController.cs

    rb373fea r0b502c2  
    66using Microsoft.AspNetCore.Mvc;
    77using Microsoft.Extensions.Logging;
    8 using System.IdentityModel.Tokens.Jwt;
    98using System.Security.Claims;
    109
     
    6564        public async Task<ActionResult> Add([FromBody] AddRequest request)
    6665        {
    67             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     66            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    6867            _logger.LogInformation("Adding a new comment");
    6968            var response = await _mediator.Send(request with { UserId = callerId });
     
    8180            }
    8281
    83             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     82            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    8483            var response = await _mediator.Send(request with { CallerId = callerId });
    8584            return Ok(response);
     
    9190        {
    9291            _logger.LogInformation("Deleting comment with ID: {CommentId}", id);
    93             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     92            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    9493            var response = await _mediator.Send(new DeleteRequest(id, callerId));
    9594            return Ok(response);
  • ChapterX.API/Controllers/StoriesController.cs

    rb373fea r0b502c2  
    55using Microsoft.AspNetCore.Mvc;
    66using Microsoft.Extensions.Logging;
    7 using System.IdentityModel.Tokens.Jwt;
    87using System.Security.Claims;
    98
     
    4847        public async Task<ActionResult> Add([FromBody] AddRequest request)
    4948        {
    50             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     49            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    5150            _logger.LogInformation("Adding a new story for UserId: {UserId}", callerId);
    5251            var response = await _mediator.Send(request with { UserId = callerId });
     
    6564            }
    6665
    67             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     66            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    6867            var response = await _mediator.Send(request with { CallerId = callerId });
    6968            return Ok(response);
     
    7675        {
    7776            _logger.LogInformation("Deleting story with ID: {StoryId}", id);
    78             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     77            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    7978            var response = await _mediator.Send(new DeleteRequest(id, callerId));
    8079            return Ok(response);
  • ChapterX.API/Controllers/UsersController.cs

    rb373fea r0b502c2  
    55using Microsoft.AspNetCore.Mvc;
    66using Microsoft.Extensions.Logging;
    7 using System.IdentityModel.Tokens.Jwt;
    87using System.Security.Claims;
    98
     
    6968            }
    7069
    71             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     70            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    7271            var isAdmin = User.IsInRole("Admin");
    7372            if (callerId != id && !isAdmin)
     
    8382        {
    8483            _logger.LogInformation("Deleting user with ID: {UserId}", id);
    85             var callerId = int.Parse(User.FindFirstValue(JwtRegisteredClaimNames.Sub)!);
     84            var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
    8685            var isAdmin = User.IsInRole("Admin");
    8786            if (callerId != id && !isAdmin)
  • ChapterX.API/Program.cs

    rb373fea r0b502c2  
    99
    1010var jwtKey = builder.Configuration["Jwt:Key"];
    11 if (string.IsNullOrWhiteSpace(jwtKey) || jwtKey.StartsWith("change-this"))
     11if (string.IsNullOrWhiteSpace(jwtKey))
    1212    throw new InvalidOperationException("Jwt:Key is not configured. Set it via environment variable DOTNET_Jwt__Key before starting the application.");
    1313
     
    1717        policy.WithOrigins("http://localhost:5173", "https://localhost:5173")
    1818              .AllowAnyHeader()
    19               .AllowAnyMethod());
     19              .AllowAnyMethod()
     20              .AllowCredentials());
    2021});
    2122
  • ChapterX.Infrastructure/Data/DataContext/ApplicationDbContext.cs

    rb373fea r0b502c2  
    187187                e.Property(x => x.IsPublic).HasColumnName("is_public");
    188188                e.Property(x => x.UserId).HasColumnName("user_id");
    189                 e.Property(x => x.CreatedAt).HasColumnName("created_at");
    190                 e.Property(x => x.UpdatedAt).HasColumnName("updated_at");
     189                e.Property(x => x.CreatedAt).HasColumnName("list_created_at");
     190                e.Property(x => x.UpdatedAt).HasColumnName("list_updated_at");
    191191                e.HasOne(x => x.User).WithMany(u => u.ReadingLists).HasForeignKey(x => x.UserId);
    192192            });
  • ChapterX.Infrastructure/Repositories/StoryRepository.cs

    rb373fea r0b502c2  
    1919                .Include(s => s.Writer)
    2020                    .ThenInclude(w => w!.User)
     21                .Include(s => s.Likes)
     22                .Include(s => s.Comments)
     23                .Include(s => s.Chapters)
    2124                .ToListAsync(cancellationToken);
    2225        }
  • chapterx-frontend/src/App.tsx

    rb373fea r0b502c2  
    88import { Footer } from './components/layout/Footer'
    99import { ToastContainer } from './components/ui/Toast'
    10 import { DevSwitcher } from './components/DevSwitcher'
    1110import { Spinner } from './components/ui/Spinner'
    1211
     
    188187      <Footer />
    189188      <ToastContainer />
    190       <DevSwitcher />
    191189    </div>
    192190  )
  • chapterx-frontend/src/components/layout/Footer.tsx

    rb373fea r0b502c2  
    1414            <span className="font-serif font-bold text-white">ChapterX</span>
    1515          </Link>
    16           <p className="text-slate-500 text-sm">
    17             A collaborative storytelling platform for writers and readers.
    18           </p>
    1916        </div>
    2017        <div>
  • chapterx-frontend/src/data/mockData.ts

    rb373fea r0b502c2  
    406406
    407407export const mockGenres: Genre[] = [
    408   { genre_id: 1, name: 'Fantasy', story_count: 4 },
    409   { genre_id: 2, name: 'Adventure', story_count: 2 },
    410   { genre_id: 3, name: 'Romance', story_count: 3 },
    411   { genre_id: 4, name: 'Sci-Fi', story_count: 1 },
    412   { genre_id: 5, name: 'Historical Fiction', story_count: 2 },
    413   { genre_id: 6, name: 'Thriller', story_count: 1 },
    414   { genre_id: 7, name: 'Mystery', story_count: 0 },
    415   { genre_id: 8, name: 'Horror', story_count: 0 },
    416   { genre_id: 9, name: 'Contemporary', story_count: 0 },
    417   { genre_id: 10, name: 'Poetry', story_count: 0 },
     408  { genre_id: 1, name: 'Fantasy' },
     409  { genre_id: 2, name: 'Adventure' },
     410  { genre_id: 3, name: 'Romance' },
     411  { genre_id: 4, name: 'Sci-Fi' },
     412  { genre_id: 5, name: 'Historical Fiction' },
     413  { genre_id: 6, name: 'Thriller' },
     414  { genre_id: 7, name: 'Mystery' },
     415  { genre_id: 8, name: 'Horror' },
     416  { genre_id: 9, name: 'Contemporary' },
     417  { genre_id: 10, name: 'Poetry' },
    418418]
    419419
  • chapterx-frontend/src/pages/LandingPage.tsx

    rb373fea r0b502c2  
    11import React from 'react'
    22import { useNavigate } from 'react-router-dom'
    3 import { Feather, Star, BookOpen, Users, Sparkles, ArrowRight, ChevronRight } from 'lucide-react'
     3import { Feather, BookOpen, Users, Sparkles, ArrowRight, ChevronRight } from 'lucide-react'
    44import logo from '../assets/chapterX-removebg-preview.png'
    55import { useStoryStore } from '../store/storyStore'
     
    3434
    3535        <div className="relative max-w-7xl mx-auto px-4 text-center">
    36           <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass text-sm text-indigo-300 mb-8 border border-indigo-500/20">
    37             <Star size={14} className="text-amber-400 fill-amber-400" />
    38             The collaborative storytelling platform
    39           </div>
    40 
    4136          <div className="flex justify-center mb-6">
    4237            <img src={logo} alt="ChapterX" className="h-32 w-32 object-contain" />
  • chapterx-frontend/src/pages/admin/AdminGenresPage.tsx

    rb373fea r0b502c2  
    1010export const AdminGenresPage: React.FC = () => {
    1111  const navigate = useNavigate()
    12   const { genres, fetchGenres, addGenre, deleteGenre } = useStoryStore()
     12  const { genres, fetchGenres, addGenre, deleteGenre, stories } = useStoryStore()
     13  const publishedStories = stories.filter(s => s.status === 'published')
    1314  const { addToast } = useUIStore()
    1415  const [addOpen, setAddOpen] = useState(false)
     
    7576              </div>
    7677              <div className="flex items-center gap-4">
    77                 <span className="text-slate-500 text-sm">{genre.story_count} stories</span>
     78                <span className="text-slate-500 text-sm">{publishedStories.filter(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase())).length} stories</span>
    7879                <button
    7980                  onClick={() => setDeleteTarget(genre)}
    8081                  className={`transition-colors p-1 rounded ${
    81                     genre.story_count > 0
     82                    publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase()))
    8283                      ? 'text-slate-700 cursor-not-allowed'
    8384                      : 'text-slate-500 hover:text-rose-400 hover:bg-rose-500/10'
    8485                  }`}
    85                   disabled={genre.story_count > 0}
    86                   title={genre.story_count > 0 ? 'Cannot delete genre with stories' : 'Delete genre'}
     86                  disabled={publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase()))}
     87                  title={publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase())) ? 'Cannot delete genre with stories' : 'Delete genre'}
    8788                >
    8889                  <Trash2 size={14} />
  • chapterx-frontend/src/pages/auth/LoginPage.tsx

    rb373fea r0b502c2  
    66import { useUIStore } from '../../store/uiStore'
    77import { Button } from '../../components/ui/Button'
    8 import { Avatar } from '../../components/ui/Avatar'
    9 import { RoleBadge } from '../../components/ui/Badge'
    10 
    11 const quickUsers = [
    12   { username: 'admin_alex', name: 'Alex Admin', role: 'admin' as const },
    13   { username: 'elena_writes', name: 'Elena Dimitrova', role: 'writer' as const },
    14   { username: 'boris_writer', name: 'Boris Nikolov', role: 'writer' as const },
    15   { username: 'sara_reader', name: 'Sara Petkovska', role: 'regular' as const },
    16 ]
    178
    189export const LoginPage: React.FC = () => {
    1910  const navigate = useNavigate()
    20   const { login, switchUser } = useAuthStore()
     11  const { login } = useAuthStore()
    2112  const { addToast } = useUIStore()
    2213  const [email, setEmail] = useState('')
     
    4940  }
    5041
    51   const handleQuickLogin = (username: string) => {
    52     const { allUsers } = useAuthStore.getState()
    53     const user = allUsers.find(u => u.username === username)
    54     if (user) {
    55       switchUser(user.user_id)
    56       addToast(`Signed in as ${user.name}`)
    57       navigate('/')
    58     }
    59   }
    60 
    6142  return (
    6243    <div className="min-h-[80vh] flex items-center justify-center px-4 py-12">
     
    6950          <h1 className="font-serif text-2xl font-bold text-white">Welcome back</h1>
    7051          <p className="text-slate-400 text-sm mt-2">Sign in to your account</p>
    71         </div>
    72 
    73         {/* Quick login */}
    74         <div className="mb-6">
    75           <p className="text-xs text-slate-500 text-center mb-3">Quick demo login</p>
    76           <div className="grid grid-cols-2 gap-2">
    77             {quickUsers.map(u => (
    78               <button
    79                 key={u.username}
    80                 onClick={() => handleQuickLogin(u.username)}
    81                 className="flex items-center gap-2 p-3 bg-slate-800 border border-slate-700 rounded-xl hover:border-indigo-500/50 hover:bg-slate-700/50 transition-all group"
    82               >
    83                 <Avatar name={u.name} size="sm" />
    84                 <div className="text-left min-w-0">
    85                   <p className="text-white text-xs font-medium truncate">{u.name}</p>
    86                   <RoleBadge role={u.role} />
    87                 </div>
    88               </button>
    89             ))}
    90           </div>
    91         </div>
    92 
    93         <div className="flex items-center gap-3 mb-6">
    94           <div className="flex-1 h-px bg-slate-700" />
    95           <span className="text-slate-600 text-xs">or sign in with email</span>
    96           <div className="flex-1 h-px bg-slate-700" />
    9752        </div>
    9853
  • chapterx-frontend/src/pages/browse/BrowsePage.tsx

    rb373fea r0b502c2  
    1010  { value: 'recent', label: 'Most Recent' },
    1111  { value: 'views', label: 'Most Viewed' },
    12   { value: 'comments', label: 'Most Discussed' },
    1312]
    1413
     
    2625    let list = stories.filter(s => s.status === 'published')
    2726
    28     if (!showMatureContent && !currentUser) {
     27    if (!showMatureContent) {
    2928      list = list.filter(s => !s.mature_content)
    3029    }
     
    4746      case 'recent': return [...list].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
    4847      case 'views': return [...list].sort((a, b) => b.total_views - a.total_views)
    49       case 'comments': return [...list].sort((a, b) => b.total_comments - a.total_comments)
    50       default: return list
     48default: return list
    5149    }
    5250  }, [stories, search, selectedGenres, sort, showMatureContent, currentUser])
  • chapterx-frontend/src/pages/browse/GenrePage.tsx

    rb373fea r0b502c2  
    4949export const GenresListPage: React.FC = () => {
    5050  const navigate = useNavigate()
    51   const { genres } = useStoryStore()
     51  const { genres, stories } = useStoryStore()
     52  const publishedStories = stories.filter(s => s.status === 'published')
    5253
    5354  return (
     
    6869              <div className="absolute inset-0 flex flex-col items-center justify-center p-4">
    6970                <p className="font-serif font-semibold text-white text-center">{genre.name}</p>
    70                 <p className="text-white/60 text-xs mt-1">{genre.story_count} stories</p>
     71                <p className="text-white/60 text-xs mt-1">{publishedStories.filter(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase())).length} stories</p>
    7172              </div>
    7273              <div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors" />
  • chapterx-frontend/src/pages/profile/ProfilePage.tsx

    rb373fea r0b502c2  
    1 import React, { useState } from 'react'
     1import React, { useState, useEffect } from 'react'
    22import { useParams, useNavigate } from 'react-router-dom'
    33import { BookOpen, Heart, Users, Calendar, MessageCircle, Eye } from 'lucide-react'
    44import { useAuthStore } from '../../store/authStore'
    55import { useStoryStore } from '../../store/storyStore'
     6import { useUIStore } from '../../store/uiStore'
    67import { Avatar } from '../../components/ui/Avatar'
    78import { RoleBadge, StatusBadge } from '../../components/ui/Badge'
    89import { StoryCard } from '../../components/ui/StoryCard'
    910import { GenreBadge } from '../../components/ui/Badge'
     11import { Modal } from '../../components/ui/Modal'
     12import { Button } from '../../components/ui/Button'
    1013
    1114type Tab = 'stories' | 'about'
     
    1417  const { username } = useParams<{ username: string }>()
    1518  const navigate = useNavigate()
    16   const { allUsers, currentUser } = useAuthStore()
     19  const { allUsers, currentUser, fetchAllUsers, updateUser } = useAuthStore()
    1720  const { stories, comments } = useStoryStore()
     21  const { addToast } = useUIStore()
    1822  const [tab, setTab] = useState<Tab>('stories')
     23  const [loading, setLoading] = useState(false)
     24  const [editOpen, setEditOpen] = useState(false)
     25  const [editForm, setEditForm] = useState({ username: '', email: '', name: '', surname: '' })
     26  const [saving, setSaving] = useState(false)
     27
     28  useEffect(() => {
     29    if (allUsers.length === 0) {
     30      setLoading(true)
     31      fetchAllUsers().finally(() => setLoading(false))
     32    }
     33  }, [])
    1934
    2035  const user = allUsers.find(u => u.username === username)
     36
     37  const openEdit = () => {
     38    if (!user) return
     39    setEditForm({ username: user.username, email: user.email, name: user.name, surname: user.surname })
     40    setEditOpen(true)
     41  }
     42
     43  const handleEditSave = async () => {
     44    if (!user) return
     45    setSaving(true)
     46    try {
     47      await updateUser(user.user_id, editForm)
     48      addToast('Profile updated successfully!')
     49      setEditOpen(false)
     50      navigate(`/profile/${editForm.username}`, { replace: true })
     51    } catch (err: any) {
     52      addToast(err.response?.data?.message ?? 'Failed to update profile.', 'error')
     53    } finally {
     54      setSaving(false)
     55    }
     56  }
     57
     58  if (loading) {
     59    return (
     60      <div className="max-w-4xl mx-auto px-4 py-20 text-center">
     61        <p className="text-slate-400">Loading profile...</p>
     62      </div>
     63    )
     64  }
    2165
    2266  if (!user) {
     
    5094          <Avatar name={`${user.name} ${user.surname}`} size="xl" className="ring-4 ring-slate-950" />
    5195          {currentUser?.user_id === user.user_id && (
    52             <button className="mb-2 text-sm text-indigo-400 hover:text-indigo-300 transition-colors">
     96            <button onClick={openEdit} className="mb-2 text-sm text-indigo-400 hover:text-indigo-300 transition-colors">
    5397              Edit Profile
    5498            </button>
     
    159203        </div>
    160204      )}
     205
     206      <Modal isOpen={editOpen} onClose={() => setEditOpen(false)} title="Edit Profile">
     207        <div className="space-y-4">
     208          {[
     209            { label: 'First Name', key: 'name' },
     210            { label: 'Last Name', key: 'surname' },
     211            { label: 'Username', key: 'username' },
     212            { label: 'Email', key: 'email' },
     213          ].map(({ label, key }) => (
     214            <div key={key}>
     215              <label className="block text-sm text-slate-400 mb-1.5">{label}</label>
     216              <input
     217                type={key === 'email' ? 'email' : 'text'}
     218                value={editForm[key as keyof typeof editForm]}
     219                onChange={e => setEditForm(p => ({ ...p, [key]: e.target.value }))}
     220                className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
     221              />
     222            </div>
     223          ))}
     224          <div className="flex gap-3 pt-2">
     225            <Button variant="ghost" className="flex-1" onClick={() => setEditOpen(false)}>Cancel</Button>
     226            <Button className="flex-1" loading={saving} onClick={handleEditSave}>Save Changes</Button>
     227          </div>
     228        </div>
     229      </Modal>
    161230    </div>
    162231  )
  • chapterx-frontend/src/store/authStore.ts

    rb373fea r0b502c2  
    1818  setShowMatureContent: (show: boolean) => void
    1919  updateUserRole: (userId: number, role: UserRole) => void
     20  updateUser: (userId: number, data: { username: string; email: string; name: string; surname: string }) => Promise<void>
    2021  addUser: (user: User) => void
    2122  fetchAllUsers: () => Promise<void>
     
    6667        )
    6768        if (!user) throw new Error('User not found. Try using a quick-login option.')
    68         set({ currentUser: user, token: 'mock-token' })
     69        set({ currentUser: user, token: null })
    6970      },
    7071
     
    110111          allUsers: [...state.allUsers, newUser],
    111112          currentUser: newUser,
    112           token: 'mock-token',
     113          token: null,
    113114        }))
    114115      },
     
    120121        }
    121122        const user = get().allUsers.find(u => u.user_id === userId)
    122         if (user) set({ currentUser: user, token: 'mock-token' })
     123        if (user) set({ currentUser: user, token: null })
    123124      },
    124125
     
    133134              : state.currentUser,
    134135        })),
     136
     137      updateUser: async (userId, data) => {
     138        const { token } = get()
     139        await axios.put(`${API_BASE}/users/${userId}`, { id: userId, ...data }, {
     140          headers: token ? { Authorization: `Bearer ${token}` } : {},
     141        })
     142        set(state => ({
     143          allUsers: state.allUsers.map(u => u.user_id === userId ? { ...u, ...data } : u),
     144          currentUser: state.currentUser?.user_id === userId ? { ...state.currentUser, ...data } : state.currentUser,
     145        }))
     146      },
    135147
    136148      addUser: (user: User) =>
  • chapterx-frontend/src/store/storyStore.ts

    rb373fea r0b502c2  
    4949  try {
    5050    const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token
    51     return token ? { Authorization: `Bearer ${token}` } : {}
     51    if (!token || token === 'mock-token') return {}
     52    return { Authorization: `Bearer ${token}` }
    5253  } catch {
    5354    return {}
     
    145146        created_at: s.createdAt,
    146147        updated_at: s.updatedAt,
    147         total_likes: 0,
    148         total_comments: 0,
    149         total_chapters: 0,
    150         total_views: 0,
     148        total_likes: s.likes?.length ?? 0,
     149        total_comments: s.comments?.length ?? 0,
     150        total_chapters: s.chapters?.length ?? 0,
     151        total_views: s.chapters?.reduce((sum: number, c: any) => sum + (c.viewCount ?? 0), 0) ?? 0,
    151152        genres: (s.hasGenres ?? []).map((hg: any) => hg.genre?.name ?? hg.name).filter(Boolean),
    152153      }))
  • chapterx-frontend/src/types/index.ts

    rb373fea r0b502c2  
    120120  genre_id: number
    121121  name: string
    122   story_count: number
    123122}
    124123
Note: See TracChangeset for help on using the changeset viewer.