Changeset 0b502c2
- Timestamp:
- 06/23/26 17:20:47 (12 days ago)
- Branches:
- main
- Children:
- 99c1e45
- Parents:
- b373fea
- Files:
-
- 19 edited
-
ChapterX.API/Controllers/ChaptersController.cs (modified) (3 diffs)
-
ChapterX.API/Controllers/CommentsController.cs (modified) (4 diffs)
-
ChapterX.API/Controllers/StoriesController.cs (modified) (4 diffs)
-
ChapterX.API/Controllers/UsersController.cs (modified) (3 diffs)
-
ChapterX.API/Program.cs (modified) (2 diffs)
-
ChapterX.Infrastructure/Data/DataContext/ApplicationDbContext.cs (modified) (1 diff)
-
ChapterX.Infrastructure/Repositories/StoryRepository.cs (modified) (1 diff)
-
chapterx-frontend/src/App.tsx (modified) (2 diffs)
-
chapterx-frontend/src/components/layout/Footer.tsx (modified) (1 diff)
-
chapterx-frontend/src/data/mockData.ts (modified) (1 diff)
-
chapterx-frontend/src/pages/LandingPage.tsx (modified) (2 diffs)
-
chapterx-frontend/src/pages/admin/AdminGenresPage.tsx (modified) (2 diffs)
-
chapterx-frontend/src/pages/auth/LoginPage.tsx (modified) (3 diffs)
-
chapterx-frontend/src/pages/browse/BrowsePage.tsx (modified) (3 diffs)
-
chapterx-frontend/src/pages/browse/GenrePage.tsx (modified) (2 diffs)
-
chapterx-frontend/src/pages/profile/ProfilePage.tsx (modified) (4 diffs)
-
chapterx-frontend/src/store/authStore.ts (modified) (5 diffs)
-
chapterx-frontend/src/store/storyStore.ts (modified) (2 diffs)
-
chapterx-frontend/src/types/index.ts (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
ChapterX.API/Controllers/ChaptersController.cs
rb373fea r0b502c2 5 5 using Microsoft.AspNetCore.Mvc; 6 6 using Microsoft.Extensions.Logging; 7 using System.IdentityModel.Tokens.Jwt;8 7 using System.Security.Claims; 9 8 … … 60 59 } 61 60 62 var callerId = int.Parse(User.FindFirstValue( JwtRegisteredClaimNames.Sub)!);61 var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); 63 62 var response = await _mediator.Send(request with { CallerId = callerId }); 64 63 return Ok(response); … … 70 69 { 71 70 _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)!); 73 72 var response = await _mediator.Send(new DeleteRequest(id, callerId)); 74 73 return Ok(response); -
ChapterX.API/Controllers/CommentsController.cs
rb373fea r0b502c2 6 6 using Microsoft.AspNetCore.Mvc; 7 7 using Microsoft.Extensions.Logging; 8 using System.IdentityModel.Tokens.Jwt;9 8 using System.Security.Claims; 10 9 … … 65 64 public async Task<ActionResult> Add([FromBody] AddRequest request) 66 65 { 67 var callerId = int.Parse(User.FindFirstValue( JwtRegisteredClaimNames.Sub)!);66 var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); 68 67 _logger.LogInformation("Adding a new comment"); 69 68 var response = await _mediator.Send(request with { UserId = callerId }); … … 81 80 } 82 81 83 var callerId = int.Parse(User.FindFirstValue( JwtRegisteredClaimNames.Sub)!);82 var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); 84 83 var response = await _mediator.Send(request with { CallerId = callerId }); 85 84 return Ok(response); … … 91 90 { 92 91 _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)!); 94 93 var response = await _mediator.Send(new DeleteRequest(id, callerId)); 95 94 return Ok(response); -
ChapterX.API/Controllers/StoriesController.cs
rb373fea r0b502c2 5 5 using Microsoft.AspNetCore.Mvc; 6 6 using Microsoft.Extensions.Logging; 7 using System.IdentityModel.Tokens.Jwt;8 7 using System.Security.Claims; 9 8 … … 48 47 public async Task<ActionResult> Add([FromBody] AddRequest request) 49 48 { 50 var callerId = int.Parse(User.FindFirstValue( JwtRegisteredClaimNames.Sub)!);49 var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); 51 50 _logger.LogInformation("Adding a new story for UserId: {UserId}", callerId); 52 51 var response = await _mediator.Send(request with { UserId = callerId }); … … 65 64 } 66 65 67 var callerId = int.Parse(User.FindFirstValue( JwtRegisteredClaimNames.Sub)!);66 var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); 68 67 var response = await _mediator.Send(request with { CallerId = callerId }); 69 68 return Ok(response); … … 76 75 { 77 76 _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)!); 79 78 var response = await _mediator.Send(new DeleteRequest(id, callerId)); 80 79 return Ok(response); -
ChapterX.API/Controllers/UsersController.cs
rb373fea r0b502c2 5 5 using Microsoft.AspNetCore.Mvc; 6 6 using Microsoft.Extensions.Logging; 7 using System.IdentityModel.Tokens.Jwt;8 7 using System.Security.Claims; 9 8 … … 69 68 } 70 69 71 var callerId = int.Parse(User.FindFirstValue( JwtRegisteredClaimNames.Sub)!);70 var callerId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); 72 71 var isAdmin = User.IsInRole("Admin"); 73 72 if (callerId != id && !isAdmin) … … 83 82 { 84 83 _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)!); 86 85 var isAdmin = User.IsInRole("Admin"); 87 86 if (callerId != id && !isAdmin) -
ChapterX.API/Program.cs
rb373fea r0b502c2 9 9 10 10 var jwtKey = builder.Configuration["Jwt:Key"]; 11 if (string.IsNullOrWhiteSpace(jwtKey) || jwtKey.StartsWith("change-this"))11 if (string.IsNullOrWhiteSpace(jwtKey)) 12 12 throw new InvalidOperationException("Jwt:Key is not configured. Set it via environment variable DOTNET_Jwt__Key before starting the application."); 13 13 … … 17 17 policy.WithOrigins("http://localhost:5173", "https://localhost:5173") 18 18 .AllowAnyHeader() 19 .AllowAnyMethod()); 19 .AllowAnyMethod() 20 .AllowCredentials()); 20 21 }); 21 22 -
ChapterX.Infrastructure/Data/DataContext/ApplicationDbContext.cs
rb373fea r0b502c2 187 187 e.Property(x => x.IsPublic).HasColumnName("is_public"); 188 188 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"); 191 191 e.HasOne(x => x.User).WithMany(u => u.ReadingLists).HasForeignKey(x => x.UserId); 192 192 }); -
ChapterX.Infrastructure/Repositories/StoryRepository.cs
rb373fea r0b502c2 19 19 .Include(s => s.Writer) 20 20 .ThenInclude(w => w!.User) 21 .Include(s => s.Likes) 22 .Include(s => s.Comments) 23 .Include(s => s.Chapters) 21 24 .ToListAsync(cancellationToken); 22 25 } -
chapterx-frontend/src/App.tsx
rb373fea r0b502c2 8 8 import { Footer } from './components/layout/Footer' 9 9 import { ToastContainer } from './components/ui/Toast' 10 import { DevSwitcher } from './components/DevSwitcher'11 10 import { Spinner } from './components/ui/Spinner' 12 11 … … 188 187 <Footer /> 189 188 <ToastContainer /> 190 <DevSwitcher />191 189 </div> 192 190 ) -
chapterx-frontend/src/components/layout/Footer.tsx
rb373fea r0b502c2 14 14 <span className="font-serif font-bold text-white">ChapterX</span> 15 15 </Link> 16 <p className="text-slate-500 text-sm">17 A collaborative storytelling platform for writers and readers.18 </p>19 16 </div> 20 17 <div> -
chapterx-frontend/src/data/mockData.ts
rb373fea r0b502c2 406 406 407 407 export 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' }, 418 418 ] 419 419 -
chapterx-frontend/src/pages/LandingPage.tsx
rb373fea r0b502c2 1 1 import React from 'react' 2 2 import { useNavigate } from 'react-router-dom' 3 import { Feather, Star,BookOpen, Users, Sparkles, ArrowRight, ChevronRight } from 'lucide-react'3 import { Feather, BookOpen, Users, Sparkles, ArrowRight, ChevronRight } from 'lucide-react' 4 4 import logo from '../assets/chapterX-removebg-preview.png' 5 5 import { useStoryStore } from '../store/storyStore' … … 34 34 35 35 <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 platform39 </div>40 41 36 <div className="flex justify-center mb-6"> 42 37 <img src={logo} alt="ChapterX" className="h-32 w-32 object-contain" /> -
chapterx-frontend/src/pages/admin/AdminGenresPage.tsx
rb373fea r0b502c2 10 10 export const AdminGenresPage: React.FC = () => { 11 11 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') 13 14 const { addToast } = useUIStore() 14 15 const [addOpen, setAddOpen] = useState(false) … … 75 76 </div> 76 77 <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> 78 79 <button 79 80 onClick={() => setDeleteTarget(genre)} 80 81 className={`transition-colors p-1 rounded ${ 81 genre.story_count > 082 publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase())) 82 83 ? 'text-slate-700 cursor-not-allowed' 83 84 : 'text-slate-500 hover:text-rose-400 hover:bg-rose-500/10' 84 85 }`} 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'} 87 88 > 88 89 <Trash2 size={14} /> -
chapterx-frontend/src/pages/auth/LoginPage.tsx
rb373fea r0b502c2 6 6 import { useUIStore } from '../../store/uiStore' 7 7 import { 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 ]17 8 18 9 export const LoginPage: React.FC = () => { 19 10 const navigate = useNavigate() 20 const { login , switchUser} = useAuthStore()11 const { login } = useAuthStore() 21 12 const { addToast } = useUIStore() 22 13 const [email, setEmail] = useState('') … … 49 40 } 50 41 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 61 42 return ( 62 43 <div className="min-h-[80vh] flex items-center justify-center px-4 py-12"> … … 69 50 <h1 className="font-serif text-2xl font-bold text-white">Welcome back</h1> 70 51 <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 <button79 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" />97 52 </div> 98 53 -
chapterx-frontend/src/pages/browse/BrowsePage.tsx
rb373fea r0b502c2 10 10 { value: 'recent', label: 'Most Recent' }, 11 11 { value: 'views', label: 'Most Viewed' }, 12 { value: 'comments', label: 'Most Discussed' },13 12 ] 14 13 … … 26 25 let list = stories.filter(s => s.status === 'published') 27 26 28 if (!showMatureContent && !currentUser) {27 if (!showMatureContent) { 29 28 list = list.filter(s => !s.mature_content) 30 29 } … … 47 46 case 'recent': return [...list].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) 48 47 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 48 default: return list 51 49 } 52 50 }, [stories, search, selectedGenres, sort, showMatureContent, currentUser]) -
chapterx-frontend/src/pages/browse/GenrePage.tsx
rb373fea r0b502c2 49 49 export const GenresListPage: React.FC = () => { 50 50 const navigate = useNavigate() 51 const { genres } = useStoryStore() 51 const { genres, stories } = useStoryStore() 52 const publishedStories = stories.filter(s => s.status === 'published') 52 53 53 54 return ( … … 68 69 <div className="absolute inset-0 flex flex-col items-center justify-center p-4"> 69 70 <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> 71 72 </div> 72 73 <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'1 import React, { useState, useEffect } from 'react' 2 2 import { useParams, useNavigate } from 'react-router-dom' 3 3 import { BookOpen, Heart, Users, Calendar, MessageCircle, Eye } from 'lucide-react' 4 4 import { useAuthStore } from '../../store/authStore' 5 5 import { useStoryStore } from '../../store/storyStore' 6 import { useUIStore } from '../../store/uiStore' 6 7 import { Avatar } from '../../components/ui/Avatar' 7 8 import { RoleBadge, StatusBadge } from '../../components/ui/Badge' 8 9 import { StoryCard } from '../../components/ui/StoryCard' 9 10 import { GenreBadge } from '../../components/ui/Badge' 11 import { Modal } from '../../components/ui/Modal' 12 import { Button } from '../../components/ui/Button' 10 13 11 14 type Tab = 'stories' | 'about' … … 14 17 const { username } = useParams<{ username: string }>() 15 18 const navigate = useNavigate() 16 const { allUsers, currentUser } = useAuthStore()19 const { allUsers, currentUser, fetchAllUsers, updateUser } = useAuthStore() 17 20 const { stories, comments } = useStoryStore() 21 const { addToast } = useUIStore() 18 22 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 }, []) 19 34 20 35 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 } 21 65 22 66 if (!user) { … … 50 94 <Avatar name={`${user.name} ${user.surname}`} size="xl" className="ring-4 ring-slate-950" /> 51 95 {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"> 53 97 Edit Profile 54 98 </button> … … 159 203 </div> 160 204 )} 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> 161 230 </div> 162 231 ) -
chapterx-frontend/src/store/authStore.ts
rb373fea r0b502c2 18 18 setShowMatureContent: (show: boolean) => void 19 19 updateUserRole: (userId: number, role: UserRole) => void 20 updateUser: (userId: number, data: { username: string; email: string; name: string; surname: string }) => Promise<void> 20 21 addUser: (user: User) => void 21 22 fetchAllUsers: () => Promise<void> … … 66 67 ) 67 68 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 }) 69 70 }, 70 71 … … 110 111 allUsers: [...state.allUsers, newUser], 111 112 currentUser: newUser, 112 token: 'mock-token',113 token: null, 113 114 })) 114 115 }, … … 120 121 } 121 122 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 }) 123 124 }, 124 125 … … 133 134 : state.currentUser, 134 135 })), 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 }, 135 147 136 148 addUser: (user: User) => -
chapterx-frontend/src/store/storyStore.ts
rb373fea r0b502c2 49 49 try { 50 50 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}` } 52 53 } catch { 53 54 return {} … … 145 146 created_at: s.createdAt, 146 147 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, 151 152 genres: (s.hasGenres ?? []).map((hg: any) => hg.genre?.name ?? hg.name).filter(Boolean), 152 153 })) -
chapterx-frontend/src/types/index.ts
rb373fea r0b502c2 120 120 genre_id: number 121 121 name: string 122 story_count: number123 122 } 124 123
Note:
See TracChangeset
for help on using the changeset viewer.
