1+ use leptos:: * ;
2+ use leptos_router:: A ;
3+ use gloo_net:: http:: Request ;
4+ use web_sys:: RequestCredentials ;
5+ use crate :: components:: navbar:: Navbar ;
6+ use crate :: api:: api_base;
7+ use crate :: types:: { User , AnalyticsDashboardData } ;
8+
9+ #[ component]
10+ pub fn AnalyticsPage ( ) -> impl IntoView {
11+ let ( _user, set_user) = create_signal ( None :: < User > ) ;
12+ let ( data, set_data) = create_signal ( None :: < AnalyticsDashboardData > ) ;
13+
14+ // Auth & Data Fetch
15+ create_resource ( || ( ) , move |_| async move {
16+ // 1. Check Auth
17+ let auth_url = format ! ( "{}/api/me" , api_base( ) ) ;
18+ let auth_resp = Request :: get ( & auth_url)
19+ . credentials ( RequestCredentials :: Include )
20+ . send ( )
21+ . await ;
22+
23+ if let Ok ( resp) = auth_resp {
24+ if resp. ok ( ) {
25+ if let Ok ( u) = resp. json :: < User > ( ) . await {
26+ set_user. set ( Some ( u) ) ;
27+
28+ // 2. Fetch Analytics
29+ let analytics_url = format ! ( "{}/api/analytics" , api_base( ) ) ;
30+ if let Ok ( data_resp) = Request :: get ( & analytics_url)
31+ . credentials ( RequestCredentials :: Include )
32+ . send ( )
33+ . await
34+ {
35+ if let Ok ( d) = data_resp. json :: < AnalyticsDashboardData > ( ) . await {
36+ set_data. set ( Some ( d) ) ;
37+ }
38+ }
39+ }
40+ }
41+ }
42+ } ) ;
43+
44+ let format_uptime = |seconds : u64 | {
45+ if seconds < 60 {
46+ format ! ( "{}s" , seconds)
47+ } else if seconds < 3600 {
48+ format ! ( "{}m {}s" , seconds / 60 , seconds % 60 )
49+ } else {
50+ format ! ( "{}h {}m" , seconds / 3600 , ( seconds % 3600 ) / 60 )
51+ }
52+ } ;
53+
54+ view ! {
55+ <div style="min-height: 100vh; background: var(--bg-dark);" >
56+ <Navbar >
57+ <div class="navbar-actions" >
58+ <A href="/dashboard" class="btn-nav" >"← Back to Dashboard" </A >
59+ </div>
60+ </Navbar >
61+
62+ <div class="dashboard-section" >
63+ <div class="section-header" >
64+ <h2>"Project Analytics" </h2>
65+ { move || {
66+ let d = data. get( ) ;
67+ view! {
68+ <span style="color: var(--text-muted); font-family: var(--font-mono);" >
69+ { move || match d. clone( ) {
70+ Some ( _val) => format!( "Last Updated: Just now" ) ,
71+ None => "Loading..." . to_string( )
72+ } }
73+ </span>
74+ }
75+ } }
76+ </div>
77+
78+ { move || match data. get( ) {
79+ Some ( stats) => view! {
80+ // 1. TOP STATS
81+ <div class="stats-grid" >
82+ <div class="stat-card" >
83+ <span class="stat-label" >"Total Lifetime Views" </span>
84+ <span class="stat-value" >{ stats. total_lifetime_views} </span>
85+ <span class="stat-sub" >"Across all projects" </span>
86+ </div>
87+ <div class="stat-card" >
88+ <span class="stat-label" >"Active Viewers Now" </span>
89+ <span class="stat-value" >{ stats. active_sessions. len( ) } </span>
90+ <span class="stat-sub" >"Live Containers" </span>
91+ </div>
92+ <div class="stat-card" >
93+ <span class="stat-label" >"Total Projects" </span>
94+ <span class="stat-value" >{ stats. project_breakdown. len( ) } </span>
95+ <span class="stat-sub" >"Published Images" </span>
96+ </div>
97+ </div>
98+
99+ // 2. LIVE SESSIONS
100+ <h3 style="margin-bottom: 20px; color: var(--text-main);" >"Live Sessions" </h3>
101+ <div class="data-table-container" >
102+ <table class="data-table" >
103+ <thead>
104+ <tr>
105+ <th>"Project" </th>
106+ <th>"Status" </th>
107+ <th>"Uptime" </th>
108+ <th>"Container ID" </th>
109+ </tr>
110+ </thead>
111+ <tbody>
112+ { if stats. active_sessions. is_empty( ) {
113+ view! {
114+ <tr>
115+ <td colspan="4" style="text-align: center; color: var(--text-muted); padding: 32px;" >
116+ "No active viewers right now."
117+ </td>
118+ </tr>
119+ } . into_view( )
120+ } else {
121+ stats. active_sessions. into_iter( ) . map( |session| {
122+ view! {
123+ <tr>
124+ <td style="font-weight: 600;" >{ session. slug} </td>
125+ <td>
126+ <span class="status-dot active" ></span> "Running"
127+ </td>
128+ <td>
129+ <span class="uptime-badge" >{ format_uptime( session. uptime_seconds) } </span>
130+ </td>
131+ <td style="font-family: var(--font-mono); color: var(--text-muted); font-size: 0.85rem;" >
132+ { session. container_name}
133+ </td>
134+ </tr>
135+ }
136+ } ) . collect_view( )
137+ } }
138+ </tbody>
139+ </table>
140+ </div>
141+
142+ // 3. PROJECT PERFORMANCE
143+ <h3 style="margin-bottom: 20px; color: var(--text-main);" >"Performance by Project" </h3>
144+ <div class="data-table-container" >
145+ <table class="data-table" >
146+ <thead>
147+ <tr>
148+ <th>"Project Name" </th>
149+ <th>"Total Views" </th>
150+ <th style="width: 40%;" >"Engagement" </th>
151+ </tr>
152+ </thead>
153+ <tbody>
154+ { stats. project_breakdown. into_iter( ) . map( |proj| {
155+ // Calculate percentage for bar width
156+ let percent = if stats. total_lifetime_views > 0 {
157+ ( proj. view_count as f64 / stats. total_lifetime_views as f64 ) * 100.0
158+ } else {
159+ 0.0
160+ } ;
161+
162+ view! {
163+ <tr>
164+ <td style="font-weight: 600;" >{ proj. slug} </td>
165+ <td style="font-family: var(--font-mono);" >{ proj. view_count} </td>
166+ <td>
167+ <div style="display: flex; align-items: center; gap: 12px;" >
168+ <div class="progress-bar-bg" >
169+ <div class="progress-bar-fill" style=format!( "width: {}%" , percent) ></div>
170+ </div>
171+ <span style="font-size: 0.8rem; color: var(--text-muted); width: 40px; text-align: right;" >
172+ { format!( "{:.1}%" , percent) }
173+ </span>
174+ </div>
175+ </td>
176+ </tr>
177+ }
178+ } ) . collect_view( ) }
179+ </tbody>
180+ </table>
181+ </div>
182+
183+ } . into_view( ) ,
184+ None => view! {
185+ <div style="display: flex; justify-content: center; padding: 100px;" >
186+ <div class="spinner" ></div>
187+ </div>
188+ } . into_view( )
189+ } }
190+ </div>
191+ </div>
192+ }
193+ }
0 commit comments