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

main
Last change on this file since acf690c was acf690c, checked in by kikisrbinoska <srbinoskakristina07@…>, 4 months ago

Added fixes for the login,stories management and reading lists

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