web: put the traffic screen in the admin frame

installTracking() beacons one view per page load and one click per control that
carries a data-key, so every existing button is already counted. The new STATS
pane reads it back: a 7/30/90-day range, the three totals, an SVG timeline and
eight proportional bar lists (pages, clicked features, country, region, city,
browser, system, device).
This commit is contained in:
2026-09-18 14:23:25 +07:00
parent 8a7192eae8
commit 89beb5f160
8 changed files with 305 additions and 1 deletions
+6 -1
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { api, type AdminPhoto, type AdminUser, type PhotoSlot } from './api';
import { useI18n } from './i18n/I18nProvider';
import { Profile } from './ui/Profile';
import { Stats } from './ui/Stats';
import type { MsgKey } from './i18n/vi';
// Curating screen: one bordered frame, a left column of what can be managed and
@@ -9,13 +10,14 @@ import type { MsgKey } from './i18n/vi';
// Profile — the signed-in account's own picture/email/password
// User account — every account: its name, email, picture and contribution count
// Pictures — every photo in the system, with the slot that shows it
// Stats — the visitor counter: views, clicks and their breakdowns
// Close — leaves the frame and goes back to the landing page
// The frame holds no privilege of its own — the API answers 403 unless the
// signed-in account is on the ADMIN_EMAILS allowlist, so this is only a viewer.
// ponytail: no pagination. The upload quota caps the table at a handful of
// rows per account; add a page cursor when the strip outgrows one screen.
type State = 'loading' | 'guest' | 'forbidden' | 'ready';
type Tab = 'profile' | 'users' | 'pictures';
type Tab = 'profile' | 'users' | 'pictures' | 'stats';
// The four places a photo can be drawn from. `strip` feeds the community reel;
// each live slot shows one photo of its set, picked at random per page load.
@@ -31,6 +33,7 @@ const MENU: { id: Tab; key: MsgKey }[] = [
{ id: 'profile', key: 'adm.tabProfile' },
{ id: 'users', key: 'adm.tabUsers' },
{ id: 'pictures', key: 'adm.tabPictures' },
{ id: 'stats', key: 'adm.tabStats' },
];
export function Admin() {
@@ -429,6 +432,8 @@ export function Admin() {
) : null}
{tab === 'profile' ? <Profile onSaved={(msg) => setNote(msg)} /> : null}
{tab === 'stats' ? <Stats /> : null}
</div>
</div>
) : null}