import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { query } from '../api/client'; import { useQuery } from '../lib/hooks'; import { duration, num, percent, when } from '../lib/format'; import { Banner, Card, EmptyRow, Field, Grid, Loading, Meter, PageHead, TableWrap, Tag, Tiles, } from '../components/ui'; /* Memby's major feature catalogue, in the order it is presented. * * It is a list here rather than derived from what has been used, and that is the whole * point of the table it feeds: a feature nobody has touched has no row in the analytics * and would simply be absent, which is indistinguishable from a feature that does not * exist. Listing it and showing a zero is what makes "not used" an answer. */ const FEATURE_CATALOGUE = [ 'home', 'movies', 'shows', 'favorites', 'search', 'recent_searches', 'genre_browse', 'for_you', 'for_you_time', 'recommendation', 'continue', 'latest', 'my_shows', 'details', 'playback', 'notifications', 'profiles', 'settings', ]; /** The wire values are ids; this is the render, so it is spelled — the same boundary the * launcher's own row titles observe. */ function label(value: string | undefined | null): string { const text = String(value || '—').replaceAll('_', ' '); if (text === 'favorites') return 'Favourites'; if (text === 'abandoned') return 'Abandoned / interrupted'; return text; } interface JourneysResponse { days: number; retentionDays: number; stats: { events: number; journeys: number; viewers: number; completed: number; abandoned: number; active: number; averageSteps: number; averageTimeMs: number; completionRate: number; }; users: { userId: string; username: string }[] | null; features: { feature: string; uses: number; lastUsedAt: string }[] | null; actions: { category: string; action: string; events: number; journeys: number }[] | null; paths: { from: string; to: string; count: number }[] | null; } export function JourneysPage() { const [days, setDays] = useState(30); const [userId, setUserId] = useState(''); const navigate = useNavigate(); const path = useMemo(() => `/admin/api/journeys${query({ days, userId })}`, [days, userId]); const { data, error, loading } = useQuery(path); const stats = data?.stats; const users = data?.users ?? []; const actions = data?.actions ?? []; const paths = data?.paths ?? []; const topPath = paths[0]; const features = useMemo(() => { const used = new Map((data?.features ?? []).map((feature) => [feature.feature, feature])); const position = new Map(FEATURE_CATALOGUE.map((name, index) => [name, index])); return [...new Set([...FEATURE_CATALOGUE, ...used.keys()])] .map((name) => ({ name, stat: used.get(name) })) .sort((left, right) => { const byUse = (right.stat?.uses ?? 0) - (left.stat?.uses ?? 0); if (byUse) return byUse; return ( (position.get(left.name) ?? Number.MAX_SAFE_INTEGER) - (position.get(right.name) ?? Number.MAX_SAFE_INTEGER) ); }); }, [data?.features]); // Individual visits are shown only for one person: across a household they are a wall // of cards with nothing to compare against, and the question they answer is always // "what did *they* do". return ( <> } > {loading ? ( ) : ( <>
Visit completion {percent(stats?.completionRate)}

{num(stats?.completed)} completed · {num(stats?.abandoned)} abandoned ·{' '} {num(stats?.active)} active

Most common route
{topPath ? `${label(topPath.from)} → ${label(topPath.to)}` : 'Not enough data'}

{topPath ? `${num(topPath.count)} times in this window` : 'Journeys will appear here as viewers move through Memby.'}

)}
{actions.length === 0 ? ( No significant actions in this window. ) : ( actions.map((action) => ( )) )}
Action Uses Visits
{label(action.action)} {label(action.category)} {num(action.events)} {num(action.journeys)}
{paths.length === 0 ? ( No repeated paths in this window. ) : ( paths.map((entry, index) => ( )) )}
Route Times
{label(entry.from)} {label(entry.to)} {num(entry.count)}
{features.map(({ name, stat }) => { const uses = stat?.uses ?? 0; return ( ); })}
Feature Uses Last used Status
{label(name)} {num(uses)} {stat ? when(stat.lastUsedAt) : '—'} {uses === 0 ? ( not used ) : uses < 3 ? ( rare ) : ( used )}
{userId ? null : (

A viewing journey follows one intent through to playback, so two films watched in a single app session appear as two separate journeys.

)} ); }