diff --git a/src/app/default/api/route.ts b/src/app/default/api/route.ts new file mode 100644 index 0000000..bcbb1ad --- /dev/null +++ b/src/app/default/api/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { data } from "@/app/default/data"; + +export async function GET() { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 100)); + + return NextResponse.json({ + data, + total: data.length, + }); +} diff --git a/src/app/default/client.tsx b/src/app/default/client.tsx new file mode 100644 index 0000000..d16bf50 --- /dev/null +++ b/src/app/default/client.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { columns } from "./columns"; +import { filterFields } from "./constants"; +import { DataTable } from "./data-table"; +import { dataOptions } from "./query-options"; +import { useQueryStates } from "nuqs"; +import { searchParamsParser } from "./search-params"; + +export function Client() { + const [search] = useQueryStates(searchParamsParser); + const { data } = useQuery(dataOptions(search)); + + if (!data) return null; + + return ( + ({ + id: key, + value, + })) + .filter(({ value }) => value ?? undefined)} + /> + ); +} diff --git a/src/app/default/loading.tsx b/src/app/default/loading.tsx new file mode 100644 index 0000000..7aca0bc --- /dev/null +++ b/src/app/default/loading.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from "./skeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/default/page.tsx b/src/app/default/page.tsx index fa46dad..7a450e3 100644 --- a/src/app/default/page.tsx +++ b/src/app/default/page.tsx @@ -1,10 +1,8 @@ -import * as React from "react"; -import { columns } from "./columns"; -import { filterFields } from "./constants"; -import { data } from "./data"; -import { DataTable } from "./data-table"; +import { getQueryClient } from "@/providers/get-query-client"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; import { searchParamsCache } from "./search-params"; -import { Skeleton } from "./skeleton"; +import { dataOptions } from "./query-options"; +import { Client } from "./client"; export default async function Page({ searchParams, @@ -12,20 +10,14 @@ export default async function Page({ searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const search = searchParamsCache.parse(await searchParams); + const queryClient = getQueryClient(); + await queryClient.prefetchQuery(dataOptions(search)); + + const dehydratedState = dehydrate(queryClient); return ( - }> - ({ - id: key, - value, - })) - .filter(({ value }) => value ?? undefined)} - /> - + + + ); } diff --git a/src/app/default/query-options.ts b/src/app/default/query-options.ts new file mode 100644 index 0000000..cea6a42 --- /dev/null +++ b/src/app/default/query-options.ts @@ -0,0 +1,26 @@ +import { queryOptions } from "@tanstack/react-query"; +import type { ColumnSchema } from "./types"; + +interface ApiResponse { + data: ColumnSchema[]; + total: number; +} + +export const dataOptions = (search: Record) => + queryOptions({ + queryKey: ["default-data", search], + queryFn: async () => { + // Use absolute URL for server-side fetching + const baseUrl = typeof window === 'undefined' + ? `http://localhost:${process.env.PORT || 3001}` + : ''; + + const response = await fetch(`${baseUrl}/default/api`); + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + const result: ApiResponse = await response.json(); + return result; + }, + staleTime: 1000 * 60 * 5, // 5 minutes + }); diff --git a/src/app/infinite/client.tsx b/src/app/infinite/client.tsx index 70eeecc..33774ab 100644 --- a/src/app/infinite/client.tsx +++ b/src/app/infinite/client.tsx @@ -5,7 +5,7 @@ import { getLevelRowClassName } from "@/lib/request/level"; import { cn } from "@/lib/utils"; import { useInfiniteQuery } from "@tanstack/react-query"; import type { Table as TTable } from "@tanstack/react-table"; -import { useQueryState, useQueryStates } from "nuqs"; +import { useQueryState } from "nuqs"; import * as React from "react"; import { LiveRow } from "./_components/live-row"; import { columns } from "./columns"; @@ -13,10 +13,9 @@ import { filterFields as defaultFilterFields, sheetFields } from "./constants"; import { DataTableInfinite } from "./data-table-infinite"; import { dataOptions } from "./query-options"; import type { FacetMetadataSchema } from "./schema"; -import { searchParamsParser } from "./search-params"; +import { searchParamsParser, type SearchParamsType } from "./search-params"; -export function Client() { - const [search] = useQueryStates(searchParamsParser); +export function Client({ search }: { search: SearchParamsType }) { const { data, isFetching, @@ -76,6 +75,15 @@ export function Client() { }); }, [facets]); + const defaultColumnFilters = React.useMemo(() => { + return Object.entries(filter) + .map(([key, value]) => ({ + id: key, + value, + })) + .filter(({ value }) => value ?? undefined); + }, [filter]); + return ( ({ - id: key, - value, - })) - .filter(({ value }) => value ?? undefined)} + defaultColumnFilters={defaultColumnFilters} defaultColumnSorting={sort ? [sort] : undefined} defaultRowSelection={search.uuid ? { [search.uuid]: true } : undefined} // FIXME: make it configurable - TODO: use `columnHidden: boolean` in `filterFields` diff --git a/src/app/infinite/loading.tsx b/src/app/infinite/loading.tsx new file mode 100644 index 0000000..7aca0bc --- /dev/null +++ b/src/app/infinite/loading.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from "./skeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/infinite/page.tsx b/src/app/infinite/page.tsx index fd656b6..ccf620e 100644 --- a/src/app/infinite/page.tsx +++ b/src/app/infinite/page.tsx @@ -1,17 +1,23 @@ -import * as React from "react"; import { searchParamsCache } from "./search-params"; import { getQueryClient } from "@/providers/get-query-client"; import { dataOptions } from "./query-options"; import { Client } from "./client"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; export default async function Page({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { - const search = searchParamsCache.parse(await searchParams); + const search = await searchParamsCache.parse(searchParams); const queryClient = getQueryClient(); await queryClient.prefetchInfiniteQuery(dataOptions(search)); - return ; + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); } diff --git a/src/app/infinite/query-options.ts b/src/app/infinite/query-options.ts index f666b1e..c3d1149 100644 --- a/src/app/infinite/query-options.ts +++ b/src/app/infinite/query-options.ts @@ -49,7 +49,7 @@ export const dataOptions = (search: SearchParamsType) => { json, ); }, - initialPageParam: { cursor: new Date().getTime(), direction: "next" }, + initialPageParam: { cursor: search.cursor.getTime(), direction: "next" }, getPreviousPageParam: (firstPage, _pages) => { if (!firstPage.prevCursor) return null; return { cursor: firstPage.prevCursor, direction: "prev" }; @@ -60,5 +60,6 @@ export const dataOptions = (search: SearchParamsType) => { }, refetchOnWindowFocus: false, placeholderData: keepPreviousData, - }); + staleTime: 1000 * 60 * 5, // 5 minutes + }) }; diff --git a/src/app/infinite/skeleton.tsx b/src/app/infinite/skeleton.tsx new file mode 100644 index 0000000..5a1331f --- /dev/null +++ b/src/app/infinite/skeleton.tsx @@ -0,0 +1,237 @@ +import { Skeleton as DefaultSkeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/custom/table"; +import { cn } from "@/lib/utils"; + +export function Skeleton() { + return ( +
+ {/* Left Sidebar - Filters */} +
+
+
+

