Changeset 73b69b2 for chapterx-frontend/src/pages
- Timestamp:
- 03/24/26 22:13:36 (3 months ago)
- Branches:
- main
- Children:
- 7fbb91c
- Parents:
- acf690c
- Location:
- chapterx-frontend/src/pages
- Files:
-
- 4 edited
-
admin/AdminGenresPage.tsx (modified) (4 diffs)
-
reading-list/CommunityListsPage.tsx (modified) (2 diffs)
-
reading-list/ReadingListPage.tsx (modified) (5 diffs)
-
story/StoryDetailPage.tsx (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
chapterx-frontend/src/pages/admin/AdminGenresPage.tsx
racf690c r73b69b2 1 import React, { useState } from 'react'1 import React, { useState, useEffect } from 'react' 2 2 import { useNavigate } from 'react-router-dom' 3 3 import { ArrowLeft, Plus, Trash2, Tag } from 'lucide-react' … … 10 10 export const AdminGenresPage: React.FC = () => { 11 11 const navigate = useNavigate() 12 const { genres, addGenre, deleteGenre } = useStoryStore()12 const { genres, fetchGenres, addGenre, deleteGenre } = useStoryStore() 13 13 const { addToast } = useUIStore() 14 14 const [addOpen, setAddOpen] = useState(false) … … 16 16 const [deleteTarget, setDeleteTarget] = useState<Genre | null>(null) 17 17 18 const handleAdd = () => { 18 useEffect(() => { fetchGenres() }, []) 19 20 const handleAdd = async () => { 19 21 if (!newName.trim()) return 20 22 if (genres.some(g => g.name.toLowerCase() === newName.trim().toLowerCase())) { … … 22 24 return 23 25 } 24 addGenre({ genre_id: Date.now(), name: newName.trim(), story_count: 0 }) 25 addToast(`Genre "${newName.trim()}" added!`) 26 setNewName('') 27 setAddOpen(false) 26 try { 27 await addGenre(newName.trim()) 28 addToast(`Genre "${newName.trim()}" added!`) 29 setNewName('') 30 setAddOpen(false) 31 } catch { 32 addToast('Failed to add genre', 'error') 33 } 28 34 } 29 35 30 const handleDelete = (genre: Genre) => { 31 if (genre.story_count > 0) { 32 addToast('Cannot delete genre with associated stories', 'error') 33 return 36 const handleDelete = async (genre: Genre) => { 37 try { 38 await deleteGenre(genre.genre_id) 39 addToast(`Genre "${genre.name}" deleted`, 'info') 40 } catch { 41 addToast('Failed to delete genre', 'error') 34 42 } 35 deleteGenre(genre.genre_id)36 addToast(`Genre "${genre.name}" deleted`, 'info')37 43 setDeleteTarget(null) 38 44 } -
chapterx-frontend/src/pages/reading-list/CommunityListsPage.tsx
racf690c r73b69b2 1 import React, { useState } from 'react'1 import React, { useState, useEffect } from 'react' 2 2 import { useNavigate } from 'react-router-dom' 3 3 import { Globe, BookOpen, User } from 'lucide-react' … … 10 10 export const CommunityListsPage: React.FC = () => { 11 11 const navigate = useNavigate() 12 const { readingLists } = useStoryStore()12 const { readingLists, fetchReadingLists } = useStoryStore() 13 13 const [selectedList, setSelectedList] = useState<ReadingList | null>(null) 14 15 useEffect(() => { fetchReadingLists() }, []) 14 16 15 17 const publicLists = readingLists.filter(l => l.is_public) -
chapterx-frontend/src/pages/reading-list/ReadingListPage.tsx
racf690c r73b69b2 12 12 const navigate = useNavigate() 13 13 const { currentUser } = useAuthStore() 14 const { readingLists, fetchReadingLists, createReadingList, deleteReadingList, removeStoryFromList } = useStoryStore() 14 const { readingLists, fetchUserReadingLists, createReadingList, deleteReadingList, removeStoryFromList } = useStoryStore() 15 const [fetching, setFetching] = useState(false) 16 const [confirmRemove, setConfirmRemove] = useState<{ listId: number; storyId: number; title: string } | null>(null) 15 17 16 18 useEffect(() => { 17 fetchReadingLists() 18 }, []) 19 if (!currentUser) return 20 setFetching(true) 21 fetchUserReadingLists(currentUser.user_id).finally(() => setFetching(false)) 22 }, [currentUser?.user_id]) 19 23 const { addToast } = useUIStore() 20 24 const [createOpen, setCreateOpen] = useState(false) … … 33 37 } 34 38 35 const myLists = readingLists.filter(l => l.user_id === currentUser .user_id)39 const myLists = readingLists.filter(l => l.user_id === currentUser!.user_id) 36 40 37 41 const handleCreate = async () => { … … 73 77 </div> 74 78 75 {myLists.length === 0 ? ( 79 {fetching ? ( 80 <div className="flex flex-col items-center py-20 text-slate-500"> 81 <BookOpen size={48} className="mb-4 opacity-40 animate-pulse" /> 82 <p className="text-sm">Loading your lists...</p> 83 </div> 84 ) : myLists.length === 0 ? ( 76 85 <div className="flex flex-col items-center py-20 text-slate-500"> 77 86 <BookOpen size={48} className="mb-4 opacity-40" /> … … 124 133 <div className="space-y-2"> 125 134 {list.stories.slice(0, 4).map(item => ( 126 <div key={item.item_id} className="flex items-center gap- 3 p-2 rounded-lg hover:bg-slate-700/50 group">127 <div className="flex-1 min-w-0 ">135 <div key={item.item_id} className="flex items-center gap-2 p-2 rounded-lg hover:bg-slate-700/50"> 136 <div className="flex-1 min-w-0 overflow-hidden"> 128 137 <button 129 138 onClick={() => navigate(`/story/${item.story_id}`)} 130 className="text-white text-sm font-medium hover:text-indigo-300 transition-colors truncate block text-left "139 className="text-white text-sm font-medium hover:text-indigo-300 transition-colors truncate block text-left w-full" 131 140 > 132 141 {item.story_title} 133 142 </button> 134 143 <div className="flex items-center gap-2 mt-0.5"> 135 <span className="text-slate-500 text-xs ">by {item.author_username}</span>144 <span className="text-slate-500 text-xs truncate">by {item.author_username}</span> 136 145 {item.genres.slice(0, 1).map(g => <GenreBadge key={g} genre={g} />)} 137 146 </div> 138 147 </div> 139 148 <button 140 onClick={() => removeStoryFromList(list.list_id, item.story_id)} 141 className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-rose-400 transition-all" 149 onClick={() => setConfirmRemove({ listId: list.list_id, storyId: item.story_id, title: item.story_title })} 150 className="flex-shrink-0 text-slate-500 hover:text-rose-400 hover:bg-rose-400/10 transition-all p-1.5 rounded-lg" 151 title="Remove from list" 142 152 > 143 < Xsize={14} />153 <Trash2 size={14} /> 144 154 </button> 145 155 </div> … … 165 175 )} 166 176 177 {/* Confirm remove modal */} 178 <Modal isOpen={!!confirmRemove} onClose={() => setConfirmRemove(null)} title="Remove Story"> 179 <div className="space-y-4"> 180 <p className="text-slate-300 text-sm"> 181 Are you sure you want to remove <span className="text-white font-medium">"{confirmRemove?.title}"</span> from this list? 182 </p> 183 <div className="flex gap-3"> 184 <Button variant="secondary" className="flex-1" onClick={() => setConfirmRemove(null)}>Cancel</Button> 185 <Button variant="danger" className="flex-1" onClick={async () => { 186 if (!confirmRemove) return 187 await removeStoryFromList(confirmRemove.listId, confirmRemove.storyId) 188 addToast('Story removed from list', 'info') 189 setConfirmRemove(null) 190 }}>Remove</Button> 191 </div> 192 </div> 193 </Modal> 194 167 195 {/* Create modal */} 168 196 <Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Reading List"> -
chapterx-frontend/src/pages/story/StoryDetailPage.tsx
racf690c r73b69b2 23 23 const [listModalOpen, setListModalOpen] = useState(false) 24 24 const [newListName, setNewListName] = useState('') 25 const [liveLikes, setLiveLikes] = useState<number | null>(null) 26 const [liveComments, setLiveComments] = useState<number | null>(null) 25 27 26 28 const story = stories.find(s => s.story_id === Number(id)) … … 153 155 {/* Action bar */} 154 156 <div className="flex items-center gap-3 flex-wrap"> 155 <LikeButton storyId={story.story_id} authorUserId={story.user_id} totalLikes={story.total_likes} />157 <LikeButton storyId={story.story_id} authorUserId={story.user_id} totalLikes={story.total_likes} onCountChange={setLiveLikes} /> 156 158 {currentUser && ( 157 159 <Button variant="secondary" size="sm" onClick={() => setListModalOpen(true)}> … … 183 185 {/* Comments */} 184 186 <div className="border-t border-slate-700 pt-8"> 185 <CommentSection storyId={story.story_id} authorUserId={story.user_id} />187 <CommentSection storyId={story.story_id} authorUserId={story.user_id} onCountChange={setLiveComments} /> 186 188 </div> 187 189 </div> … … 203 205 <div className="flex justify-between"> 204 206 <span className="text-slate-400">Likes</span> 205 <span className="text-white">{ story.total_likes.toLocaleString()}</span>207 <span className="text-white">{(liveLikes ?? story.total_likes).toLocaleString()}</span> 206 208 </div> 207 209 <div className="flex justify-between"> … … 211 213 <div className="flex justify-between"> 212 214 <span className="text-slate-400">Comments</span> 213 <span className="text-white">{ story.total_comments}</span>215 <span className="text-white">{liveComments ?? story.total_comments}</span> 214 216 </div> 215 217 <div className="flex justify-between">
Note:
See TracChangeset
for help on using the changeset viewer.
