simple event list and fix tables on settings

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-29 21:14:12 +01:00
parent 48becb23dc
commit 3f7db1935c
13 changed files with 303 additions and 106 deletions

View File

@@ -16,12 +16,13 @@ Mixan is a simple analytics tool for logging events on web and react-native. My
### GUI ### GUI
* [ ] Fix tables on settings * [X] Fix tables on settings
* [ ] Rename event label * [ ] Rename event label
* [ ] Real time data (mostly screen_views stats) * [ ] Real time data (mostly screen_views stats)
* [ ] Active users (5min, 10min, 30min) * [ ] Active users (5min, 10min, 30min)
* [X] Save report to a specific dashboard * [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 * [ ] View profiles in a list
* [ ] Invite users * [ ] Invite users
* [ ] Drag n Drop reports on dashboard * [ ] Drag n Drop reports on dashboard

View File

@@ -1,23 +1,34 @@
import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" import {
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table" type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "./ui/table";
interface DataTableProps<TData, TValue> { interface DataTableProps<TData> {
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, any>[];
data: TData[] data: TData[];
} }
export function DataTable<TData, TValue>({ export function DataTable<TData>({
columns, columns,
data, data,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData>) {
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
return ( return (
<div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@@ -29,10 +40,10 @@ export function DataTable<TData, TValue>({
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext(),
)} )}
</TableHead> </TableHead>
) );
})} })}
</TableRow> </TableRow>
))} ))}
@@ -60,7 +71,5 @@ export function DataTable<TData, TValue>({
)} )}
</TableBody> </TableBody>
</Table> </Table>
</div> );
)
} }

View File

@@ -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<typeof usePagination>) {
return (
<div className="flex select-none items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => props.prev()}
disabled={!props.canPrev}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => props.next()}
disabled={!props.canNext}
>
Next
</Button>
</div>
);
}

View File

@@ -10,11 +10,12 @@ import {
getSortedRowModel, getSortedRowModel,
type SortingState, type SortingState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { memo, useEffect, useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useElementSize } from "usehooks-ts"; import { useElementSize } from "usehooks-ts";
import { useChartContext } from "./ChartProvider"; import { useChartContext } from "./ChartProvider";
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/utils/cn"; import { cn } from "@/utils/cn";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
const columnHelper = const columnHelper =
createColumnHelper<RouterOutputs["chart"]["chart"]["series"][number]>(); createColumnHelper<RouterOutputs["chart"]["chart"]["series"][number]>();
@@ -109,7 +110,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
</div> </div>
)} )}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table <Table
{...{ {...{
className: editMode ? '' : 'mini', className: editMode ? '' : 'mini',
style: { style: {
@@ -117,11 +118,11 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
}, },
}} }}
> >
<thead> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th <TableHead
key={header.id} key={header.id}
{...{ {...{
colSpan: header.colSpan, colSpan: header.colSpan,
@@ -161,16 +162,16 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
} }
: {})} : {})}
/> />
</th> </TableHead>
))} ))}
</tr> </TableRow>
))} ))}
</thead> </TableHeader>
<tbody> <TableBody>
{table.getRowModel().rows.map((row) => ( {table.getRowModel().rows.map((row) => (
<tr key={row.id}> <TableRow key={row.id}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td <TableCell
key={cell.id} key={cell.id}
{...{ {...{
style: { style: {
@@ -179,12 +180,12 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
}} }}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td> </TableCell>
))} ))}
</tr> </TableRow>
))} ))}
</tbody> </TableBody>
</table> </Table>
</div> </div>
</div> </div>
); );

View File

@@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", "relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full",
className className
)} )}
{...props} {...props}
@@ -37,7 +37,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted", "flex h-full w-full items-center justify-center rounded-full bg-primary text-white",
className className
)} )}
{...props} {...props}

View File

