source: chapterx-frontend/src/pages/story/StoryDetailPage.tsx@ 99c1e45

main
Last change on this file since 99c1e45 was 99c1e45, checked in by kikisrbinoska <srbinoskakristina07@…>, 11 days ago

Fixed writer section and admin management

  • Property mode set to 100644
File size: 12.7 KB
RevLine 
[b62cefc]1import React, { useState } from 'react'
2import { useParams, useNavigate } from 'react-router-dom'
[99c1e45]3import { ArrowLeft, BookOpen, Users, Calendar, Plus, BookmarkPlus } from 'lucide-react'
[b62cefc]4import { useStoryStore } from '../../store/storyStore'
5import { useAuthStore } from '../../store/authStore'
6import { useUIStore } from '../../store/uiStore'
7import { Button } from '../../components/ui/Button'
8import { GenreBadge } from '../../components/ui/Badge'
9import { Avatar } from '../../components/ui/Avatar'
10import { ChapterList } from '../../components/story/ChapterList'
11import { CommentSection } from '../../components/story/CommentSection'
12import { LikeButton } from '../../components/story/LikeButton'
13import { Modal } from '../../components/ui/Modal'
14import { getGenreGradient } from '../../components/story/GenreBadge'
15import { ReadingListItem } from '../../types'
16
17export const StoryDetailPage: React.FC = () => {
18 const { id } = useParams<{ id: string }>()
19 const navigate = useNavigate()
20 const { stories, chapters, collaborations, readingLists, addStoryToList, createReadingList } = useStoryStore()
21 const { currentUser } = useAuthStore()
22 const { addToast } = useUIStore()
23 const [listModalOpen, setListModalOpen] = useState(false)
24 const [newListName, setNewListName] = useState('')
[73b69b2]25 const [liveLikes, setLiveLikes] = useState<number | null>(null)
26 const [liveComments, setLiveComments] = useState<number | null>(null)
[b62cefc]27
28 const story = stories.find(s => s.story_id === Number(id))
29 if (!story) {
30 return (
31 <div className="max-w-4xl mx-auto px-4 py-20 text-center">
32 <h2 className="text-2xl text-white mb-4">Story not found</h2>
33 <Button onClick={() => navigate('/browse')}>Browse Stories</Button>
34 </div>
35 )
36 }
37
38 const storyChapters = chapters.filter(c => c.story_id === story.story_id)
39 const storyCollabs = collaborations.filter(c => c.story_id === story.story_id)
40 const gradient = getGenreGradient(story.genres[0])
41 const myLists = currentUser ? readingLists.filter(l => l.user_id === currentUser.user_id) : []
42
[acf690c]43 const handleAddToList = async (listId: number) => {
[b62cefc]44 const list = readingLists.find(l => l.list_id === listId)
45 if (!list) return
46 if (list.stories.some(s => s.story_id === story.story_id)) {
47 addToast('Already in this list', 'info')
48 return
49 }
50 const item: ReadingListItem = {
51 item_id: Date.now(),
52 list_id: listId,
53 story_id: story.story_id,
54 story_title: story.title,
55 author_username: story.author_username,
56 added_at: new Date().toISOString(),
57 genres: story.genres,
58 }
[acf690c]59 try {
60 await addStoryToList(listId, item)
61 addToast(`Added to "${list.name}"!`)
62 } catch (err: any) {
63 const msg = err?.response?.data?.message || ''
64 if (msg.includes('already') || msg.includes('duplicate') || err?.response?.status === 400) {
65 addToast('Already in this list', 'info')
66 } else {
67 addToast('Failed to add to list.', 'error')
68 }
69 }
[b62cefc]70 setListModalOpen(false)
71 }
72
[acf690c]73 const handleCreateList = async () => {
[b62cefc]74 if (!newListName.trim() || !currentUser) return
75 const newList = {
76 list_id: Date.now(),
77 user_id: currentUser.user_id,
78 username: currentUser.username,
79 name: newListName.trim(),
80 is_public: false,
81 created_at: new Date().toISOString(),
[acf690c]82 stories: [],
83 }
84 try {
85 const realListId = await createReadingList(newList)
86 await addStoryToList(realListId, {
[b62cefc]87 item_id: Date.now() + 1,
[acf690c]88 list_id: realListId,
[b62cefc]89 story_id: story.story_id,
90 story_title: story.title,
91 author_username: story.author_username,
92 added_at: new Date().toISOString(),
93 genres: story.genres,
[acf690c]94 })
95 addToast(`Created "${newListName}" and added story!`)
96 } catch {
97 addToast('Failed to create list.', 'error')
[b62cefc]98 }
99 setNewListName('')
100 setListModalOpen(false)
101 }
102
103 const isOwner = currentUser?.user_id === story.user_id
[7fbb91c]104 const isCollaborator = currentUser ? storyCollabs.some(c => c.user_id === currentUser.user_id) : false
[b62cefc]105
106 return (
107 <div className="max-w-5xl mx-auto px-4 py-8">
108 {/* Back */}
109 <button
110 onClick={() => navigate(-1)}
111 className="flex items-center gap-2 text-slate-400 hover:text-white mb-6 text-sm transition-colors"
112 >
113 <ArrowLeft size={16} />
114 Back
115 </button>
116
117 {/* Hero */}
118 <div className={`relative rounded-2xl overflow-hidden mb-8 bg-gradient-to-br ${gradient}`}>
[99c1e45]119 {story.cover_image && (
120 <img src={story.cover_image} alt={story.title} className="absolute inset-0 w-full h-full object-cover opacity-30" />
121 )}
[b62cefc]122 <div className="absolute inset-0 bg-gradient-to-t from-slate-950/90 via-slate-950/30 to-transparent" />
123 <div className="relative p-8 sm:p-12">
124 <div className="flex flex-wrap gap-2 mb-4">
125 {story.genres.map(g => <GenreBadge key={g} genre={g} />)}
126 {story.mature_content && (
127 <span className="px-2 py-0.5 text-xs font-medium rounded-full border bg-rose-500/20 text-rose-400 border-rose-500/30">
128 18+
129 </span>
130 )}
131 </div>
132 <h1 className="font-serif text-3xl sm:text-4xl font-bold text-white mb-3">{story.title}</h1>
133 <p className="text-slate-300 text-lg mb-6 leading-relaxed max-w-2xl">{story.short_description}</p>
134
135 <div className="flex items-center gap-4 flex-wrap">
136 <div className="flex items-center gap-2">
137 <Avatar name={story.author_username} size="sm" />
138 <span className="text-white text-sm font-medium">{story.author_username}</span>
139 </div>
140 <div className="flex items-center gap-1 text-slate-400 text-sm">
141 <BookOpen size={14} />
142 {story.total_chapters} chapters
143 </div>
144 <div className="flex items-center gap-1 text-slate-400 text-sm">
145 <Calendar size={14} />
146 {new Date(story.created_at).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
147 </div>
148 </div>
149 </div>
150 </div>
151
152 <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
153 {/* Main content */}
154 <div className="lg:col-span-2 space-y-8">
155 {/* Action bar */}
156 <div className="flex items-center gap-3 flex-wrap">
[73b69b2]157 <LikeButton storyId={story.story_id} authorUserId={story.user_id} totalLikes={story.total_likes} onCountChange={setLiveLikes} />
[b62cefc]158 {currentUser && (
159 <Button variant="secondary" size="sm" onClick={() => setListModalOpen(true)}>
160 <BookmarkPlus size={14} />
161 Save to List
162 </Button>
163 )}
[7fbb91c]164 {(isOwner || isCollaborator) && (
[b62cefc]165 <Button variant="ghost" size="sm" onClick={() => navigate(`/writer/edit-story/${story.story_id}`)}>
166 Edit Story
167 </Button>
168 )}
169 </div>
170
[99c1e45]171 {/* Story content */}
172 {story.content && (
173 <div className="bg-slate-800/50 border border-slate-700 rounded-2xl p-6">
174 <p className="text-slate-200 leading-relaxed font-serif whitespace-pre-wrap">{story.content}</p>
175 </div>
176 )}
177
[b62cefc]178 {/* Chapters */}
179 <div>
180 <div className="flex items-center justify-between mb-4">
181 <h2 className="font-serif text-xl font-bold text-white">Chapters</h2>
[7fbb91c]182 {(isOwner || isCollaborator) && (
[b62cefc]183 <Button size="sm" onClick={() => navigate(`/writer/create-chapter/${story.story_id}`)}>
184 <Plus size={14} />
185 Add Chapter
186 </Button>
187 )}
188 </div>
189 <ChapterList chapters={storyChapters} storyId={story.story_id} />
190 </div>
191
192 {/* Comments */}
193 <div className="border-t border-slate-700 pt-8">
[73b69b2]194 <CommentSection storyId={story.story_id} authorUserId={story.user_id} onCountChange={setLiveComments} />
[b62cefc]195 </div>
196 </div>
197
198 {/* Sidebar */}
199 <div className="space-y-6">
200 {/* Story info */}
201 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-5">
202 <h3 className="text-white font-semibold mb-4">Story Info</h3>
203 <div className="space-y-3 text-sm">
204 <div className="flex justify-between">
205 <span className="text-slate-400">Status</span>
206 <span className="text-white capitalize">{story.status}</span>
207 </div>
208 <div className="flex justify-between">
209 <span className="text-slate-400">Chapters</span>
210 <span className="text-white">{story.total_chapters}</span>
211 </div>
212 <div className="flex justify-between">
213 <span className="text-slate-400">Likes</span>
[73b69b2]214 <span className="text-white">{(liveLikes ?? story.total_likes).toLocaleString()}</span>
[b62cefc]215 </div>
216 <div className="flex justify-between">
217 <span className="text-slate-400">Comments</span>
[73b69b2]218 <span className="text-white">{liveComments ?? story.total_comments}</span>
[b62cefc]219 </div>
220 <div className="flex justify-between">
221 <span className="text-slate-400">Published</span>
222 <span className="text-white">{new Date(story.created_at).toLocaleDateString()}</span>
223 </div>
224 </div>
225 </div>
226
227 {/* Collaborators */}
228 {storyCollabs.length > 0 && (
229 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-5">
230 <div className="flex items-center gap-2 mb-4">
231 <Users size={16} className="text-violet-400" />
232 <h3 className="text-white font-semibold">Collaborators</h3>
233 </div>
234 <div className="space-y-3">
235 {storyCollabs.map(c => (
236 <div key={c.collab_id} className="flex items-center gap-2">
237 <Avatar name={c.name} size="sm" />
238 <div>
239 <p className="text-white text-sm">@{c.username}</p>
240 <p className="text-slate-500 text-xs">{c.role}</p>
241 </div>
242 </div>
243 ))}
244 </div>
245 </div>
246 )}
247 </div>
248 </div>
249
250 {/* Reading list modal */}
251 <Modal isOpen={listModalOpen} onClose={() => setListModalOpen(false)} title="Save to Reading List">
252 <div className="space-y-3">
253 {myLists.length > 0 ? (
254 <>
255 <p className="text-slate-400 text-sm mb-3">Select a list:</p>
256 {myLists.map(list => (
257 <button
258 key={list.list_id}
259 onClick={() => handleAddToList(list.list_id)}
260 className="w-full flex items-center justify-between p-3 bg-slate-800 rounded-xl border border-slate-700 hover:border-indigo-500/50 transition-colors"
261 >
262 <span className="text-white text-sm">{list.name}</span>
263 <span className="text-slate-500 text-xs">{list.stories.length} stories</span>
264 </button>
265 ))}
266 <div className="border-t border-slate-700 pt-3">
267 <p className="text-slate-400 text-sm mb-2">Or create a new list:</p>
268 <div className="flex gap-2">
269 <input
270 value={newListName}
271 onChange={e => setNewListName(e.target.value)}
272 placeholder="List name..."
273 className="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm placeholder-slate-500 focus:outline-none focus:border-indigo-500"
274 />
275 <Button size="sm" onClick={handleCreateList} disabled={!newListName.trim()}>
276 Create
277 </Button>
278 </div>
279 </div>
280 </>
281 ) : (
282 <div>
283 <p className="text-slate-400 text-sm mb-3">You don't have any reading lists yet. Create one:</p>
284 <div className="flex gap-2">
285 <input
286 value={newListName}
287 onChange={e => setNewListName(e.target.value)}
288 placeholder="My Favorites..."
289 className="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm placeholder-slate-500 focus:outline-none focus:border-indigo-500"
290 />
291 <Button size="sm" onClick={handleCreateList} disabled={!newListName.trim()}>
292 Create
293 </Button>
294 </div>
295 </div>
296 )}
297 </div>
298 </Modal>
299 </div>
300 )
301}
Note: See TracBrowser for help on using the repository browser.