AnalyticsPanel.svelte RawHistoryFolder
1<script lang="ts">
2 import { homepageState } from '../state.svelte';
3 import { fetchAnalytics } from '../messaging';
4 import { DailyVisitsChart, HourlyActivityChart } from '../analytics';
5
6 // Fetch analytics on mount
7 $effect(() => {
8 if (!homepageState.analytics) {
9 fetchAnalytics();
10 }
11 });
12
13 const formatNumber = (n: number): string => {
14 if (n >= 1000) {
15 return `${(n / 1000).toFixed(1)}k`;
16 }
17 return n.toString();
18 };
19</script>
20
21<div class="analytics-panel">
22 {#if homepageState.analyticsLoading}
23 <div class="loading-state">
24 <span class="loading-spinner"></span>
25 <span class="loading-text">LOADING ANALYTICS...</span>
26 </div>
27 {:else if homepageState.analytics}
28 <div class="stats-grid">
29 <div class="stat-card">
30 <span class="stat-value">{formatNumber(homepageState.analytics.total_entries)}</span>
31 <span class="stat-label">TOTAL ENTRIES</span>
32 </div>
33 <div class="stat-card">
34 <span class="stat-value">{formatNumber(homepageState.analytics.total_visits)}</span>
35 <span class="stat-label">TOTAL VISITS</span>
36 </div>
37 <div class="stat-card">
38 <span class="stat-value">{homepageState.analytics.unique_days}</span>
39 <span class="stat-label">DAYS ACTIVE</span>
40 </div>
41 <div class="stat-card">
42 <span class="stat-value">{homepageState.domainStats.length}</span>
43 <span class="stat-label">UNIQUE DOMAINS</span>
44 </div>
45 </div>
46
47 <div class="section">
48 <div class="section-header">
49 <span class="section-title">TOP DOMAINS</span>
50 </div>
51 <div class="domain-list">
52 {#each homepageState.domainStats.slice(0, 10) as domain, i (domain.domain)}
53 <div class="domain-item">
54 <span class="domain-rank">{i + 1}</span>
55 <span class="domain-name">{domain.domain}</span>
56 <span class="domain-stats">
57 <span class="domain-pages">{domain.page_count} pages</span>
58 <span class="domain-visits">{domain.total_visits} visits</span>
59 </span>
60 </div>
61 {/each}
62 </div>
63 </div>
64
65 <!-- Daily Visits Chart (30 days) -->
66 {#if homepageState.dailyVisits.length > 0}
67 <div class="section">
68 <div class="section-header">
69 <span class="section-title">VISITS (LAST 30 DAYS)</span>
70 </div>
71 <div class="chart-container">
72 <DailyVisitsChart dailyVisits={homepageState.dailyVisits} />
73 </div>
74 </div>
75 {/if}
76
77 <!-- Hourly Activity Chart -->
78 {#if homepageState.analytics.hourly_distribution.length > 0}
79 <div class="section">
80 <div class="section-header">
81 <span class="section-title">ACTIVITY BY HOUR</span>
82 </div>
83 <div class="chart-container">
84 <HourlyActivityChart hourlyDistribution={homepageState.analytics.hourly_distribution} />
85 </div>
86 </div>
87 {/if}
88 {:else}
89 <div class="empty-state">
90 <span class="empty-icon"></span>
91 <span class="empty-text">NO ANALYTICS DATA</span>
92 <span class="empty-hint">Browse some sites to generate analytics</span>
93 </div>
94 {/if}
95</div>
96
97<style>
98 .analytics-panel {
99 display: flex;
100 flex-direction: column;
101 gap: 1rem;
102 padding: 0.5rem 1rem;
103 }
104
105 .loading-state,
106 .empty-state {
107 flex: 1;
108 display: flex;
109 flex-direction: column;
110 align-items: center;
111 justify-content: center;
112 gap: 0.5rem;
113 padding: 2rem;
114 color: var(--muted-foreground);
115 text-align: center;
116 }
117
118 .loading-spinner {
119 width: 24px;
120 height: 24px;
121 border-width: 2px;
122 border-style: solid;
123 border-color: var(--border);
124 border-top-color: var(--primary, #4ade80);
125 border-radius: 50%;
126 animation: spin 0.8s linear infinite;
127 }
128
129 @keyframes spin {
130 to { transform: rotate(360deg); }
131 }
132
133 .loading-text,
134 .empty-text {
135 font-size: 0.68rem;
136 font-weight: 600;
137 letter-spacing: 0.12em;
138 }
139
140 .empty-icon {
141 font-size: 1.5rem;
142 opacity: 0.5;
143 }
144
145 .empty-hint {
146 font-size: 0.65rem;
147 letter-spacing: 0.06em;
148 opacity: 0.7;
149 }
150
151 .stats-grid {
152 display: grid;
153 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
154 gap: 0.5rem;
155 }
156
157 .stat-card {
158 display: flex;
159 flex-direction: column;
160 gap: 0.3rem;
161 padding: 0.85rem;
162 border-width: 1px;
163 border-style: solid;
164 border-color: var(--border);
165 background: color-mix(in srgb, var(--background) 92%, var(--card) 8%);
166 }
167
168 .stat-value {
169 font-size: 1.5rem;
170 font-weight: 700;
171 color: var(--foreground);
172 letter-spacing: -0.02em;
173 }
174
175 .stat-label {
176 font-size: 0.6rem;
177 font-weight: 600;
178 color: var(--muted-foreground);
179 letter-spacing: 0.12em;
180 }
181
182 .section {
183 border-width: 1px;
184 border-style: solid;
185 border-color: var(--border);
186 background: color-mix(in srgb, var(--background) 92%, var(--card) 8%);
187 }
188
189 .section-header {
190 padding: 0.5rem 0.75rem;
191 border-bottom: 1px solid var(--border);
192 background: color-mix(in srgb, var(--background) 80%, var(--card) 20%);
193 }
194
195 .section-title {
196 font-size: 0.6rem;
197 font-weight: 600;
198 color: var(--muted-foreground);
199 letter-spacing: 0.12em;
200 }
201
202 .domain-list {
203 display: flex;
204 flex-direction: column;
205 }
206
207 .domain-item {
208 display: grid;
209 grid-template-columns: auto 1fr auto;
210 align-items: center;
211 gap: 0.75rem;
212 padding: 0.5rem 0.75rem;
213 border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
214 }
215
216 .domain-item:last-child {
217 border-bottom: none;
218 }
219
220 .domain-rank {
221 width: 1.5rem;
222 font-size: 0.65rem;
223 font-weight: 600;
224 color: var(--muted-foreground);
225 text-align: center;
226 }
227
228 .domain-name {
229 font-size: 0.75rem;
230 color: var(--foreground);
231 white-space: nowrap;
232 overflow: hidden;
233 text-overflow: ellipsis;
234 }
235
236 .domain-stats {
237 display: flex;
238 gap: 0.75rem;
239 font-size: 0.6rem;
240 color: var(--muted-foreground);
241 }
242
243 .chart-container {
244 padding: 0.5rem;
245 }
246</style>