add suspense for project cards
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||||
import FullWidthNavbar from '@/components/full-width-navbar';
|
import FullWidthNavbar from '@/components/full-width-navbar';
|
||||||
import { ProjectCard } from '@/components/projects/project-card';
|
import ProjectCard from '@/components/projects/project-card';
|
||||||
import SignOutButton from '@/components/sign-out-button';
|
import SignOutButton from '@/components/sign-out-button';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
|||||||
27
apps/dashboard/src/components/fade-in.tsx
Normal file
27
apps/dashboard/src/components/fade-in.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FadeIn({ className, children }: Props) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.classList.remove('opacity-0');
|
||||||
|
ref.current.classList.add('opacity-100');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('opacity-0 transition-opacity duration-500', className)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,36 +1,17 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
|
import withSuspense from '@/hocs/with-suspense';
|
||||||
import { shortNumber } from '@/hooks/useNumerFormatter';
|
import { shortNumber } from '@/hooks/useNumerFormatter';
|
||||||
|
import { Fallback } from '@radix-ui/react-avatar';
|
||||||
|
import { endOfMonth, startOfMonth } from 'date-fns';
|
||||||
import { escape } from 'sqlstring';
|
import { escape } from 'sqlstring';
|
||||||
|
|
||||||
import type { IServiceProject } from '@openpanel/db';
|
import type { IServiceProject } from '@openpanel/db';
|
||||||
import { chQuery } from '@openpanel/db';
|
import { chQuery } from '@openpanel/db';
|
||||||
|
|
||||||
import { ChartSSR } from '../chart-ssr';
|
import { ChartSSR } from '../chart-ssr';
|
||||||
|
import { FadeIn } from '../fade-in';
|
||||||
|
|
||||||
export async function ProjectCard({
|
function ProjectCard({ id, name, organizationSlug }: IServiceProject) {
|
||||||
id,
|
|
||||||
name,
|
|
||||||
organizationSlug,
|
|
||||||
}: IServiceProject) {
|
|
||||||
const [chart, [data]] = await Promise.all([
|
|
||||||
chQuery<{ value: number; date: string }>(
|
|
||||||
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM events WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`
|
|
||||||
),
|
|
||||||
chQuery<{ total: number; month: number; day: number }>(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
(
|
|
||||||
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)}
|
|
||||||
) as total,
|
|
||||||
(
|
|
||||||
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 month'
|
|
||||||
) as month,
|
|
||||||
(
|
|
||||||
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 day'
|
|
||||||
) as day
|
|
||||||
`
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// For some unknown reason I get when navigating back to this page when using <Link />
|
// For some unknown reason I get when navigating back to this page when using <Link />
|
||||||
// Should be solved: https://github.com/vercel/next.js/issues/61336
|
// Should be solved: https://github.com/vercel/next.js/issues/61336
|
||||||
// But still get the error
|
// But still get the error
|
||||||
@@ -41,31 +22,69 @@ export async function ProjectCard({
|
|||||||
>
|
>
|
||||||
<div className="font-medium">{name}</div>
|
<div className="font-medium">{name}</div>
|
||||||
<div className="-mx-4 aspect-[15/1]">
|
<div className="-mx-4 aspect-[15/1]">
|
||||||
<ChartSSR data={chart.map((d) => ({ ...d, date: new Date(d.date) }))} />
|
<Suspense>
|
||||||
|
<ProjectChart id={id} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-4 text-sm text-muted-foreground">
|
<div className="flex justify-between gap-4 text-sm text-muted-foreground">
|
||||||
<div className="font-medium">Visitors</div>
|
<div className="font-medium">Visitors</div>
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex flex-col gap-2 md:flex-row">
|
<Suspense>
|
||||||
<div>Total</div>
|
<ProjectMetrics id={id} />
|
||||||
<span className="font-medium text-black">
|
</Suspense>
|
||||||
{shortNumber('en')(data?.total)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 md:flex-row">
|
|
||||||
<div>Month</div>
|
|
||||||
<span className="font-medium text-black">
|
|
||||||
{shortNumber('en')(data?.month)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 md:flex-row">
|
|
||||||
<div>24h</div>
|
|
||||||
<span className="font-medium text-black">
|
|
||||||
{shortNumber('en')(data?.day)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ProjectChart({ id }: { id: string }) {
|
||||||
|
const chart = await chQuery<{ value: number; date: string }>(
|
||||||
|
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM events WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FadeIn className="h-full w-full">
|
||||||
|
<ChartSSR data={chart.map((d) => ({ ...d, date: new Date(d.date) }))} />
|
||||||
|
</FadeIn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metric({ value, label }: { value: React.ReactNode; label: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 md:flex-row">
|
||||||
|
<div>{label}</div>
|
||||||
|
<span className="font-medium text-black">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ProjectMetrics({ id }: { id: string }) {
|
||||||
|
const [metrics] = await chQuery<{
|
||||||
|
total: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
}>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)}
|
||||||
|
) as total,
|
||||||
|
(
|
||||||
|
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 month'
|
||||||
|
) as month,
|
||||||
|
(
|
||||||
|
SELECT count(DISTINCT profile_id) as count FROM events WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 day'
|
||||||
|
) as day
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FadeIn className="flex gap-4">
|
||||||
|
<Metric label="Total" value={shortNumber('en')(metrics?.total)} />
|
||||||
|
<Metric label="Month" value={shortNumber('en')(metrics?.month)} />
|
||||||
|
<Metric label="24h" value={shortNumber('en')(metrics?.day)} />
|
||||||
|
</FadeIn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectCard;
|
||||||
|
|||||||
Reference in New Issue
Block a user