Filters

+
+
+
+ {/* Filter Controls Skeleton */} +
+ {/* Time Range */} +
+ + +
+ + {/* Level Filters */} +
+ +
+ + + +
+
+ + {/* Other Filters */} + {Array.from({ length: 9 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+ {/* Socials Footer Skeleton */} +
+ + + +
+
+
+ + {/* Main Content Area */} +
+ {/* Top Bar */} +
+ {/* Search Command Palette */} + + + {/* Toolbar */} +
+
+ +
+
+ +
+ + +
+
+
+ + {/* Timeline Chart Skeleton */} +
+
+ {/* Chart bars area - positioned at top */} +
+ {Array.from({ length: 40 }).map((_, i) => ( + + ))} +
+ {/* Axis labels area - positioned at bottom */} +
+ + + +
+
+
+
+ + {/* Data Table */} +
+ + + *]:border-t [&>:not(:last-child)]:border-r", + )} + > + {/* Level */} + + + + {/* Date */} + + + + {/* Status */} + + + + {/* Method */} + + + + {/* Host */} + + + + {/* Pathname */} + + + + {/* Latency */} + + + + {/* Region */} + + + + {/* Timing Phases */} + + + + + + + {Array.from({ length: 12 }).map((_, i) => ( + + {/* Level */} + + + + {/* Date */} + + + + {/* Status */} + + + + {/* Method */} + + + + {/* Host */} + + + + {/* Pathname */} + + + + {/* Latency */} + + + + {/* Region */} + + + + {/* Timing Phases */} + +
+ + + + + +
+
+
+ ))} + {/* Load More Button Row */} + + + + + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/light/page.tsx b/src/app/light/page.tsx index 07ff9b9..d5b13f0 100644 --- a/src/app/light/page.tsx +++ b/src/app/light/page.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import { Client } from "./client"; import { dataOptions } from "./query-options"; import { searchParamsCache } from "./search-params"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; export default async function Page({ searchParams, @@ -14,5 +15,11 @@ export default async function Page({ const queryClient = getQueryClient(); await queryClient.prefetchInfiniteQuery(dataOptions(search)); - return ; + const dehydratedState = dehydrate(queryClient); + + return ( + + + + ); } diff --git a/src/app/light/query-options.ts b/src/app/light/query-options.ts index 3e9a00b..5ad7c22 100644 --- a/src/app/light/query-options.ts +++ b/src/app/light/query-options.ts @@ -36,5 +36,6 @@ export const dataOptions = (search: SearchParamsType) => { }, refetchOnWindowFocus: false, placeholderData: keepPreviousData, + staleTime: 1000 * 60 * 5, // 5 minutes }); };