From 3f7db1935c4e36eed793dea6f3e3da46ce1d1063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Sun, 29 Oct 2023 21:14:12 +0100 Subject: [PATCH] simple event list and fix tables on settings --- README.md | 5 +- apps/web/src/components/DataTable.tsx | 123 ++++++++++-------- apps/web/src/components/Pagination.tsx | 40 ++++++ .../report/chart/ReportBarChart.tsx | 31 ++--- apps/web/src/components/ui/avatar.tsx | 6 +- apps/web/src/components/ui/table.tsx | 11 +- .../[organization]/[project]/[dashboard].tsx | 1 - .../pages/[organization]/[project]/events.tsx | 109 ++++++++++++++++ apps/web/src/pages/[organization]/index.tsx | 2 +- apps/web/src/server/api/root.ts | 2 + apps/web/src/server/api/routers/event.ts | 41 ++++++ apps/web/src/styles/globals.css | 18 --- apps/web/src/utils/date.ts | 20 ++- 13 files changed, 303 insertions(+), 106 deletions(-) create mode 100644 apps/web/src/components/Pagination.tsx create mode 100644 apps/web/src/pages/[organization]/[project]/events.tsx create mode 100644 apps/web/src/server/api/routers/event.ts diff --git a/README.md b/README.md index 43573ada..0bc93e26 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,13 @@ Mixan is a simple analytics tool for logging events on web and react-native. My ### GUI -* [ ] Fix tables on settings +* [X] Fix tables on settings * [ ] Rename event label * [ ] Real time data (mostly screen_views stats) * [ ] Active users (5min, 10min, 30min) * [X] Save report to a specific dashboard -* [ ] View events in a list +* [X] View events in a list + * [ ] Simple filters * [ ] View profiles in a list * [ ] Invite users * [ ] Drag n Drop reports on dashboard diff --git a/apps/web/src/components/DataTable.tsx b/apps/web/src/components/DataTable.tsx index 57f832a3..7a488188 100644 --- a/apps/web/src/components/DataTable.tsx +++ b/apps/web/src/components/DataTable.tsx @@ -1,66 +1,75 @@ -import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table" +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./ui/table"; -interface DataTableProps { - columns: ColumnDef[] - data: TData[] +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; } - -export function DataTable({ + +export function DataTable({ columns, data, -}: DataTableProps) { +}: DataTableProps) { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), - }) - - return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
- ) -} + }); + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ ); +} diff --git a/apps/web/src/components/Pagination.tsx b/apps/web/src/components/Pagination.tsx new file mode 100644 index 00000000..e38809d6 --- /dev/null +++ b/apps/web/src/components/Pagination.tsx @@ -0,0 +1,40 @@ +import { useMemo, useState } from "react"; +import { Button } from "./ui/button"; + +export function usePagination(take = 100) { + const [skip, setSkip] = useState(0); + return useMemo( + () => ({ + skip, + next: () => setSkip((p) => p + take), + prev: () => setSkip((p) => Math.max(p - take)), + take, + canPrev: skip > 0, + canNext: true, + }), + [skip, setSkip, take], + ); +} + +export function Pagination(props: ReturnType) { + return ( +
+ + +
+ ); +} diff --git a/apps/web/src/components/report/chart/ReportBarChart.tsx b/apps/web/src/components/report/chart/ReportBarChart.tsx index 3986e1d9..d048a2f3 100644 --- a/apps/web/src/components/report/chart/ReportBarChart.tsx +++ b/apps/web/src/components/report/chart/ReportBarChart.tsx @@ -10,11 +10,12 @@ import { getSortedRowModel, type SortingState, } from "@tanstack/react-table"; -import { memo, useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useElementSize } from "usehooks-ts"; import { useChartContext } from "./ChartProvider"; import { ChevronDown, ChevronUp } from "lucide-react"; import { cn } from "@/utils/cn"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; const columnHelper = createColumnHelper(); @@ -109,7 +110,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) { )}
- - + {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( - + ))} - + ))} - - + + {table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( - + ))} - + ))} - -
-
{flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ +
); diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx index 57c67b5b..440f497a 100644 --- a/apps/web/src/components/ui/avatar.tsx +++ b/apps/web/src/components/ui/avatar.tsx @@ -10,7 +10,7 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( -
+
+
+ + )) Table.displayName = "Table" @@ -70,7 +73,7 @@ const TableHead = React.forwardRef<
(({ className, ...props }, ref) => ( )) diff --git a/apps/web/src/pages/[organization]/[project]/[dashboard].tsx b/apps/web/src/pages/[organization]/[project]/[dashboard].tsx index 639caa05..131541d0 100644 --- a/apps/web/src/pages/[organization]/[project]/[dashboard].tsx +++ b/apps/web/src/pages/[organization]/[project]/[dashboard].tsx @@ -10,7 +10,6 @@ import { Chart } from "@/components/report/chart"; import { timeRanges } from "@/utils/constants"; import { type IChartRange } from "@/types"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { cn } from "@/utils/cn"; import { getRangeLabel } from "@/utils/getRangeLabel"; export const getServerSideProps = createServerSideProps(); diff --git a/apps/web/src/pages/[organization]/[project]/events.tsx b/apps/web/src/pages/[organization]/[project]/events.tsx new file mode 100644 index 00000000..b02ed554 --- /dev/null +++ b/apps/web/src/pages/[organization]/[project]/events.tsx @@ -0,0 +1,109 @@ +import { Container } from "@/components/Container"; +import { DataTable } from "@/components/DataTable"; +import { PageTitle } from "@/components/PageTitle"; +import { Pagination, usePagination } from "@/components/Pagination"; +import { MainLayout } from "@/components/layouts/MainLayout"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; +import { useOrganizationParams } from "@/hooks/useOrganizationParams"; +import { type RouterOutputs, api } from "@/utils/api"; +import { formatDateTime } from "@/utils/date"; +import { toDots } from "@/utils/object"; +import { AvatarImage } from "@radix-ui/react-avatar"; +import { createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; + +const columnHelper = + createColumnHelper(); + +export default function Events() { + const pagination = usePagination(); + const params = useOrganizationParams(); + const eventsQuery = api.event.list.useQuery( + { + projectSlug: params.project, + ...pagination, + }, + { + keepPreviousData: true, + }, + ); + const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]); + const columns = useMemo(() => { + return [ + columnHelper.accessor((row) => row.createdAt, { + id: "createdAt", + header: () => "Created At", + cell(info) { + return formatDateTime(info.getValue()); + }, + footer: () => "Created At", + }), + columnHelper.accessor((row) => row.name, { + id: "event", + header: () => "Event", + cell(info) { + return {info.getValue()}; + }, + footer: () => "Created At", + }), + columnHelper.accessor((row) => row.profile, { + id: "profile", + header: () => "Profile", + cell(info) { + const profile = info.getValue(); + return ( +
+ + {profile?.avatar && } + + {profile?.first_name?.at(0)} + + + {`${profile?.first_name} ${profile?.last_name ?? ""}`} +
+ ); + }, + footer: () => "Created At", + }), + columnHelper.accessor((row) => row.properties, { + id: "properties", + header: () => "Properties", + cell(info) { + const dots = toDots(info.getValue() as Record); + return ( + + + {Object.keys(dots).map((key) => { + return ( + + {key} + + {typeof dots[key] === "boolean" + ? dots[key] + ? "true" + : "false" + : dots[key]} + + + ); + })} + +
+ ); + }, + footer: () => "Created At", + }), + ]; + }, []); + return ( + + + Events + + + + + + ); +} diff --git a/apps/web/src/pages/[organization]/index.tsx b/apps/web/src/pages/[organization]/index.tsx index 6088cc93..61dbdfa4 100644 --- a/apps/web/src/pages/[organization]/index.tsx +++ b/apps/web/src/pages/[organization]/index.tsx @@ -24,7 +24,7 @@ export default function Home() { return ( - Reports + Projects
{projects.map((item) => ( diff --git a/apps/web/src/server/api/root.ts b/apps/web/src/server/api/root.ts index bcb17858..d93cea36 100644 --- a/apps/web/src/server/api/root.ts +++ b/apps/web/src/server/api/root.ts @@ -6,6 +6,7 @@ import { userRouter } from "./routers/user"; import { projectRouter } from "./routers/project"; import { clientRouter } from "./routers/client"; import { dashboardRouter } from "./routers/dashboard"; +import { eventRouter } from "./routers/event"; /** * This is the primary router for your server. @@ -20,6 +21,7 @@ export const appRouter = createTRPCRouter({ user: userRouter, project: projectRouter, client: clientRouter, + event: eventRouter, }); // export type definition of API diff --git a/apps/web/src/server/api/routers/event.ts b/apps/web/src/server/api/routers/event.ts new file mode 100644 index 00000000..c156f010 --- /dev/null +++ b/apps/web/src/server/api/routers/event.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; +import { db } from "@/server/db"; + +export const config = { + api: { + responseLimit: false, + }, +}; + +export const eventRouter = createTRPCRouter({ + list: protectedProcedure + .input( + z.object({ + cursor: z.string().optional(), + projectSlug: z.string(), + take: z.number().default(100), + skip: z.number().default(0), + }), + ) + .query(async ({ input: { take, skip, projectSlug } }) => { + const project = await db.project.findUniqueOrThrow({ + where: { + slug: projectSlug, + }, + }); + return db.event.findMany({ + take, + skip, + where: { + project_id: project.id, + }, + orderBy: { + createdAt: "desc", + }, + include: { + profile: true, + }, + }); + }), +}); diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 94225f1d..77ed4ecb 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -120,24 +120,6 @@ } } -table { - @apply w-fit border border-border; -} - -table.mini { - @apply text-xs; -} - -th, -td { - @apply border border-border px-4 py-2; -} - -th { - /* relative is for resizing */ - @apply relative text-left font-medium; -} - .resizer { position: absolute; right: 0; diff --git a/apps/web/src/utils/date.ts b/apps/web/src/utils/date.ts index f8aa4347..b4e36930 100644 --- a/apps/web/src/utils/date.ts +++ b/apps/web/src/utils/date.ts @@ -10,13 +10,23 @@ export function dateDifferanceInDays(date1: Date, date2: Date) { } export function getLocale() { - if(typeof navigator === 'undefined') { - return 'en-US' + if (typeof navigator === "undefined") { + return "en-US"; } - return navigator.language ?? 'en-US'; + return navigator.language ?? "en-US"; } export function formatDate(date: Date) { - return new Intl.DateTimeFormat(getLocale()).format(date) -} \ No newline at end of file + return new Intl.DateTimeFormat(getLocale()).format(date); +} + +export function formatDateTime(date: Date) { + return new Intl.DateTimeFormat(getLocale(), { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + }).format(date); +}