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
* [ ] 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

View File

@@ -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<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[];
data: TData[];
}
export function DataTable<TData, TValue>({
export function DataTable<TData>({
columns,
data,
}: DataTableProps<TData, TValue>) {
}: DataTableProps<TData>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
});
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}

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,
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<RouterOutputs["chart"]["chart"]["series"][number]>();
@@ -109,7 +110,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
</div>
)}
<div className="overflow-x-auto">
<table
<Table
{...{
className: editMode ? '' : 'mini',
style: {
@@ -117,11 +118,11 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
},
}}
>
<thead>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
<TableHead
key={header.id}
{...{
colSpan: header.colSpan,
@@ -161,16 +162,16 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
}
: {})}
/>
</th>
</TableHead>
))}
</tr>
</TableRow>
))}
</thead>
<tbody>
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<td
<TableCell
key={cell.id}
{...{
style: {
@@ -179,12 +180,12 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
</TableCell>
))}
</tr>
</TableRow>
))}
</tbody>
</table>
</TableBody>
</Table>
</div>
</div>
);

View File

@@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
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
)}
{...props}
@@ -37,7 +37,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
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
)}
{...props}
@@ -45,4 +45,4 @@ const AvatarFallback = React.forwardRef<
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -6,13 +6,16 @@ const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<div className="border border-border rounded-md">
<div className="relative w-full overflow-auto ">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn("w-full caption-bottom text-sm [&.mini]:text-xs", className)}
{...props}
/>
</div>
</div>
))
Table.displayName = "Table"
@@ -70,7 +73,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
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
)}
{...props}
@@ -84,7 +87,7 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
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}
/>
))

View File

@@ -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();

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 (
<MainLayout>
<Container>
<PageTitle>Reports</PageTitle>
<PageTitle>Projects</PageTitle>
<div className="grid grid-cols-2 gap-4">
{projects.map((item) => (
<Card key={item.id}>

View File

@@ -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

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

View File

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