1+ "use client" ;
2+
3+ import React , { useEffect , useRef , useState } from "react" ;
4+ import StatCard from "@/components/dashboard/StatCard" ;
5+ import Skeleton from "@/components/ui/Skeleton" ;
6+ import { Download , Eye , Clock , BarChart3 } from "lucide-react" ;
7+ import analytics from "@/app/lib/analytics" ;
8+
9+ type AnalyticsResponse = {
10+ totalViews : number ;
11+ totalWatchTime : number ;
12+ avgEngagement : number ;
13+ byPlatform : { platform : string ; views : number ; engagement : number } [ ] ;
14+ top5 : { clipId : string ; title : string ; views : number ; platform : string } [ ] ;
15+ dateRange : { startDate : string | null ; endDate : string | null } ;
16+ } ;
17+
18+ export default function AnalyticsPage ( ) {
19+ const [ data , setData ] = useState < AnalyticsResponse | null > ( null ) ;
20+ const [ loading , setLoading ] = useState ( true ) ;
21+ const [ error , setError ] = useState < string | null > ( null ) ;
22+ const [ range , setRange ] = useState ( "30d" ) ;
23+ const [ platform , setPlatform ] = useState ( "all" ) ;
24+ const initialized = useRef ( false ) ;
25+
26+ useEffect ( ( ) => {
27+ if ( initialized . current ) return ;
28+ initialized . current = true ;
29+ analytics . trackPageView ( "/analytics" ) ;
30+ } , [ ] ) ;
31+
32+ useEffect ( ( ) => {
33+ let cancelled = false ;
34+ async function load ( ) {
35+ try {
36+ setLoading ( true ) ;
37+ const params = new URLSearchParams ( ) ;
38+ if ( range !== "all" ) params . set ( "startDate" , new Date ( Date . now ( ) - Number ( range . replace ( "d" , "" ) ) * 86400000 ) . toISOString ( ) . split ( "T" ) [ 0 ] ) ;
39+ if ( platform !== "all" ) params . set ( "platform" , platform ) ;
40+ const res = await fetch ( `/api/analytics?${ params . toString ( ) } ` ) ;
41+ if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
42+ const json = ( await res . json ( ) ) as AnalyticsResponse ;
43+ if ( ! cancelled ) setData ( json ) ;
44+ } catch ( err ) {
45+ if ( ! cancelled ) setError ( err instanceof Error ? err . message : "Failed to load analytics" ) ;
46+ } finally {
47+ if ( ! cancelled ) setLoading ( false ) ;
48+ }
49+ }
50+ load ( ) ;
51+ return ( ) => { cancelled = true ; } ;
52+ } , [ range , platform ] ) ;
53+
54+ const exportCsv = ( ) => {
55+ if ( ! data ) return ;
56+ const header = "clipId,title,views,watchTimeMinutes,engagementRate,platform\n" ;
57+ const rows = data . top5 . map ( t => `${ t . clipId } ,"${ t . title } ",${ t . views } ,,,${ t . platform } ` ) . join ( "\n" ) ;
58+ const blob = new Blob ( [ header + rows ] , { type : "text/csv" } ) ;
59+ const url = URL . createObjectURL ( blob ) ;
60+ const a = document . createElement ( "a" ) ;
61+ a . href = url ;
62+ a . download = `analytics-${ range } .csv` ;
63+ a . click ( ) ;
64+ URL . revokeObjectURL ( url ) ;
65+ } ;
66+
67+ return (
68+ < div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-10 py-10" >
69+ < div className = "space-y-8" >
70+ < div className = "flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4" >
71+ < div >
72+ < h1 className = "text-[28px] sm:text-[32px] font-extrabold tracking-tight text-white" > Clip Analytics</ h1 >
73+ < p className = "text-muted text-[14px] mt-1" > Views, watch time, engagement, and platform breakdown.</ p >
74+ </ div >
75+ < div className = "flex items-center gap-3" >
76+ < select value = { range } onChange = { ( e : React . ChangeEvent < HTMLSelectElement > ) => setRange ( e . target . value ) } className = "bg-input text-white text-sm rounded-xl px-3 py-2 border border-white/10" >
77+ < option value = "7d" > Last 7 days</ option >
78+ < option value = "30d" > Last 30 days</ option >
79+ < option value = "90d" > Last 90 days</ option >
80+ < option value = "all" > All time</ option >
81+ </ select >
82+ < select value = { platform } onChange = { ( e : React . ChangeEvent < HTMLSelectElement > ) => setPlatform ( e . target . value ) } className = "bg-input text-white text-sm rounded-xl px-3 py-2 border border-white/10" >
83+ < option value = "all" > All platforms</ option >
84+ < option value = "YouTube" > YouTube</ option >
85+ < option value = "TikTok" > TikTok</ option >
86+ < option value = "Instagram" > Instagram</ option >
87+ < option value = "Twitch" > Twitch</ option >
88+ </ select >
89+ < button onClick = { exportCsv } className = "inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-brand text-black font-bold text-sm hover:bg-brand-hover transition-colors" >
90+ < Download className = "w-4 h-4" /> Export
91+ </ button >
92+ </ div >
93+ </ div >
94+
95+ { loading ? (
96+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-6" >
97+ { [ 1 , 2 , 3 ] . map ( ( i ) => ( < div key = { i } className = "h-32 rounded-2xl bg-white/6 animate-pulse" /> ) ) }
98+ </ div >
99+ ) : error ? (
100+ < div className = "rounded-2xl border border-error/30 bg-error/10 p-6" >
101+ < p className = "text-error text-sm" > { error } </ p >
102+ </ div >
103+ ) : data ? (
104+ < >
105+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-6" >
106+ < StatCard label = "Total Views" value = { String ( data . totalViews ) } icon = { Eye } trend = { `${ data . totalViews . toLocaleString ( ) } views` } />
107+ < StatCard label = "Watch Time" value = { `${ data . totalWatchTime . toLocaleString ( ) } m` } icon = { Clock } trend = "Total minutes watched" />
108+ < StatCard label = "Engagement" value = { `${ data . avgEngagement . toFixed ( 2 ) } %` } icon = { BarChart3 } trend = "Average rate" />
109+ </ div >
110+
111+ < div className = "grid grid-cols-1 lg:grid-cols-2 gap-6" >
112+ < div className = "bg-surface border border-white/5 rounded-2xl p-6" >
113+ < h3 className = "text-white font-bold mb-4" > Engagement by Platform</ h3 >
114+ < div className = "space-y-3" >
115+ { data . byPlatform . map ( ( p ) => (
116+ < div key = { p . platform } className = "flex items-center justify-between" >
117+ < span className = "text-sm text-muted" > { p . platform } </ span >
118+ < div className = "flex-1 mx-4" >
119+ < div className = "h-2 rounded-full bg-white/6 overflow-hidden" >
120+ < div className = "h-full bg-brand rounded-full" style = { { width : `${ Math . min ( p . engagement * 10 , 100 ) } %` } } />
121+ </ div >
122+ </ div >
123+ < span className = "text-sm text-white font-mono w-12 text-right" > { p . engagement } %</ span >
124+ </ div >
125+ ) ) }
126+ </ div >
127+ </ div >
128+ < div className = "bg-surface border border-white/5 rounded-2xl p-6" >
129+ < h3 className = "text-white font-bold mb-4" > Top 5 Clips</ h3 >
130+ < div className = "space-y-3" >
131+ { data . top5 . map ( ( clip , idx ) => (
132+ < div key = { clip . clipId } className = "flex items-center justify-between" >
133+ < div className = "min-w-0" >
134+ < p className = "text-sm text-white font-semibold truncate" > { clip . title } </ p >
135+ < p className = "text-xs text-muted" > { clip . platform } · { clip . views . toLocaleString ( ) } views</ p >
136+ </ div >
137+ < span className = "text-xs text-muted w-6 text-right" > #{ idx + 1 } </ span >
138+ </ div >
139+ ) ) }
140+ </ div >
141+ </ div >
142+ </ div >
143+ </ >
144+ ) : null }
145+ </ div >
146+ </ div >
147+ ) ;
148+ }
0 commit comments