import { useQuery } from '@tanstack/react-query'; import { AppWindowIcon, ChevronRightIcon } from 'lucide-react'; import { useState } from 'react'; import { AppTile } from '#/components/atoms/app-tile.jsx'; import { Badge } from '#/components/atoms/badge.jsx'; import { Button } from '#/components/atoms/button.jsx'; import { Notice } from '#/components/atoms/notice.jsx'; import { Sparkline } from '#/components/atoms/sparkline.jsx'; import { EmptyState } from '#/components/molecules/empty-state.jsx'; import { ErrorNotice } from '#/components/molecules/error-notice.jsx'; import { ListRow } from '#/components/molecules/list-row.jsx'; import { SearchField } from '#/components/molecules/search-field.jsx'; import { get, post } from '#/lib/api.js'; import { appActivitySeries, combineOffers, lastActiveAt, mergeApps, num, timeAgo, } from '#/lib/format.js'; import { queryKeys, useApiMutation } from '#/lib/query.js'; const SEARCH_THRESHOLD = 6; export function AppsPage({ data, appHref, onNavigate }) { const { data: appsData, error } = useQuery({ queryKey: queryKeys.apps, queryFn: () => get('/apps'), }); const { data: spacesData } = useQuery({ queryKey: queryKeys.spaces, queryFn: () => get('/spaces'), enabled: Boolean(data.features?.spaces), }); const [query, setQuery] = useState(''); const combine = useApiMutation([queryKeys.apps]); if (error) { return {error.message}; } if (!appsData) return null; const apps = mergeApps( data.collections, appsData.apps, appsData.aliases, spacesData?.spaces, ); const offers = appsData.aliasesAvailable ? combineOffers(apps, appsData) : []; if (!apps.length) { return ( } title="No apps yet" description="An app you sign in to, or that writes records to your account, appears here. Open one to see what it has stored and manage its access." /> ); } const needle = query.trim().toLowerCase(); const matches = needle ? apps.filter( (app) => app.domain.toLowerCase().includes(needle) || (app.folded || []).some((domain) => domain.includes(needle)), ) : apps; return (
{offers.map((offer) => ( ${offer.to}`}> {offer.to} and{' '} {offer.from}{' '} look like the same app. One name holds the sign-in and the other holds its records. Combine them into one entry?
))} {apps.length > SEARCH_THRESHOLD && ( )} {matches.length === 0 && (

No apps match "{query.trim()}".

)} {matches.map((app) => { const active = lastActiveAt(app); // Lead with recency — the app's own page and History carry the record // detail. Fall back to a count only when nothing datable is known (an // app whose records all have non-TID rkeys, and no session). let description; if (active) { description = `Active ${timeAgo(active)}`; } else if (app.count > 0) { description = `${num(app.count)} record${app.count === 1 ? '' : 's'}`; } else { description = 'No records stored'; } if (app.folded?.length) { description += ` · includes ${app.folded.join(', ')}`; } const series = appActivitySeries( data.recordActivity, app.domain, app.folded, ); return ( } media={} title={
{app.domain} {app.sessions.length > 0 && ( {app.sessions.length} session {app.sessions.length === 1 ? '' : 's'} )}
} description={description} actions={ } /> ); })}
); }