improve profile page
This commit is contained in:
@@ -30,7 +30,7 @@ interface EventListProps {
|
||||
count: number;
|
||||
}
|
||||
export function EventList({ data, count }: EventListProps) {
|
||||
const { cursor, setCursor } = useCursor();
|
||||
const { cursor, setCursor, loading } = useCursor();
|
||||
const [filters] = useEventQueryFilters();
|
||||
return (
|
||||
<>
|
||||
@@ -77,6 +77,7 @@ export function EventList({ data, count }: EventListProps) {
|
||||
setCursor={setCursor}
|
||||
count={count}
|
||||
take={50}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -92,6 +93,7 @@ export function EventList({ data, count }: EventListProps) {
|
||||
setCursor={setCursor}
|
||||
count={count}
|
||||
take={50}
|
||||
loading={loading}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||
|
||||
export default FullPageLoadingState;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Suspense, useMemo } from 'react';
|
||||
import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout';
|
||||
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||
@@ -64,12 +64,7 @@ export default async function Page({
|
||||
};
|
||||
const startDate = parseAsString.parseServerSide(searchParams.startDate);
|
||||
const endDate = parseAsString.parseServerSide(searchParams.endDate);
|
||||
const [profile, events, count, metrics] = await Promise.all([
|
||||
getProfileById(profileId, projectId),
|
||||
getEventList(eventListOptions),
|
||||
getEventsCount(eventListOptions),
|
||||
getProfileMetrics(profileId, projectId),
|
||||
]);
|
||||
const profile = await getProfileById(profileId, projectId);
|
||||
|
||||
const pageViewsChart: IChartInput = {
|
||||
projectId,
|
||||
@@ -148,11 +143,11 @@ export default async function Page({
|
||||
return (
|
||||
<>
|
||||
<PageLayout organizationSlug={organizationSlug} title={<div />} />
|
||||
<StickyBelowHeader className="!relative !top-auto !z-0 flex items-center gap-8 p-8">
|
||||
<StickyBelowHeader className="!relative !top-auto !z-0 flex flex-col gap-8 p-4 md:flex-row md:items-center md:p-8">
|
||||
<div className="flex flex-1 gap-4">
|
||||
<ProfileAvatar {...profile} size={'lg'} />
|
||||
<div className="">
|
||||
<h1 className="text-2xl font-semibold">
|
||||
<div className="min-w-0">
|
||||
<h1 className="max-w-full overflow-hidden text-ellipsis break-words text-lg font-semibold md:max-w-sm md:whitespace-nowrap md:text-2xl">
|
||||
{getProfileName(profile)}
|
||||
</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -194,31 +189,19 @@ export default async function Page({
|
||||
</Widget>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<EventList data={events} count={count} />
|
||||
<Suspense fallback={<div />}>
|
||||
<EventListServer {...eventListOptions} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ValueRow({ name, value }: { name: string; value?: unknown }) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="font-medium capitalize text-muted-foreground">
|
||||
{name.replace('_', ' ')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-right">
|
||||
{typeof value === 'string' ? (
|
||||
<>
|
||||
<SerieIcon name={value} /> {value}
|
||||
</>
|
||||
) : (
|
||||
<>{value}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
async function EventListServer(props: GetEventListOptions) {
|
||||
const [events, count] = await Promise.all([
|
||||
getEventList(props),
|
||||
getEventsCount(props),
|
||||
]);
|
||||
return <EventList data={events} count={count} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import withLoadingWidget from '@/hocs/with-loading-widget';
|
||||
import withSuspense from '@/hocs/with-suspense';
|
||||
|
||||
import { getProfileMetrics } from '@openpanel/db';
|
||||
|
||||
@@ -14,4 +14,4 @@ const ProfileMetricsServer = async ({ projectId, profileId }: Props) => {
|
||||
return <ProfileMetrics data={data} />;
|
||||
};
|
||||
|
||||
export default withLoadingWidget(ProfileMetricsServer);
|
||||
export default withSuspense(ProfileMetricsServer, () => null);
|
||||
|
||||
@@ -12,52 +12,52 @@ type Props = {
|
||||
const ProfileMetrics = ({ data }: Props) => {
|
||||
const number = useNumber();
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<div className="rounded-xl text-right">
|
||||
<div className="flex flex-wrap gap-6 whitespace-nowrap md:justify-end md:text-right">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
First seen
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
<div className="text-lg font-semibold">
|
||||
{formatDistanceToNow(data.firstSeen)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl text-right">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Last seen
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
<div className="text-lg font-semibold">
|
||||
{formatDistanceToNow(data.lastSeen)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl text-right">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Sessions
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
<div className="text-lg font-semibold">
|
||||
{number.format(data.sessions)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl text-right">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Avg. Session
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
<div className="text-lg font-semibold">
|
||||
{number.formatWithUnit(data.durationAvg / 1000, 'min')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl text-right">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
P90. Session
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
<div className="text-lg font-semibold">
|
||||
{number.formatWithUnit(data.durationP90 / 1000, 'min')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl text-right">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
Page views
|
||||
</div>
|
||||
<div className="text-xl font-medium">
|
||||
<div className="text-lg font-semibold">
|
||||
{number.format(data.screenViews)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||
|
||||
export default FullPageLoadingState;
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
filters?: IChartEventFilter[];
|
||||
}
|
||||
|
||||
const limit = 50;
|
||||
const limit = 40;
|
||||
|
||||
async function ProfileListServer({ projectId, cursor, filters }: Props) {
|
||||
const [profiles, count] = await Promise.all([
|
||||
|
||||
@@ -23,7 +23,7 @@ interface ProfileListProps {
|
||||
}
|
||||
export function ProfileList({ data, count, limit = 50 }: ProfileListProps) {
|
||||
const { organizationSlug, projectId } = useAppParams();
|
||||
const { cursor, setCursor } = useCursor();
|
||||
const { cursor, setCursor, loading } = useCursor();
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetHead className="flex items-center justify-between">
|
||||
@@ -32,6 +32,7 @@ export function ProfileList({ data, count, limit = 50 }: ProfileListProps) {
|
||||
size="sm"
|
||||
cursor={cursor}
|
||||
setCursor={setCursor}
|
||||
loading={loading}
|
||||
count={count}
|
||||
take={limit}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@ export function Pagination({
|
||||
setCursor,
|
||||
className,
|
||||
size = 'base',
|
||||
loading,
|
||||
}: {
|
||||
take: number;
|
||||
count: number;
|
||||
@@ -36,6 +37,7 @@ export function Pagination({
|
||||
setCursor: Dispatch<SetStateAction<number>>;
|
||||
className?: string;
|
||||
size?: 'sm' | 'base';
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const lastCursor = Math.floor(count / take) - 1;
|
||||
const isNextDisabled = count === 0 || lastCursor === cursor;
|
||||
@@ -56,42 +58,43 @@ export function Pagination({
|
||||
)}
|
||||
{size === 'base' && (
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCursor(0)}
|
||||
disabled={cursor === 0}
|
||||
className="max-sm:hidden"
|
||||
>
|
||||
<ChevronsLeftIcon size={14} />
|
||||
</Button>
|
||||
icon={ChevronsLeftIcon}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
||||
disabled={cursor === 0}
|
||||
>
|
||||
<ChevronLeftIcon size={14} />
|
||||
</Button>
|
||||
icon={ChevronLeftIcon}
|
||||
/>
|
||||
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCursor((p) => Math.min(lastCursor, p + 1))}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
<ChevronRightIcon size={14} />
|
||||
</Button>
|
||||
icon={ChevronRightIcon}
|
||||
/>
|
||||
|
||||
{size === 'base' && (
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setCursor(lastCursor)}
|
||||
disabled={isNextDisabled}
|
||||
className="max-sm:hidden"
|
||||
>
|
||||
<ChevronsRightIcon size={14} />
|
||||
</Button>
|
||||
icon={ChevronsRightIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -76,9 +76,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4 flex-shrink-0',
|
||||
responsive && 'mr-0 sm:mr-2',
|
||||
loading && 'animate-spin'
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
loading && 'animate-spin',
|
||||
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
||||
size !== 'icon' && 'mr-2'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useTransition } from 'react';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
|
||||
export function useCursor() {
|
||||
const [loading, startTransition] = useTransition();
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger
|
||||
.withOptions({ shallow: false, history: 'push' })
|
||||
.withOptions({ shallow: false, history: 'push', startTransition })
|
||||
.withDefault(0)
|
||||
);
|
||||
return {
|
||||
cursor,
|
||||
setCursor,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user