/** * Visitors, with zero PII: a generated face and name, where they came from, * their country and browser, how many pages they read and when they were last * seen. The card shows the latest eight (D7); the Details window lists * everyone. A click opens that visitor's panel. */ import { useState } from 'react'; import { __, _n, sprintf } from '@wordpress/i18n'; import { useUi } from './ui-context'; import CardShell, { MetricName } from './card'; import { useApi } from './api'; import { identity } from './identity'; import { labelFor } from './table'; import VisitorFace from './visitor-face'; import LoadError from './load-error'; const CARD_ROWS = 8; const PAGE = 25; const ONLINE_MS = 5 * 60 * 1000; // The summary's "online" window. const seenAt = ( ts ) => new Date( String( ts ).replace( ' ', 'T' ) + 'Z' ); /** "2 min. ago", "3 hr. ago", "4 days ago", then the date. */ export function ago( ts ) { let rtf; try { rtf = new Intl.RelativeTimeFormat( undefined, { numeric: 'auto', style: 'short' } ); } catch { return seenAt( ts ).toLocaleString(); } const min = Math.round( ( Date.now() - seenAt( ts ).getTime() ) / 60000 ); if ( min < 60 ) { return rtf.format( -Math.max( 0, min ), 'minute' ); } if ( min < 24 * 60 ) { return rtf.format( -Math.round( min / 60 ), 'hour' ); } if ( min < 7 * 24 * 60 ) { return rtf.format( -Math.round( min / 1440 ), 'day' ); } return seenAt( ts ).toLocaleDateString( undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' } ); } /** Where a visitor came from: the referrer host, else the campaign, else the channel. */ export function cameFrom( row, meta ) { return row.first_referrer || row.first_campaign || ( meta.channels[ row.first_channel ] ?? __( 'Direct', 'saddle-analytics' ) ); } const pagesText = ( n ) => sprintf( /* translators: %s: number of pages a visitor read. */ _n( '%s page', '%s pages', n, 'saddle-analytics' ), Number( n ).toLocaleString() ); export function VisitorsCard( { meta, onOpen, onDetails } ) { const { Skeleton } = useUi(); const { data, error, retry } = useApi( 'journeys', { limit: CARD_ROWS, offset: 0 } ); const rows = data?.rows ?? []; let body; if ( error ) { body = ; } else if ( ! data ) { body = ; } else if ( 0 === rows.length ) { body =

{ __( 'No visitors yet.', 'saddle-analytics' ) }

; } else { body = ( ); } return ( { __( 'Last seen', 'saddle-analytics' ) } } onDetails={ onDetails } > { body } ); } /** Everyone, newest first, 25 at a time; for the Details window. */ export function VisitorsTable( { meta, search, onOpen } ) { const { Button, DataTable } = useUi(); const [ offset, setOffset ] = useState( 0 ); const { data, error, retry } = useApi( 'journeys', { limit: PAGE, offset } ); const total = data?.total ?? 0; const query = search.trim().toLowerCase(); const rows = ( data?.rows ?? [] ).filter( ( row ) => ! query || `${ identity( row.visitor_id ).name } ${ cameFrom( row, meta ) }`.toLowerCase().includes( query ) ); if ( error ) { return ; } return (
( { identity( row.visitor_id ).name } ), }, { key: 'from', header: __( 'Came from', 'saddle-analytics' ), render: ( row ) => cameFrom( row, meta ) }, { key: 'country', header: __( 'Country', 'saddle-analytics' ), render: ( row ) => ( row.country ? labelFor( { value: row.country }, { key: 'country' }, meta ) : '' ), }, { key: 'device', header: __( 'Device', 'saddle-analytics' ), render: ( row ) => [ labelFor( { value: row.device }, { key: 'device', format: 'device' }, meta ), row.browser ] .filter( ( part ) => part && __( 'None', 'saddle-analytics' ) !== part ) .join( ' · ' ), }, { key: 'pageviews', header: __( 'Pages', 'saddle-analytics' ), align: 'right', render: ( row ) => row.pageviews.toLocaleString() }, { key: 'last_seen', header: __( 'Last seen', 'saddle-analytics' ), render: ( row ) => ago( row.last_seen ) }, ] } rows={ rows } rowId={ ( row ) => row.visitor_id } loading={ ! data } stickyHeader onRowClick={ ( row ) => onOpen( row.visitor_id ) } emptyTitle={ query ? __( 'Nothing matches.', 'saddle-analytics' ) : __( 'No visitors yet', 'saddle-analytics' ) } /> { total > PAGE && (
{ sprintf( /* translators: 1: first row number, 2: last row number, 3: total rows. */ __( '%1$d to %2$d of %3$d', 'saddle-analytics' ), offset + 1, Math.min( offset + PAGE, total ), total ) }
) }
); }