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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user