/**
* 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 = (
{ rows.map( ( row ) => {
const live = Date.now() - seenAt( row.last_seen ).getTime() < ONLINE_MS;
return (
-
);
} ) }
);
}
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
) }
) }
);
}