source: chapterx-frontend/src/pages/story/ChapterReadPage.tsx@ a8f4a2d

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

Fixed views count

  • Property mode set to 100644
File size: 7.5 KB
Line 
1import React, { useEffect, useState } from 'react'
2import { useParams, useNavigate } from 'react-router-dom'
3import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react'
4import axios from 'axios'
5import { useStoryStore } from '../../store/storyStore'
6import { Chapter } from '../../types'
7import { Button } from '../../components/ui/Button'
8
9const API = 'https://localhost:7125/api'
10
11function mapChapter(c: any): Chapter {
12 return {
13 chapter_id: c.id ?? c.chapter_id,
14 story_id: c.storyId ?? c.story_id,
15 title: c.title ?? c.name ?? '',
16 content: c.content ?? '',
17 chapter_number: c.number ?? c.chapter_number ?? 0,
18 word_count: c.wordCount ?? c.word_count ?? 0,
19 view_count: c.viewCount ?? c.view_count ?? 0,
20 is_published: true,
21 created_at: c.createdAt ?? c.created_at ?? '',
22 updated_at: c.updatedAt ?? c.updated_at ?? '',
23 }
24}
25
26export const ChapterReadPage: React.FC = () => {
27 const { storyId, chapterId } = useParams<{ storyId: string; chapterId: string }>()
28 const navigate = useNavigate()
29 const { stories, chapters, incrementViewCount } = useStoryStore()
30 const [scrollProgress, setScrollProgress] = useState(0)
31 const [viewed, setViewed] = useState(false)
32 const [chapter, setChapter] = useState<Chapter | null>(null)
33 const [loading, setLoading] = useState(true)
34
35 useEffect(() => {
36 setLoading(true)
37 setViewed(false)
38 setScrollProgress(0)
39 axios.get(`${API}/chapters/${chapterId}`)
40 .then(res => {
41 const data = res.data?.chapter ?? res.data
42 setChapter(mapChapter(data))
43 })
44 .catch(() => {
45 const fallback = chapters.find(c => c.chapter_id === Number(chapterId))
46 if (fallback) setChapter(fallback)
47 })
48 .finally(() => setLoading(false))
49 }, [chapterId])
50
51 const story = stories.find(s => s.story_id === Number(storyId))
52 const storyChapters = chapters
53 .filter(c => c.story_id === Number(storyId) && c.is_published)
54 .sort((a, b) => a.chapter_number - b.chapter_number)
55
56 const currentIndex = storyChapters.findIndex(c => c.chapter_id === Number(chapterId))
57 const prevChapter = currentIndex > 0 ? storyChapters[currentIndex - 1] : null
58 const nextChapter = currentIndex < storyChapters.length - 1 ? storyChapters[currentIndex + 1] : null
59
60 // Track scroll progress
61 useEffect(() => {
62 const handler = () => {
63 const el = document.documentElement
64 const progress = (el.scrollTop / (el.scrollHeight - el.clientHeight)) * 100
65 setScrollProgress(Math.min(100, progress))
66 // Increment view count when 30% read
67 if (progress > 30 && !viewed) {
68 setViewed(true)
69 if (chapter) incrementViewCount(chapter.chapter_id)
70 }
71 }
72 window.addEventListener('scroll', handler)
73 return () => window.removeEventListener('scroll', handler)
74 }, [chapter, viewed, incrementViewCount])
75
76 if (loading) {
77 return (
78 <div className="max-w-4xl mx-auto px-4 py-20 text-center">
79 <p className="text-slate-400">Loading chapter...</p>
80 </div>
81 )
82 }
83
84 if (!story || !chapter) {
85 return (
86 <div className="max-w-4xl mx-auto px-4 py-20 text-center">
87 <h2 className="text-2xl text-white mb-4">Chapter not found</h2>
88 <Button onClick={() => navigate('/browse')}>Browse Stories</Button>
89 </div>
90 )
91 }
92
93 return (
94 <div className="min-h-screen">
95 {/* Progress bar */}
96 <div className="fixed top-0 left-0 right-0 z-50 h-1 bg-slate-800">
97 <div
98 className="h-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all duration-100"
99 style={{ width: `${scrollProgress}%` }}
100 />
101 </div>
102
103 {/* Top nav */}
104 <div className="sticky top-1 z-40 glass border-b border-white/5">
105 <div className="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
106 <button
107 onClick={() => navigate(`/story/${story.story_id}`)}
108 className="flex items-center gap-2 text-slate-400 hover:text-white text-sm transition-colors"
109 >
110 <ArrowLeft size={16} />
111 <span className="hidden sm:block">{story.title}</span>
112 </button>
113
114 <div className="text-center">
115 <p className="text-white text-sm font-medium">{chapter.title}</p>
116 <p className="text-slate-500 text-xs">Chapter {chapter.chapter_number}</p>
117 </div>
118
119 <div />
120 </div>
121 </div>
122
123 {/* Content */}
124 <div className="max-w-3xl mx-auto px-4 py-12">
125 {/* Chapter header */}
126 <div className="text-center mb-12">
127 <p className="text-indigo-400 text-sm font-medium mb-2 uppercase tracking-widest">
128 Chapter {chapter.chapter_number}
129 </p>
130 <h1 className="font-serif text-3xl sm:text-4xl font-bold text-white mb-4">
131 {chapter.title}
132 </h1>
133 <div className="mt-4 w-16 h-px bg-gradient-to-r from-transparent via-indigo-500 to-transparent mx-auto" />
134 </div>
135
136 {/* Chapter content */}
137 <div className="prose prose-lg prose-invert max-w-none">
138 {chapter.content.split('\n\n').map((para, i) => (
139 <p
140 key={i}
141 className="text-slate-300 leading-relaxed text-lg mb-6 first-letter:text-4xl first-letter:font-serif first-letter:font-bold first-letter:text-white first-letter:float-left first-letter:mr-2 first-letter:mt-1"
142 style={i !== 0 ? { textIndent: '2rem' } : undefined}
143 >
144 {para}
145 </p>
146 ))}
147 </div>
148
149 {/* Chapter navigation */}
150 <div className="mt-16 pt-8 border-t border-slate-700">
151 <div className="flex items-center justify-between gap-4">
152 {prevChapter ? (
153 <button
154 onClick={() => navigate(`/story/${storyId}/chapter/${prevChapter.chapter_id}`)}
155 className="flex items-center gap-2 px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl hover:border-indigo-500/50 transition-all group flex-1 max-w-[45%]"
156 >
157 <ChevronLeft size={18} className="text-slate-400 group-hover:text-indigo-400 flex-shrink-0" />
158 <div className="text-left min-w-0">
159 <p className="text-xs text-slate-500">Previous</p>
160 <p className="text-white text-sm truncate">{prevChapter.title}</p>
161 </div>
162 </button>
163 ) : (
164 <div className="flex-1" />
165 )}
166
167 <Button
168 variant="ghost"
169 size="sm"
170 onClick={() => navigate(`/story/${story.story_id}`)}
171 >
172 Contents
173 </Button>
174
175 {nextChapter ? (
176 <button
177 onClick={() => navigate(`/story/${storyId}/chapter/${nextChapter.chapter_id}`)}
178 className="flex items-center gap-2 px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl hover:border-indigo-500/50 transition-all group flex-1 max-w-[45%] justify-end"
179 >
180 <div className="text-right min-w-0">
181 <p className="text-xs text-slate-500">Next</p>
182 <p className="text-white text-sm truncate">{nextChapter.title}</p>
183 </div>
184 <ChevronRight size={18} className="text-slate-400 group-hover:text-indigo-400 flex-shrink-0" />
185 </button>
186 ) : (
187 <div className="flex-1 text-center">
188 <p className="text-slate-500 text-sm">End of story</p>
189 </div>
190 )}
191 </div>
192 </div>
193 </div>
194 </div>
195 )
196}
Note: See TracBrowser for help on using the repository browser.