@@ -6,13 +6,16 @@ const Table = React.forwardRef<
HTMLTableElement, HTMLTableElement,
React.HTMLAttributes<HTMLTableElement> React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="border border-border rounded-md">
<div className="relative w-full overflow-auto "> <div className="relative w-full overflow-auto ">
<table <table
ref={ref} ref={ref}
className={cn("w-full caption-bottom text-sm", className)} className={cn("w-full caption-bottom text-sm [&.mini]:text-xs", className)}
{...props} {...props}
/> />
</div> </div>
</div>
)) ))
Table.displayName = "Table" Table.displayName = "Table"
@@ -70,7 +73,7 @@ const TableHead = React.forwardRef<
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", "p-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 shadow-[0_0_0_0.5px] shadow-border [.mini_&]:p-2",
className className
)} )}
{...props} {...props}
@@ -84,7 +87,7 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0 shadow-[0_0_0_0.5px] shadow-border [.mini_&]:p-2", className)}
{...props} {...props}
/> />
)) ))

View File

@@ -10,7 +10,6 @@ import { Chart } from "@/components/report/chart";
import { timeRanges } from "@/utils/constants"; import { timeRanges } from "@/utils/constants";
import { type IChartRange } from "@/types"; import { type IChartRange } from "@/types";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { cn } from "@/utils/cn";
import { getRangeLabel } from "@/utils/getRangeLabel"; import { getRangeLabel } from "@/utils/getRangeLabel";
export const getServerSideProps = createServerSideProps(); export const getServerSideProps = createServerSideProps();

View File

@@ -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<RouterOutputs["event"]["list"][number]>();
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 <span className="font-medium">{info.getValue()}</span>;
},
footer: () => "Created At",
}),
columnHelper.accessor((row) => row.profile, {
id: "profile",
header: () => "Profile",
cell(info) {
const profile = info.getValue();
return (
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
{profile?.avatar && <AvatarImage src={profile.avatar} />}
<AvatarFallback className="text-xs">
{profile?.first_name?.at(0)}
</AvatarFallback>
</Avatar>
{`${profile?.first_name} ${profile?.last_name ?? ""}`}
</div>
);
},
footer: () => "Created At",
}),
columnHelper.accessor((row) => row.properties, {
id: "properties",
header: () => "Properties",
cell(info) {
const dots = toDots(info.getValue() as Record<string, any>);
return (
<Table className="mini">
<TableBody>
{Object.keys(dots).map((key) => {
return (
<TableRow key={key}>
<TableCell className="font-medium">{key}</TableCell>
<TableCell>
{typeof dots[key] === "boolean"
? dots[key]
? "true"
: "false"
: dots[key]}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
},
footer: () => "Created At",
}),
];
}, []);
return (
<MainLayout>
<Container>
<PageTitle>Events</PageTitle>
<Pagination {...pagination} />
<DataTable data={events} columns={columns}></DataTable>
<Pagination {...pagination} />
</Container>
</MainLayout>
);
}

View File

@@ -24,7 +24,7 @@ export default function Home() {
return ( return (
<MainLayout> <MainLayout>
<Container> <Container>
<PageTitle>Reports</PageTitle> <PageTitle>Projects</PageTitle>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{projects.map((item) => ( {projects.map((item) => (
<Card key={item.id}> <Card key={item.id}>

View File

@@ -6,6 +6,7 @@ import { userRouter } from "./routers/user";
import { projectRouter } from "./routers/project"; import { projectRouter } from "./routers/project";
import { clientRouter } from "./routers/client"; import { clientRouter } from "./routers/client";
import { dashboardRouter } from "./routers/dashboard"; import { dashboardRouter } from "./routers/dashboard";
import { eventRouter } from "./routers/event";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
@@ -20,6 +21,7 @@ export const appRouter = createTRPCRouter({
user: userRouter, user: userRouter,
project: projectRouter, project: projectRouter,
client: clientRouter, client: clientRouter,
event: eventRouter,
}); });
// export type definition of API // export type definition of API

View File

@@ -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,
},
});
}),
});

View File

@@ -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 { .resizer {
position: absolute; position: absolute;
right: 0; right: 0;

View File

@@ -10,13 +10,23 @@ export function dateDifferanceInDays(date1: Date, date2: Date) {
} }
export function getLocale() { export function getLocale() {
if(typeof navigator === 'undefined') { if (typeof navigator === "undefined") {
return 'en-US' return "en-US";
} }
return navigator.language ?? 'en-US'; return navigator.language ?? "en-US";
} }
export function formatDate(date: Date) { export function formatDate(date: Date) {
return new Intl.DateTimeFormat(getLocale()).format(date) 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);
} }