v0.23.3 fc8669a

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>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close