simple event list and fix tables on settings
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
40
apps/web/src/components/Pagination.tsx
Normal file
40
apps/web/src/components/Pagination.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 }
|
||||
@@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -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();
|
||||
|
||||
109
apps/web/src/pages/[organization]/[project]/events.tsx
Normal file
109
apps/web/src/pages/[organization]/[project]/events.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
41
apps/web/src/server/api/routers/event.ts
Normal file
41
apps/web/src/server/api/routers/event.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user