| 1 | import React from 'react'
|
|---|
| 2 | import {
|
|---|
| 3 | BarChart,
|
|---|
| 4 | Bar,
|
|---|
| 5 | XAxis,
|
|---|
| 6 | YAxis,
|
|---|
| 7 | CartesianGrid,
|
|---|
| 8 | Tooltip,
|
|---|
| 9 | ResponsiveContainer,
|
|---|
| 10 | } from 'recharts'
|
|---|
| 11 | import { Heart, MessageCircle, BookOpen, Eye } from 'lucide-react'
|
|---|
| 12 | import { Story } from '../../types'
|
|---|
| 13 |
|
|---|
| 14 | interface Props {
|
|---|
| 15 | stories: Story[]
|
|---|
| 16 | }
|
|---|
| 17 |
|
|---|
| 18 | const StatCard: React.FC<{ icon: React.ReactNode; label: string; value: string | number; color: string }> = ({
|
|---|
| 19 | icon, label, value, color,
|
|---|
| 20 | }) => (
|
|---|
| 21 | <div className="p-4 bg-slate-800 rounded-xl border border-slate-700">
|
|---|
| 22 | <div className={`w-10 h-10 rounded-xl ${color} flex items-center justify-center mb-3`}>
|
|---|
| 23 | {icon}
|
|---|
| 24 | </div>
|
|---|
| 25 | <p className="text-2xl font-bold text-white">{typeof value === 'number' ? value.toLocaleString() : value}</p>
|
|---|
| 26 | <p className="text-slate-400 text-sm mt-0.5">{label}</p>
|
|---|
| 27 | </div>
|
|---|
| 28 | )
|
|---|
| 29 |
|
|---|
| 30 | const CustomTooltip = ({ active, payload, label }: any) => {
|
|---|
| 31 | if (active && payload && payload.length) {
|
|---|
| 32 | return (
|
|---|
| 33 | <div className="bg-slate-800 border border-slate-700 rounded-xl px-3 py-2 text-sm">
|
|---|
| 34 | <p className="text-slate-400 mb-1">{label}</p>
|
|---|
| 35 | {payload.map((p: any) => (
|
|---|
| 36 | <p key={p.dataKey} className="text-white font-medium">
|
|---|
| 37 | {p.name}: {p.value.toLocaleString()}
|
|---|
| 38 | </p>
|
|---|
| 39 | ))}
|
|---|
| 40 | </div>
|
|---|
| 41 | )
|
|---|
| 42 | }
|
|---|
| 43 | return null
|
|---|
| 44 | }
|
|---|
| 45 |
|
|---|
| 46 | export const StoryAnalytics: React.FC<Props> = ({ stories }) => {
|
|---|
| 47 | const published = stories.filter(s => s.status === 'published')
|
|---|
| 48 |
|
|---|
| 49 | const totalLikes = stories.reduce((a, s) => a + s.total_likes, 0)
|
|---|
| 50 | const totalComments = stories.reduce((a, s) => a + s.total_comments, 0)
|
|---|
| 51 | const totalChapters = stories.reduce((a, s) => a + s.total_chapters, 0)
|
|---|
| 52 | const totalViews = stories.reduce((a, s) => a + s.total_views, 0)
|
|---|
| 53 |
|
|---|
| 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 | }))
|
|---|
| 69 |
|
|---|
| 70 | if (published.length === 0) {
|
|---|
| 71 | return (
|
|---|
| 72 | <div className="text-center py-12 text-slate-500">
|
|---|
| 73 | <p>No published stories yet — analytics will appear here once you publish.</p>
|
|---|
| 74 | </div>
|
|---|
| 75 | )
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | return (
|
|---|
| 79 | <div className="space-y-6">
|
|---|
| 80 | {/* Stat cards */}
|
|---|
| 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" />
|
|---|
| 83 | <StatCard icon={<Heart size={18} className="text-rose-300" />} label="Total Likes" value={totalLikes} color="bg-rose-500/20" />
|
|---|
| 84 | <StatCard icon={<MessageCircle size={18} className="text-violet-300" />} label="Total Comments" value={totalComments} color="bg-violet-500/20" />
|
|---|
| 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 | )}
|
|---|
| 107 | </div>
|
|---|
| 108 |
|
|---|
| 109 | {/* Likes chart */}
|
|---|
| 110 | <div className="bg-slate-800 border border-slate-700 rounded-2xl p-6">
|
|---|
| 111 | <div className="flex items-center gap-2 mb-4">
|
|---|
| 112 | <Heart size={16} className="text-rose-400" />
|
|---|
| 113 | <h3 className="text-white font-semibold">Likes per Story</h3>
|
|---|
| 114 | </div>
|
|---|
| 115 | {likesData.length > 0 ? (
|
|---|
| 116 | <ResponsiveContainer width="100%" height={200}>
|
|---|
| 117 | <BarChart data={likesData}>
|
|---|
| 118 | <CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
|---|
| 119 | <XAxis dataKey="date" tick={{ fill: '#64748b', fontSize: 11 }} tickLine={false} axisLine={false} />
|
|---|
| 120 | <YAxis tick={{ fill: '#64748b', fontSize: 11 }} tickLine={false} axisLine={false} />
|
|---|
| 121 | <Tooltip content={<CustomTooltip />} />
|
|---|
| 122 | <Bar dataKey="likes" fill="#f43f5e" radius={[4, 4, 0, 0]} name="Likes" />
|
|---|
| 123 | </BarChart>
|
|---|
| 124 | </ResponsiveContainer>
|
|---|
| 125 | ) : (
|
|---|
| 126 | <p className="text-slate-500 text-sm text-center py-8">No likes data yet</p>
|
|---|
| 127 | )}
|
|---|
| 128 | </div>
|
|---|
| 129 |
|
|---|
| 130 | {/* Per-story breakdown */}
|
|---|
| 131 | {published.length > 1 && (
|
|---|
| 132 | <div className="bg-slate-800 border border-slate-700 rounded-2xl p-6">
|
|---|
| 133 | <h3 className="text-white font-semibold mb-4">Story Breakdown</h3>
|
|---|
| 134 | <div className="space-y-3">
|
|---|
| 135 | {[...published]
|
|---|
| 136 | .sort((a, b) => b.total_likes - a.total_likes)
|
|---|
| 137 | .map(s => (
|
|---|
| 138 | <div key={s.story_id} className="flex items-center justify-between text-sm">
|
|---|
| 139 | <span className="text-slate-300 truncate max-w-xs">{s.title}</span>
|
|---|
| 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>
|
|---|
| 142 | <span className="flex items-center gap-1"><Heart size={12} /> {s.total_likes}</span>
|
|---|
| 143 | <span className="flex items-center gap-1"><MessageCircle size={12} /> {s.total_comments}</span>
|
|---|
| 144 | </div>
|
|---|
| 145 | </div>
|
|---|
| 146 | ))}
|
|---|
| 147 | </div>
|
|---|
| 148 | </div>
|
|---|
| 149 | )}
|
|---|
| 150 | </div>
|
|---|
| 151 | )
|
|---|
| 152 | }
|
|---|