Changeset a8f4a2d
- Timestamp:
- 06/24/26 16:51:52 (11 days ago)
- Branches:
- main
- Children:
- 3ae4bab
- Parents:
- 99c1e45
- Location:
- chapterx-frontend/src
- Files:
-
- 6 edited
-
components/admin/PlatformStats.tsx (modified) (4 diffs)
-
components/writer/StoryAnalytics.tsx (modified) (4 diffs)
-
pages/profile/ProfilePage.tsx (modified) (3 diffs)
-
pages/story/ChapterReadPage.tsx (modified) (5 diffs)
-
pages/story/StoryDetailPage.tsx (modified) (1 diff)
-
pages/writer/WriterDashboard.tsx (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
chapterx-frontend/src/components/admin/PlatformStats.tsx
r99c1e45 ra8f4a2d 1 1 import React, { useEffect } from 'react' 2 import { Users, BookOpen, Heart, MessageCircle } from 'lucide-react'2 import { Users, BookOpen, Heart, MessageCircle, Eye } from 'lucide-react' 3 3 import { useAuthStore } from '../../store/authStore' 4 4 import { useStoryStore } from '../../store/storyStore' … … 12 12 const totalLikes = stories.reduce((acc, s) => acc + s.total_likes, 0) 13 13 const totalComments = stories.reduce((acc, s) => acc + s.total_comments, 0) 14 const totalViews = stories.reduce((acc, s) => acc + s.total_views, 0) 14 15 const published = stories.filter(s => s.status === 'published').length 15 16 … … 17 18 { icon: <Users size={24} className="text-blue-300" />, label: 'Total Users', value: allUsers.length, color: 'bg-blue-500/20', change: `${allUsers.filter(u => u.role === 'writer').length} writers` }, 18 19 { icon: <BookOpen size={24} className="text-violet-300" />, label: 'Total Stories', value: stories.length, color: 'bg-violet-500/20', change: `${published} published` }, 20 { icon: <Eye size={24} className="text-cyan-300" />, label: 'Total Views', value: totalViews.toLocaleString(), color: 'bg-cyan-500/20', change: 'Chapter reads' }, 19 21 { icon: <Heart size={24} className="text-rose-300" />, label: 'Total Likes', value: totalLikes.toLocaleString(), color: 'bg-rose-500/20', change: 'Across all stories' }, 20 22 { icon: <MessageCircle size={24} className="text-emerald-300" />, label: 'Total Comments', value: totalComments.toLocaleString(), color: 'bg-emerald-500/20', change: 'Platform-wide' }, … … 22 24 23 25 return ( 24 <div className="grid grid-cols-2 md:grid-cols- 4gap-4">26 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"> 25 27 {stats.map(stat => ( 26 28 <div key={stat.label} className="bg-slate-800 border border-slate-700 rounded-2xl p-6"> -
chapterx-frontend/src/components/writer/StoryAnalytics.tsx
r99c1e45 ra8f4a2d 9 9 ResponsiveContainer, 10 10 } from 'recharts' 11 import { Heart, MessageCircle, BookOpen } from 'lucide-react'11 import { Heart, MessageCircle, BookOpen, Eye } from 'lucide-react' 12 12 import { Story } from '../../types' 13 13 … … 50 50 const totalComments = stories.reduce((a, s) => a + s.total_comments, 0) 51 51 const totalChapters = stories.reduce((a, s) => a + s.total_chapters, 0) 52 const totalViews = stories.reduce((a, s) => a + s.total_views, 0) 52 53 53 // Likes per story (sorted by created_at) 54 const likesData = [...published] 55 .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) 56 .map(s => ({ 57 date: new Date(s.created_at).toLocaleDateString('en-US', { month: 'short', year: '2-digit' }), 58 likes: s.total_likes, 59 story: s.title, 60 })) 54 const sortedPublished = [...published].sort( 55 (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() 56 ) 57 58 const likesData = sortedPublished.map(s => ({ 59 date: new Date(s.created_at).toLocaleDateString('en-US', { month: 'short', year: '2-digit' }), 60 likes: s.total_likes, 61 story: s.title, 62 })) 63 64 const viewsData = sortedPublished.map(s => ({ 65 date: new Date(s.created_at).toLocaleDateString('en-US', { month: 'short', year: '2-digit' }), 66 views: s.total_views, 67 story: s.title, 68 })) 61 69 62 70 if (published.length === 0) { … … 71 79 <div className="space-y-6"> 72 80 {/* Stat cards */} 73 <div className="grid grid-cols-2 lg:grid-cols-3 gap-4"> 81 <div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> 82 <StatCard icon={<Eye size={18} className="text-cyan-300" />} label="Total Views" value={totalViews} color="bg-cyan-500/20" /> 74 83 <StatCard icon={<Heart size={18} className="text-rose-300" />} label="Total Likes" value={totalLikes} color="bg-rose-500/20" /> 75 84 <StatCard icon={<MessageCircle size={18} className="text-violet-300" />} label="Total Comments" value={totalComments} color="bg-violet-500/20" /> 76 85 <StatCard icon={<BookOpen size={18} className="text-emerald-300" />} label="Total Chapters" value={totalChapters} color="bg-emerald-500/20" /> 86 </div> 87 88 {/* Views chart */} 89 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-6"> 90 <div className="flex items-center gap-2 mb-4"> 91 <Eye size={16} className="text-cyan-400" /> 92 <h3 className="text-white font-semibold">Views per Story</h3> 93 </div> 94 {viewsData.length > 0 ? ( 95 <ResponsiveContainer width="100%" height={200}> 96 <BarChart data={viewsData}> 97 <CartesianGrid strokeDasharray="3 3" stroke="#334155" /> 98 <XAxis dataKey="date" tick={{ fill: '#64748b', fontSize: 11 }} tickLine={false} axisLine={false} /> 99 <YAxis tick={{ fill: '#64748b', fontSize: 11 }} tickLine={false} axisLine={false} /> 100 <Tooltip content={<CustomTooltip />} /> 101 <Bar dataKey="views" fill="#06b6d4" radius={[4, 4, 0, 0]} name="Views" /> 102 </BarChart> 103 </ResponsiveContainer> 104 ) : ( 105 <p className="text-slate-500 text-sm text-center py-8">No views yet</p> 106 )} 77 107 </div> 78 108 … … 109 139 <span className="text-slate-300 truncate max-w-xs">{s.title}</span> 110 140 <div className="flex items-center gap-4 text-slate-400 flex-shrink-0"> 141 <span className="flex items-center gap-1"><Eye size={12} /> {s.total_views.toLocaleString()}</span> 111 142 <span className="flex items-center gap-1"><Heart size={12} /> {s.total_likes}</span> 112 143 <span className="flex items-center gap-1"><MessageCircle size={12} /> {s.total_comments}</span> -
chapterx-frontend/src/pages/profile/ProfilePage.tsx
r99c1e45 ra8f4a2d 1 1 import React, { useState, useEffect } from 'react' 2 2 import { useParams, useNavigate } from 'react-router-dom' 3 import { BookOpen, Heart, Calendar, MessageCircle } from 'lucide-react'3 import { BookOpen, Heart, Calendar, MessageCircle, Eye } from 'lucide-react' 4 4 import { useAuthStore } from '../../store/authStore' 5 5 import { useStoryStore } from '../../store/storyStore' … … 76 76 const totalLikes = userStories.reduce((acc, s) => acc + s.total_likes, 0) 77 77 const totalComments = userStories.reduce((acc, s) => acc + s.total_comments, 0) 78 const totalViews = userStories.reduce((acc, s) => acc + s.total_views, 0) 78 79 const allGenres = [...new Set(userStories.flatMap(s => s.genres))] 79 80 … … 120 121 121 122 {/* Stats */} 122 <div className="grid grid-cols- 3gap-4 mb-8">123 <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8"> 123 124 {[ 124 125 { icon: <BookOpen size={16} className="text-indigo-400" />, value: userStories.length, label: 'Stories' }, 125 126 { icon: <Heart size={16} className="text-rose-400" />, value: totalLikes.toLocaleString(), label: 'Likes' }, 126 127 { icon: <MessageCircle size={16} className="text-amber-400" />, value: totalComments, label: 'Comments' }, 128 { icon: <Eye size={16} className="text-cyan-400" />, value: totalViews.toLocaleString(), label: 'Views' }, 127 129 ].map(s => ( 128 130 <div key={s.label} className="bg-slate-800 border border-slate-700 rounded-xl p-4 text-center"> -
chapterx-frontend/src/pages/story/ChapterReadPage.tsx
r99c1e45 ra8f4a2d 1 1 import React, { useEffect, useState } from 'react' 2 2 import { useParams, useNavigate } from 'react-router-dom' 3 import { ArrowLeft, ArrowRight, BookOpen, Eye, ChevronLeft, ChevronRight } from 'lucide-react' 3 import { ArrowLeft, ChevronLeft, ChevronRight } from 'lucide-react' 4 import axios from 'axios' 4 5 import { useStoryStore } from '../../store/storyStore' 6 import { Chapter } from '../../types' 5 7 import { Button } from '../../components/ui/Button' 8 9 const API = 'https://localhost:7125/api' 10 11 function 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 } 6 25 7 26 export const ChapterReadPage: React.FC = () => { … … 11 30 const [scrollProgress, setScrollProgress] = useState(0) 12 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]) 13 50 14 51 const story = stories.find(s => s.story_id === Number(storyId)) 15 const chapter = chapters.find(c => c.chapter_id === Number(chapterId))16 52 const storyChapters = chapters 17 53 .filter(c => c.story_id === Number(storyId) && c.is_published) … … 37 73 return () => window.removeEventListener('scroll', handler) 38 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 } 39 83 40 84 if (!story || !chapter) { … … 73 117 </div> 74 118 75 <div className="flex items-center gap-1 text-slate-400 text-xs"> 76 <Eye size={12} /> 77 {scrollProgress.toFixed(0)}% 78 </div> 119 <div /> 79 120 </div> 80 121 </div> … … 90 131 {chapter.title} 91 132 </h1> 92 <div className="flex items-center justify-center gap-4 text-slate-500 text-sm">93 <span className="flex items-center gap-1">94 <BookOpen size={13} />95 {chapter.word_count.toLocaleString()} words96 </span>97 <span className="flex items-center gap-1">98 <Eye size={13} />99 {chapter.view_count.toLocaleString()} reads100 </span>101 </div>102 133 <div className="mt-4 w-16 h-px bg-gradient-to-r from-transparent via-indigo-500 to-transparent mx-auto" /> 103 134 </div> -
chapterx-frontend/src/pages/story/StoryDetailPage.tsx
r99c1e45 ra8f4a2d 217 217 <span className="text-slate-400">Comments</span> 218 218 <span className="text-white">{liveComments ?? story.total_comments}</span> 219 </div> 220 <div className="flex justify-between"> 221 <span className="text-slate-400">Views</span> 222 <span className="text-white">{story.total_views.toLocaleString()}</span> 219 223 </div> 220 224 <div className="flex justify-between"> -
chapterx-frontend/src/pages/writer/WriterDashboard.tsx
r99c1e45 ra8f4a2d 1 1 import React, { useEffect } from 'react' 2 2 import { useNavigate } from 'react-router-dom' 3 import { Plus, BookOpen, Heart, MessageCircle, TrendingUp, Bell } from 'lucide-react'3 import { Plus, BookOpen, Heart, MessageCircle, TrendingUp, Bell, Eye } from 'lucide-react' 4 4 import { useAuthStore } from '../../store/authStore' 5 5 import { useStoryStore } from '../../store/storyStore' … … 37 37 const drafts = myStories.filter(s => s.status === 'draft') 38 38 const totalLikes = myStories.reduce((acc, s) => acc + s.total_likes, 0) 39 const totalViews = myStories.reduce((acc, s) => acc + s.total_views, 0) 39 40 40 41 const recentNotifs = notifications.slice(0, 5) … … 57 58 58 59 {/* Stats */} 59 <div className="grid grid-cols-2 lg:grid-cols- 3gap-4 mb-8">60 <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> 60 61 {[ 61 62 { icon: <BookOpen size={20} className="text-indigo-300" />, label: 'Total Stories', value: myStories.length, sub: `${published.length} published`, color: 'bg-indigo-500/20' }, 62 63 { icon: <Heart size={20} className="text-rose-300" />, label: 'Total Likes', value: totalLikes.toLocaleString(), sub: 'Across all stories', color: 'bg-rose-500/20' }, 64 { icon: <Eye size={20} className="text-cyan-300" />, label: 'Total Views', value: totalViews.toLocaleString(), sub: 'Chapter reads', color: 'bg-cyan-500/20' }, 63 65 { icon: <TrendingUp size={20} className="text-emerald-300" />, label: 'Drafts', value: drafts.length, sub: 'In progress', color: 'bg-emerald-500/20' }, 64 66 ].map(s => ( … … 110 112 <span className="flex items-center gap-1"><Heart size={11} /> {story.total_likes}</span> 111 113 <span className="flex items-center gap-1"><MessageCircle size={11} /> {story.total_comments}</span> 114 <span className="flex items-center gap-1"><Eye size={11} /> {story.total_views.toLocaleString()}</span> 112 115 <span>{story.total_chapters} chapters</span> 113 116 </div>
Note:
See TracChangeset
for help on using the changeset viewer.
