oh lord. prettier eslint and all that

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-11-02 20:24:41 +01:00
parent e1f37b439e
commit 107feda4ad
121 changed files with 1856 additions and 1684 deletions

16
.vscode/settings.json vendored
View File

@@ -2,11 +2,7 @@
"editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[handlebars]": { "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
"editor.formatOnSave": false,
"editor.codeActionsOnSave": { "source.fixAll.eslint": false }
},
"files.associations": { "*.hbs": "handlebars" },
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ "pattern": "apps/*/" }, { "pattern": "apps/*/" },
{ "pattern": "packages/*/" }, { "pattern": "packages/*/" },
@@ -14,10 +10,8 @@
], ],
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"[yaml]": { "typescript.preferences.autoImportFileExcludePatterns": [
"editor.insertSpaces": true, "next/router.d.ts",
"editor.tabSize": 2, "next/dist/client/router.d.ts"
"editor.autoIndent": "advanced" ]
},
"editor.inlineSuggest.enabled": true
} }

View File

@@ -1,38 +0,0 @@
/** @type {import("eslint").Linter.Config} */
const config = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
plugins: ["@typescript-eslint"],
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
],
rules: {
// These opinionated rules are enabled in stylistic-type-checked above.
// Feel free to reconfigure them to your own preference.
"@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
},
};
module.exports = config;

View File

@@ -24,17 +24,25 @@ RUN apt update \
&& rm n \ && rm n \
&& npm install -g n && npm install -g n
WORKDIR /app
COPY package.json package.json
COPY pnpm-lock.yaml pnpm-lock.yaml
COPY pnpm-workspace.yaml pnpm-workspace.yaml
COPY apps/web/package.json apps/web/package.json
COPY packages/types/package.json packages/types/package.json
FROM base AS build FROM base AS build
COPY . /app
WORKDIR /app/apps/web WORKDIR /app/apps/web
RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm install --frozen-lockfile --ignore-scripts
COPY . /app
RUN pnpm dlx prisma generate RUN pnpm dlx prisma generate
RUN pnpm run build RUN pnpm run build
FROM base AS prod FROM base AS prod
COPY . /app WORKDIR /app/apps/web
WORKDIR /app
RUN pnpm install --frozen-lockfile --prod --ignore-scripts RUN pnpm install --frozen-lockfile --prod --ignore-scripts
RUN pnpm dlx prisma generate
FROM base AS runner FROM base AS runner
COPY --from=build /app/package.json /app/package.json COPY --from=build /app/package.json /app/package.json
@@ -42,6 +50,5 @@ COPY --from=prod /app/node_modules /app/node_modules
COPY --from=build /app/apps/web /app/apps/web COPY --from=build /app/apps/web /app/apps/web
COPY --from=prod /app/apps/web/node_modules /app/apps/web/node_modules COPY --from=prod /app/apps/web/node_modules /app/apps/web/node_modules
WORKDIR /app/apps/web WORKDIR /app/apps/web
RUN pnpm dlx prisma generate
EXPOSE 3000 EXPOSE 3000
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

View File

@@ -7,7 +7,9 @@ await import('./src/env.mjs');
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
reactStrictMode: true, reactStrictMode: true,
transpilePackages: [],
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
/** /**
* If you are using `appDir` then you must comment the below `i18n` config out. * If you are using `appDir` then you must comment the below `i18n` config out.
* *

View File

@@ -8,7 +8,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"", "format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@@ -62,20 +62,23 @@
"devDependencies": { "devDependencies": {
"@mixan/eslint-config": "workspace:*", "@mixan/eslint-config": "workspace:*",
"@mixan/prettier-config": "workspace:*", "@mixan/prettier-config": "workspace:*",
"@mixan/tsconfig": "workspace:*",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/node": "^18.16.0", "@types/node": "^18.16.0",
"@types/ramda": "^0.29.6", "@types/ramda": "^0.29.6",
"@types/react": "^18.2.20", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/react-syntax-highlighter": "^15.5.9", "@types/react-syntax-highlighter": "^15.5.9",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.48.0", "eslint": "^8.48.0",
"eslint-config-next": "^13.5.4",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.1", "prettier-plugin-tailwindcss": "^0.5.1",
"prisma": "^5.1.1", "prisma": "^5.1.1",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.2.0" "typescript": "^5.2.2"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.21.0" "initVersion": "7.21.0"
@@ -84,6 +87,7 @@
"root": true, "root": true,
"extends": [ "extends": [
"@mixan/eslint-config/base", "@mixan/eslint-config/base",
"@mixan/eslint-config/nextjs",
"@mixan/eslint-config/react" "@mixan/eslint-config/react"
] ]
}, },

View File

@@ -1,6 +0,0 @@
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').options} */
const config = {
plugins: ['prettier-plugin-tailwindcss'],
};
export default config;

View File

@@ -1,3 +1,3 @@
import AutoSizer from "react-virtualized-auto-sizer"; import AutoSizer from 'react-virtualized-auto-sizer';
export { AutoSizer } export { AutoSizer };

View File

@@ -1,8 +1,11 @@
import { type HtmlProps } from "@/types"; import type { HtmlProps } from '@/types';
import { cn } from "@/utils/cn"; import { cn } from '@/utils/cn';
export function ButtonContainer({className,...props}: HtmlProps<HTMLDivElement>) { export function ButtonContainer({
className,
...props
}: HtmlProps<HTMLDivElement>) {
return ( return (
<div className={cn("flex justify-between mt-6", className)} {...props} /> <div className={cn('flex justify-between mt-6', className)} {...props} />
); );
} }

View File

@@ -1,9 +1,5 @@
import { type HtmlProps } from "@/types"; import type { HtmlProps } from '@/types';
export function Card({children}: HtmlProps<HTMLDivElement>) { export function Card({ children }: HtmlProps<HTMLDivElement>) {
return ( return <div className="border border-border rounded">{children}</div>;
<div className="border border-border rounded"> }
{children}
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { type HtmlProps } from "@/types"; import type { HtmlProps } from '@/types';
import { cn } from "@/utils/cn"; import { cn } from '@/utils/cn';
type ColorSquareProps = HtmlProps<HTMLDivElement>; type ColorSquareProps = HtmlProps<HTMLDivElement>;
@@ -7,8 +7,8 @@ export function ColorSquare({ children, className }: ColorSquareProps) {
return ( return (
<div <div
className={cn( className={cn(
"flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-purple-500 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]", 'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-purple-500 text-xs font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
className, className
)} )}
> >
{children} {children}

View File

@@ -1,8 +1,11 @@
import { type HtmlProps } from "@/types"; import type { HtmlProps } from '@/types';
import { cn } from "@/utils/cn"; import { cn } from '@/utils/cn';
export function Container({className,...props}: HtmlProps<HTMLDivElement>) { export function Container({ className, ...props }: HtmlProps<HTMLDivElement>) {
return ( return (
<div className={cn("mx-auto w-full max-w-4xl px-4", className)} {...props} /> <div
className={cn('mx-auto w-full max-w-4xl px-4', className)}
{...props}
/>
); );
} }

View File

@@ -1,10 +1,10 @@
import { cn } from "@/utils/cn"; import { cn } from '@/utils/cn';
type ContentHeaderProps = { interface ContentHeaderProps {
title: string; title: string;
text: string; text: string;
children?: React.ReactNode; children?: React.ReactNode;
}; }
export function ContentHeader({ title, text, children }: ContentHeaderProps) { export function ContentHeader({ title, text, children }: ContentHeaderProps) {
return ( return (
@@ -18,12 +18,12 @@ export function ContentHeader({ title, text, children }: ContentHeaderProps) {
); );
} }
type ContentSectionProps = { interface ContentSectionProps {
title: string; title: string;
text?: string | React.ReactNode; text?: string | React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
asCol?: boolean; asCol?: boolean;
}; }
export function ContentSection({ export function ContentSection({
title, title,
@@ -34,8 +34,8 @@ export function ContentSection({
return ( return (
<div <div
className={cn( className={cn(
"first:pt-0] flex py-6", 'first:pt-0] flex py-6',
asCol ? "col flex" : "justify-between", asCol ? 'col flex' : 'justify-between'
)} )}
> >
{title && ( {title && (

View File

@@ -1,9 +1,10 @@
import { import {
type ColumnDef,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from '@tanstack/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,17 +12,14 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "./ui/table"; } from './ui/table';
interface DataTableProps<TData> { interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[]; columns: ColumnDef<TData, any>[];
data: TData[]; data: TData[];
} }
export function DataTable<TData>({ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
columns,
data,
}: DataTableProps<TData>) {
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
@@ -40,7 +38,7 @@ export function DataTable<TData>({
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext(), header.getContext()
)} )}
</TableHead> </TableHead>
); );
@@ -53,7 +51,7 @@ export function DataTable<TData>({
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && 'selected'}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>

View File

@@ -1,4 +1,5 @@
import { cloneElement } from "react"; import { cloneElement } from 'react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -7,19 +8,24 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "./ui/dropdown-menu"; } from './ui/dropdown-menu';
type DropdownProps<Value> = { interface DropdownProps<Value> {
children: React.ReactNode; children: React.ReactNode;
label?: string; label?: string;
items: Array<{ items: {
label: string; label: string;
value: Value; value: Value;
}>; }[];
onChange?: (value: Value) => void; onChange?: (value: Value) => void;
}; }
export function Dropdown<Value extends string>({ children, label, items, onChange }: DropdownProps<Value>) { export function Dropdown<Value extends string>({
children,
label,
items,
onChange,
}: DropdownProps<Value>) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger> <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>

View File

@@ -1,4 +1,4 @@
import { type HtmlProps } from "@/types"; import type { HtmlProps } from '@/types';
type PageTitleProps = HtmlProps<HTMLDivElement>; type PageTitleProps = HtmlProps<HTMLDivElement>;

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from 'react';
import { Button } from "./ui/button";
import { Button } from './ui/button';
export function usePagination(take = 100) { export function usePagination(take = 100) {
const [skip, setSkip] = useState(0); const [skip, setSkip] = useState(0);
@@ -12,11 +13,11 @@ export function usePagination(take = 100) {
canPrev: skip > 0, canPrev: skip > 0,
canNext: true, canNext: true,
}), }),
[skip, setSkip, take], [skip, setSkip, take]
); );
} }
export type PaginationProps = ReturnType<typeof usePagination> export type PaginationProps = ReturnType<typeof usePagination>;
export function Pagination(props: PaginationProps) { export function Pagination(props: PaginationProps) {
return ( return (

View File

@@ -1,12 +1,12 @@
import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import ts from "react-syntax-highlighter/dist/cjs/languages/hljs/typescript"; import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript';
import docco from "react-syntax-highlighter/dist/cjs/styles/hljs/docco"; import docco from 'react-syntax-highlighter/dist/cjs/styles/hljs/docco';
SyntaxHighlighter.registerLanguage("typescript", ts); SyntaxHighlighter.registerLanguage('typescript', ts);
type SyntaxProps = { interface SyntaxProps {
code: string; code: string;
}; }
export default function Syntax({ code }: SyntaxProps) { export default function Syntax({ code }: SyntaxProps) {
return <SyntaxHighlighter style={docco}>{code}</SyntaxHighlighter>; return <SyntaxHighlighter style={docco}>{code}</SyntaxHighlighter>;

View File

@@ -1,21 +1,15 @@
import { type HtmlProps } from "@/types"; import type { HtmlProps } from '@/types';
type WithSidebarProps = HtmlProps<HTMLDivElement> type WithSidebarProps = HtmlProps<HTMLDivElement>;
export function WithSidebar({children}: WithSidebarProps) { export function WithSidebar({ children }: WithSidebarProps) {
return ( return (
<div className="grid grid-cols-[200px_minmax(0,1fr)] gap-8"> <div className="grid grid-cols-[200px_minmax(0,1fr)] gap-8">{children}</div>
{children} );
</div>
)
} }
type SidebarProps = HtmlProps<HTMLDivElement> type SidebarProps = HtmlProps<HTMLDivElement>;
export function Sidebar({children}: SidebarProps) { export function Sidebar({ children }: SidebarProps) {
return ( return <div className="flex flex-col gap-1">{children}</div>;
<div className="flex flex-col gap-1"> }
{children}
</div>
)
}

View File

@@ -1,3 +1,11 @@
import { useRefetchActive } from '@/hooks/useRefetchActive';
import { pushModal, showConfirm } from '@/modals';
import type { IClientWithProject } from '@/types';
import { api } from '@/utils/api';
import { clipboard } from '@/utils/clipboard';
import { MoreHorizontal } from 'lucide-react';
import { Button } from '../ui/button';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -5,27 +13,20 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from '../ui/dropdown-menu';
import { Button } from "../ui/button"; import { toast } from '../ui/use-toast';
import { MoreHorizontal } from "lucide-react";
import { pushModal, showConfirm } from "@/modals";
import { type IClientWithProject } from "@/types";
import { clipboard } from "@/utils/clipboard";
import { api } from "@/utils/api";
import { useRefetchActive } from "@/hooks/useRefetchActive";
import { toast } from "../ui/use-toast";
export function ClientActions({ id }: IClientWithProject) { export function ClientActions({ id }: IClientWithProject) {
const refetch = useRefetchActive() const refetch = useRefetchActive();
const deletion = api.client.remove.useMutation({ const deletion = api.client.remove.useMutation({
onSuccess() { onSuccess() {
toast({ toast({
title: 'Success', title: 'Success',
description: 'Client revoked, incoming requests will be rejected.', description: 'Client revoked, incoming requests will be rejected.',
}) });
refetch() refetch();
} },
}) });
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -41,7 +42,7 @@ export function ClientActions({ id }: IClientWithProject) {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
pushModal("EditClient", { id }); pushModal('EditClient', { id });
}} }}
> >
Edit Edit
@@ -56,9 +57,9 @@ export function ClientActions({ id }: IClientWithProject) {
onConfirm() { onConfirm() {
deletion.mutate({ deletion.mutate({
id, id,
}) });
} },
}) });
}} }}
> >
Revoke Revoke

View File

@@ -1,12 +1,13 @@
import { formatDate } from "@/utils/date"; import type { IClientWithProject } from '@/types';
import { type ColumnDef } from "@tanstack/react-table"; import { formatDate } from '@/utils/date';
import { type IClientWithProject } from "@/types"; import type { ColumnDef } from '@tanstack/react-table';
import { ClientActions } from "./ClientActions";
import { ClientActions } from './ClientActions';
export const columns: ColumnDef<IClientWithProject>[] = [ export const columns: ColumnDef<IClientWithProject>[] = [
{ {
accessorKey: "name", accessorKey: 'name',
header: "Name", header: 'Name',
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<div> <div>
@@ -19,25 +20,25 @@ export const columns: ColumnDef<IClientWithProject>[] = [
}, },
}, },
{ {
accessorKey: "id", accessorKey: 'id',
header: "Client ID", header: 'Client ID',
}, },
{ {
accessorKey: "secret", accessorKey: 'secret',
header: "Secret", header: 'Secret',
cell: () => <div className="italic text-muted-foreground">Hidden</div>, cell: () => <div className="italic text-muted-foreground">Hidden</div>,
}, },
{ {
accessorKey: "createdAt", accessorKey: 'createdAt',
header: "Created at", header: 'Created at',
cell({ row }) { cell({ row }) {
const date = row.original.createdAt; const date = row.original.createdAt;
return <div>{formatDate(date)}</div>; return <div>{formatDate(date)}</div>;
}, },
}, },
{ {
id: "actions", id: 'actions',
header: "Actions", header: 'Actions',
cell: ({ row }) => <ClientActions {...row.original} />, cell: ({ row }) => <ClientActions {...row.original} />,
}, },
]; ];

View File

@@ -1,66 +1,70 @@
import { useMemo } from "react"; import { useMemo } from 'react';
import { DataTable } from "@/components/DataTable"; import { DataTable } from '@/components/DataTable';
import { type PaginationProps, Pagination } from "@/components/Pagination"; import { Pagination } from '@/components/Pagination';
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import type { PaginationProps } from '@/components/Pagination';
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { type RouterOutputs } from "@/utils/api"; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
import { formatDateTime } from "@/utils/date"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { toDots } from "@/utils/object"; import type { RouterOutputs } from '@/utils/api';
import { AvatarImage } from "@radix-ui/react-avatar"; import { formatDateTime } from '@/utils/date';
import { createColumnHelper } from "@tanstack/react-table"; import { toDots } from '@/utils/object';
import Link from "next/link"; import { AvatarImage } from '@radix-ui/react-avatar';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { createColumnHelper } from '@tanstack/react-table';
import Link from 'next/link';
const columnHelper = const columnHelper =
createColumnHelper<RouterOutputs["event"]["list"][number]>(); createColumnHelper<RouterOutputs['event']['list'][number]>();
type EventsTableProps = { interface EventsTableProps {
data: RouterOutputs["event"]["list"]; data: RouterOutputs['event']['list'];
pagination: PaginationProps; pagination: PaginationProps;
}; }
export function EventsTable({ data, pagination }: EventsTableProps) { export function EventsTable({ data, pagination }: EventsTableProps) {
const params = useOrganizationParams() const params = useOrganizationParams();
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
columnHelper.accessor((row) => row.createdAt, { columnHelper.accessor((row) => row.createdAt, {
id: "createdAt", id: 'createdAt',
header: () => "Created At", header: () => 'Created At',
cell(info) { cell(info) {
return formatDateTime(info.getValue()); return formatDateTime(info.getValue());
}, },
footer: () => "Created At", footer: () => 'Created At',
}), }),
columnHelper.accessor((row) => row.name, { columnHelper.accessor((row) => row.name, {
id: "event", id: 'event',
header: () => "Event", header: () => 'Event',
cell(info) { cell(info) {
return <span className="font-medium">{info.getValue()}</span>; return <span className="font-medium">{info.getValue()}</span>;
}, },
footer: () => "Created At", footer: () => 'Created At',
}), }),
columnHelper.accessor((row) => row.profile, { columnHelper.accessor((row) => row.profile, {
id: "profile", id: 'profile',
header: () => "Profile", header: () => 'Profile',
cell(info) { cell(info) {
const profile = info.getValue(); const profile = info.getValue();
return ( return (
<Link href={`/${params.organization}/${params.project}/profiles/${profile?.id}`} className="flex items-center gap-2"> <Link
href={`/${params.organization}/${params.project}/profiles/${profile?.id}`}
className="flex items-center gap-2"
>
<Avatar className="h-6 w-6"> <Avatar className="h-6 w-6">
{profile?.avatar && <AvatarImage src={profile.avatar} />} {profile?.avatar && <AvatarImage src={profile.avatar} />}
<AvatarFallback className="text-xs"> <AvatarFallback className="text-xs">
{profile?.first_name?.at(0)} {profile?.first_name?.at(0)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
{`${profile?.first_name} ${profile?.last_name ?? ""}`} {`${profile?.first_name} ${profile?.last_name ?? ''}`}
</Link> </Link>
); );
}, },
footer: () => "Created At", footer: () => 'Created At',
}), }),
columnHelper.accessor((row) => row.properties, { columnHelper.accessor((row) => row.properties, {
id: "properties", id: 'properties',
header: () => "Properties", header: () => 'Properties',
cell(info) { cell(info) {
const dots = toDots(info.getValue() as Record<string, any>); const dots = toDots(info.getValue() as Record<string, any>);
return ( return (
@@ -71,10 +75,10 @@ export function EventsTable({ data, pagination }: EventsTableProps) {
<TableRow key={key}> <TableRow key={key}>
<TableCell className="font-medium">{key}</TableCell> <TableCell className="font-medium">{key}</TableCell>
<TableCell> <TableCell>
{typeof dots[key] === "boolean" {typeof dots[key] === 'boolean'
? dots[key] ? dots[key]
? "true" ? 'true'
: "false" : 'false'
: dots[key]} : dots[key]}
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -84,7 +88,7 @@ export function EventsTable({ data, pagination }: EventsTableProps) {
</Table> </Table>
); );
}, },
footer: () => "Created At", footer: () => 'Created At',
}), }),
]; ];
}, []); }, []);

View File

@@ -1,4 +1,6 @@
type InputErrorProps = { message?: string }; interface InputErrorProps {
message?: string;
}
export function InputError({ message }: InputErrorProps) { export function InputError({ message }: InputErrorProps) {
if (!message) { if (!message) {

View File

@@ -1,18 +1,24 @@
import { forwardRef } from "react"; import { forwardRef } from 'react';
import { Input, type InputProps } from "../ui/input";
import { Label } from "../ui/label"; import { Input } from '../ui/input';
import type { InputProps } from '../ui/input';
import { Label } from '../ui/label';
type InputWithLabelProps = InputProps & { type InputWithLabelProps = InputProps & {
label: string; label: string;
}; };
export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(({ label, ...props }, ref) => { export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
return ( ({ label, ...props }, ref) => {
<div> return (
<Label htmlFor={label} className="block mb-2">{label}</Label> <div>
<Input ref={ref} id={label} {...props} /> <Label htmlFor={label} className="block mb-2">
</div> {label}
); </Label>
}); <Input ref={ref} id={label} {...props} />
</div>
);
}
);
InputWithLabel.displayName = "InputWithLabel"; InputWithLabel.displayName = 'InputWithLabel';

View File

@@ -1,12 +1,13 @@
import { NavbarUserDropdown } from "../navbar/NavbarUserDropdown"; import Link from 'next/link';
import { NavbarMenu } from "../navbar/NavbarMenu";
import { Container } from "../Container";
import Link from "next/link";
type MainLayoutProps = { import { Container } from '../Container';
import { NavbarMenu } from '../navbar/NavbarMenu';
import { NavbarUserDropdown } from '../navbar/NavbarUserDropdown';
interface MainLayoutProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}; }
export function MainLayout({ children, className }: MainLayoutProps) { export function MainLayout({ children, className }: MainLayoutProps) {
return ( return (
@@ -14,7 +15,9 @@ export function MainLayout({ children, className }: MainLayoutProps) {
<div className="h-2 w-full bg-gradient-to-r from-blue-900 to-purple-600"></div> <div className="h-2 w-full bg-gradient-to-r from-blue-900 to-purple-600"></div>
<nav className="border-b border-border"> <nav className="border-b border-border">
<Container className="flex h-20 items-center justify-between "> <Container className="flex h-20 items-center justify-between ">
<Link href="/" className="text-3xl">mixan</Link> <Link href="/" className="text-3xl">
mixan
</Link>
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<NavbarMenu /> <NavbarMenu />
<div> <div>

View File

@@ -1,25 +1,29 @@
import { Container } from "../Container"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { MainLayout } from "./MainLayout"; import { cn } from '@/utils/cn';
import { usePathname } from "next/navigation"; import Link from 'next/link';
import { Sidebar, WithSidebar } from "../WithSidebar"; import { usePathname } from 'next/navigation';
import Link from "next/link";
import { cn } from "@/utils/cn";
import { PageTitle } from "../PageTitle";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
type SettingsLayoutProps = { import { Container } from '../Container';
import { PageTitle } from '../PageTitle';
import { Sidebar, WithSidebar } from '../WithSidebar';
import { MainLayout } from './MainLayout';
interface SettingsLayoutProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}; }
export function SettingsLayout({ children, className }: SettingsLayoutProps) { export function SettingsLayout({ children, className }: SettingsLayoutProps) {
const params = useOrganizationParams(); const params = useOrganizationParams();
const pathname = usePathname(); const pathname = usePathname();
const links = [ const links = [
{ href: `/${params.organization}/settings/organization`, label: "Organization" }, {
{ href: `/${params.organization}/settings/projects`, label: "Projects" }, href: `/${params.organization}/settings/organization`,
{ href: `/${params.organization}/settings/clients`, label: "Clients" }, label: 'Organization',
{ href: `/${params.organization}/settings/profile`, label: "Profile" }, },
{ href: `/${params.organization}/settings/projects`, label: 'Projects' },
{ href: `/${params.organization}/settings/clients`, label: 'Clients' },
{ href: `/${params.organization}/settings/profile`, label: 'Profile' },
]; ];
return ( return (
<MainLayout> <MainLayout>
@@ -32,19 +36,17 @@ export function SettingsLayout({ children, className }: SettingsLayoutProps) {
key={href} key={href}
href={href} href={href}
className={cn( className={cn(
"p-4 py-3 leading-none rounded-lg transition-colors", 'p-4 py-3 leading-none rounded-lg transition-colors',
pathname.startsWith(href) pathname.startsWith(href)
? "bg-slate-100" ? 'bg-slate-100'
: "hover:bg-slate-100", : 'hover:bg-slate-100'
)} )}
> >
{label} {label}
</Link> </Link>
))} ))}
</Sidebar> </Sidebar>
<div className={cn('flex flex-col', className)}> <div className={cn('flex flex-col', className)}>{children}</div>
{children}
</div>
</WithSidebar> </WithSidebar>
</Container> </Container>
</MainLayout> </MainLayout>

View File

@@ -1,5 +1,4 @@
import { LineChart } from "lucide-react"; import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -8,9 +7,10 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from '@/components/ui/dropdown-menu';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import Link from "next/link"; import { LineChart } from 'lucide-react';
import Link from 'next/link';
export function NavbarCreate() { export function NavbarCreate() {
const params = useOrganizationParams(); const params = useOrganizationParams();

View File

@@ -1,14 +1,23 @@
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import Link from "next/link"; import Link from 'next/link';
import { NavbarCreate } from "./NavbarCreate";
import { NavbarCreate } from './NavbarCreate';
export function NavbarMenu() { export function NavbarMenu() {
const params = useOrganizationParams() const params = useOrganizationParams();
return ( return (
<div className="flex gap-6 items-center"> <div className="flex gap-6 items-center">
<Link href={`/${params.organization}`}>Home</Link> <Link href={`/${params.organization}`}>Home</Link>
{params.project && <Link href={`/${params.organization}/${params.project}/events`}>Events</Link>} {params.project && (
{params.project && <Link href={`/${params.organization}/${params.project}/profiles`}>Profiles</Link>} <Link href={`/${params.organization}/${params.project}/events`}>
Events
</Link>
)}
{params.project && (
<Link href={`/${params.organization}/${params.project}/profiles`}>
Profiles
</Link>
)}
<NavbarCreate /> <NavbarCreate />
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -6,11 +6,11 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from '@/components/ui/dropdown-menu';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { User } from "lucide-react"; import { User } from 'lucide-react';
import { signOut } from "next-auth/react"; import { signOut } from 'next-auth/react';
import Link from "next/link"; import Link from 'next/link';
export function NavbarUserDropdown() { export function NavbarUserDropdown() {
const params = useOrganizationParams(); const params = useOrganizationParams();

View File

@@ -1,3 +1,11 @@
import { useRefetchActive } from '@/hooks/useRefetchActive';
import { pushModal, showConfirm } from '@/modals';
import type { IProject } from '@/types';
import { api } from '@/utils/api';
import { clipboard } from '@/utils/clipboard';
import { MoreHorizontal } from 'lucide-react';
import { Button } from '../ui/button';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -5,27 +13,20 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } from '../ui/dropdown-menu';
import { Button } from "../ui/button"; import { toast } from '../ui/use-toast';
import { MoreHorizontal } from "lucide-react";
import { pushModal, showConfirm } from "@/modals";
import { type IProject } from "@/types";
import { clipboard } from "@/utils/clipboard";
import { useRefetchActive } from "@/hooks/useRefetchActive";
import { api } from "@/utils/api";
import { toast } from "../ui/use-toast";
export function ProjectActions({ id }: IProject) { export function ProjectActions({ id }: IProject) {
const refetch = useRefetchActive() const refetch = useRefetchActive();
const deletion = api.project.remove.useMutation({ const deletion = api.project.remove.useMutation({
onSuccess() { onSuccess() {
toast({ toast({
title: 'Success', title: 'Success',
description: 'Project deleted successfully.', description: 'Project deleted successfully.',
}) });
refetch() refetch();
} },
}) });
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -42,7 +43,7 @@ export function ProjectActions({ id }: IProject) {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
pushModal("EditProject", { id }); pushModal('EditProject', { id });
}} }}
> >
Edit Edit
@@ -57,9 +58,9 @@ export function ProjectActions({ id }: IProject) {
onConfirm() { onConfirm() {
deletion.mutate({ deletion.mutate({
id, id,
}) });
} },
}) });
}} }}
> >
Delete Delete

View File

@@ -1,26 +1,27 @@
import { formatDate } from "@/utils/date"; import { formatDate } from '@/utils/date';
import { type ColumnDef } from "@tanstack/react-table"; import type { Project as IProject } from '@prisma/client';
import { type Project as IProject } from "@prisma/client"; import type { ColumnDef } from '@tanstack/react-table';
import { ProjectActions } from "./ProjectActions";
import { ProjectActions } from './ProjectActions';
export type Project = IProject; export type Project = IProject;
export const columns: ColumnDef<Project>[] = [ export const columns: ColumnDef<Project>[] = [
{ {
accessorKey: "name", accessorKey: 'name',
header: "Name", header: 'Name',
}, },
{ {
accessorKey: "createdAt", accessorKey: 'createdAt',
header: "Created at", header: 'Created at',
cell({ row }) { cell({ row }) {
const date = row.original.createdAt; const date = row.original.createdAt;
return <div>{formatDate(date)}</div>; return <div>{formatDate(date)}</div>;
}, },
}, },
{ {
id: "actions", id: 'actions',
header: "Actions", header: 'Actions',
cell: ({ row }) => <ProjectActions {...row.original} />, cell: ({ row }) => <ProjectActions {...row.original} />,
}, },
]; ];

View File

@@ -1,13 +1,14 @@
import { useDispatch, useSelector } from "@/redux"; import { useDispatch, useSelector } from '@/redux';
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import type { IChartType } from '@/types';
import { chartTypes } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import { import {
changeChartType, changeChartType,
changeDateRanges, changeDateRanges,
changeInterval, changeInterval,
} from "./reportSlice"; } from './reportSlice';
import { Combobox } from "../ui/combobox";
import { type IChartType } from "@/types";
import { chartTypes } from "@/utils/constants";
export function ReportChartType() { export function ReportChartType() {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@@ -1,9 +1,10 @@
import { useDispatch, useSelector } from "@/redux"; import { useDispatch, useSelector } from '@/redux';
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import type { IInterval } from '@/types';
import { changeDateRanges, changeInterval } from "./reportSlice"; import { intervals, timeRanges } from '@/utils/constants';
import { Combobox } from "../ui/combobox";
import { type IInterval } from "@/types"; import { Combobox } from '../ui/combobox';
import { intervals, timeRanges } from "@/utils/constants"; import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import { changeDateRanges, changeInterval } from './reportSlice';
export function ReportDateRange() { export function ReportDateRange() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -28,7 +29,7 @@ export function ReportDateRange() {
); );
})} })}
</RadioGroup> </RadioGroup>
{chartType === "linear" && ( {chartType === 'linear' && (
<div className="w-full max-w-[200px]"> <div className="w-full max-w-[200px]">
<Combobox <Combobox
placeholder="Interval" placeholder="Interval"

View File

@@ -1,9 +1,9 @@
import { pick } from "ramda"; import { createContext, memo, useContext, useMemo } from 'react';
import { createContext, memo, useContext, useMemo } from "react"; import { pick } from 'ramda';
type ChartContextType = { interface ChartContextType {
editMode: boolean; editMode: boolean;
}; }
type ChartProviderProps = { type ChartProviderProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -20,7 +20,7 @@ export function ChartProvider({ children, editMode }: ChartProviderProps) {
() => ({ () => ({
editMode, editMode,
}), }),
[editMode], [editMode]
)} )}
> >
{children} {children}
@@ -28,20 +28,24 @@ export function ChartProvider({ children, editMode }: ChartProviderProps) {
); );
} }
export function withChartProivder<ComponentProps>(WrappedComponent: React.FC<ComponentProps>) { export function withChartProivder<ComponentProps>(
WrappedComponent: React.FC<ComponentProps>
) {
const WithChartProvider = (props: ComponentProps & ChartContextType) => { const WithChartProvider = (props: ComponentProps & ChartContextType) => {
return ( return (
<ChartProvider {...props}> <ChartProvider {...props}>
<WrappedComponent {...props} /> <WrappedComponent {...props} />
</ChartProvider> </ChartProvider>
) );
} };
WithChartProvider.displayName = `WithChartProvider(${WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'})` WithChartProvider.displayName = `WithChartProvider(${
WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'
})`;
return memo(WithChartProvider) return memo(WithChartProvider);
} }
export function useChartContext() { export function useChartContext() {
return useContext(ChartContext); return useContext(ChartContext);
} }

View File

@@ -1,28 +1,36 @@
import { ColorSquare } from "@/components/ColorSquare"; import { useMemo, useState } from 'react';
import { type IChartData } from "@/types"; import { ColorSquare } from '@/components/ColorSquare';
import { type RouterOutputs } from "@/utils/api"; import {
import { getChartColor } from "@/utils/theme"; Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { IChartData } from '@/types';
import type { RouterOutputs } from '@/utils/api';
import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme';
import { import {
useReactTable,
getCoreRowModel,
flexRender,
createColumnHelper, createColumnHelper,
flexRender,
getCoreRowModel,
getSortedRowModel, getSortedRowModel,
type SortingState, useReactTable,
} from "@tanstack/react-table"; } from '@tanstack/react-table';
import { useMemo, useState } from "react"; import type { SortingState } from '@tanstack/react-table';
import { useElementSize } from "usehooks-ts"; import { ChevronDown, ChevronUp } from 'lucide-react';
import { useChartContext } from "./ChartProvider"; import { useElementSize } from 'usehooks-ts';
import { ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/utils/cn"; import { useChartContext } from './ChartProvider';
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]>();
type ReportBarChartProps = { interface ReportBarChartProps {
data: IChartData; data: IChartData;
}; }
export function ReportBarChart({ data }: ReportBarChartProps) { export function ReportBarChart({ data }: ReportBarChartProps) {
const { editMode } = useChartContext(); const { editMode } = useChartContext();
@@ -31,13 +39,13 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
const table = useReactTable({ const table = useReactTable({
data: useMemo( data: useMemo(
() => (editMode ? data.series : data.series.slice(0, 20)), () => (editMode ? data.series : data.series.slice(0, 20)),
[editMode, data], [editMode, data]
), ),
columns: useMemo(() => { columns: useMemo(() => {
return [ return [
columnHelper.accessor((row) => row.name, { columnHelper.accessor((row) => row.name, {
id: "label", id: 'label',
header: () => "Label", header: () => 'Label',
cell(info) { cell(info) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -50,35 +58,35 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
size: width ? width * 0.3 : undefined, size: width ? width * 0.3 : undefined,
}), }),
columnHelper.accessor((row) => row.totalCount, { columnHelper.accessor((row) => row.totalCount, {
id: "totalCount", id: 'totalCount',
cell: (info) => ( cell: (info) => (
<div className="text-right font-medium">{info.getValue()}</div> <div className="text-right font-medium">{info.getValue()}</div>
), ),
header: () => "Count", header: () => 'Count',
footer: (info) => info.column.id, footer: (info) => info.column.id,
size: width ? width * 0.1 : undefined, size: width ? width * 0.1 : undefined,
enableSorting: true, enableSorting: true,
}), }),
columnHelper.accessor((row) => row.totalCount, { columnHelper.accessor((row) => row.totalCount, {
id: "graph", id: 'graph',
cell: (info) => ( cell: (info) => (
<div <div
className="shine h-4 rounded [.mini_&]:h-3" className="shine h-4 rounded [.mini_&]:h-3"
style={{ style={{
width: width:
(info.getValue() / info.row.original.meta.highest) * 100 + (info.getValue() / info.row.original.meta.highest) * 100 +
"%", '%',
background: getChartColor(info.row.index), background: getChartColor(info.row.index),
}} }}
/> />
), ),
header: () => "Graph", header: () => 'Graph',
footer: (info) => info.column.id, footer: (info) => info.column.id,
size: width ? width * 0.6 : undefined, size: width ? width * 0.6 : undefined,
}), }),
]; ];
}, [width]), }, [width]),
columnResizeMode: "onChange", columnResizeMode: 'onChange',
state: { state: {
sorting, sorting,
}, },
@@ -100,7 +108,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
<ColorSquare>{event.id}</ColorSquare> {event.name} <ColorSquare>{event.id}</ColorSquare> {event.name}
</div> </div>
<div className="mt-6 font-mono text-5xl font-light"> <div className="mt-6 font-mono text-5xl font-light">
{new Intl.NumberFormat("en-IN", { {new Intl.NumberFormat('en-IN', {
maximumSignificantDigits: 20, maximumSignificantDigits: 20,
}).format(event.count)} }).format(event.count)}
</div> </div>
@@ -134,16 +142,16 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
<div <div
{...{ {...{
className: cn( className: cn(
"flex items-center gap-2", 'flex items-center gap-2',
header.column.getCanSort() && header.column.getCanSort() &&
"cursor-pointer select-none", 'cursor-pointer select-none'
), ),
onClick: header.column.getToggleSortingHandler(), onClick: header.column.getToggleSortingHandler(),
}} }}
> >
{flexRender( {flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext(), header.getContext()
)} )}
{{ {{
asc: <ChevronUp className="ml-auto" size={14} />, asc: <ChevronUp className="ml-auto" size={14} />,
@@ -156,7 +164,7 @@ export function ReportBarChart({ data }: ReportBarChartProps) {
onMouseDown: header.getResizeHandler(), onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(), onTouchStart: header.getResizeHandler(),
className: `resizer ${ className: `resizer ${
header.column.getIsResizing() ? "isResizing" : "" header.column.getIsResizing() ? 'isResizing' : ''
}`, }`,
style: {}, style: {},
} }

View File

@@ -1,3 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { AutoSizer } from '@/components/AutoSizer';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import type { IChartData, IInterval } from '@/types';
import { getChartColor } from '@/utils/theme';
import { import {
CartesianGrid, CartesianGrid,
Line, Line,
@@ -5,20 +10,16 @@ import {
Tooltip, Tooltip,
XAxis, XAxis,
YAxis, YAxis,
} from "recharts"; } from 'recharts';
import { ReportLineChartTooltip } from "./ReportLineChartTooltip";
import { useFormatDateInterval } from "@/hooks/useFormatDateInterval";
import { type IChartData, type IInterval } from "@/types";
import { getChartColor } from "@/utils/theme";
import { ReportTable } from "./ReportTable";
import { useEffect, useRef, useState } from "react";
import { AutoSizer } from "@/components/AutoSizer";
import { useChartContext } from "./ChartProvider";
type ReportLineChartProps = { import { useChartContext } from './ChartProvider';
import { ReportLineChartTooltip } from './ReportLineChartTooltip';
import { ReportTable } from './ReportTable';
interface ReportLineChartProps {
data: IChartData; data: IChartData;
interval: IInterval; interval: IInterval;
}; }
export function ReportLineChart({ interval, data }: ReportLineChartProps) { export function ReportLineChart({ interval, data }: ReportLineChartProps) {
const { editMode } = useChartContext(); const { editMode } = useChartContext();
@@ -31,7 +32,7 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
const max = 20; const max = 20;
setVisibleSeries( setVisibleSeries(
data?.series?.slice(0, max).map((serie) => serie.name) ?? [], data?.series?.slice(0, max).map((serie) => serie.name) ?? []
); );
// ref.current = true; // ref.current = true;
} }
@@ -42,7 +43,7 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
<AutoSizer disableHeight> <AutoSizer disableHeight>
{({ width }) => ( {({ width }) => (
<LineChart width={width} height={Math.min(width * 0.5, 400)}> <LineChart width={width} height={Math.min(width * 0.5, 400)}>
<YAxis dataKey={"count"} width={30} fontSize={12}></YAxis> <YAxis dataKey={'count'} width={30} fontSize={12}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} /> <Tooltip content={<ReportLineChartTooltip />} />
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis <XAxis
@@ -60,7 +61,7 @@ export function ReportLineChart({ interval, data }: ReportLineChartProps) {
}) })
.map((serie) => { .map((serie) => {
const realIndex = data?.series.findIndex( const realIndex = data?.series.findIndex(
(item) => item.name === serie.name, (item) => item.name === serie.name
); );
const key = serie.name; const key = serie.name;
const strokeColor = getChartColor(realIndex); const strokeColor = getChartColor(realIndex);

View File

@@ -1,7 +1,7 @@
import { useFormatDateInterval } from "@/hooks/useFormatDateInterval"; import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useMappings } from "@/hooks/useMappings"; import { useMappings } from '@/hooks/useMappings';
import { useSelector } from "@/redux"; import { useSelector } from '@/redux';
import { type IToolTipProps } from "@/types"; import type { IToolTipProps } from '@/types';
type ReportLineChartTooltipProps = IToolTipProps<{ type ReportLineChartTooltipProps = IToolTipProps<{
color: string; color: string;

View File

@@ -1,18 +1,17 @@
import * as React from "react"; import * as React from 'react';
import { useFormatDateInterval } from "@/hooks/useFormatDateInterval"; import { Checkbox } from '@/components/ui/checkbox';
import { useSelector } from "@/redux"; import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { Checkbox } from "@/components/ui/checkbox"; import { useMappings } from '@/hooks/useMappings';
import { getChartColor } from "@/utils/theme"; import { useSelector } from '@/redux';
import { cn } from "@/utils/cn"; import type { IChartData } from '@/types';
import { useMappings } from "@/hooks/useMappings"; import { cn } from '@/utils/cn';
import { type IChartData } from "@/types"; import { getChartColor } from '@/utils/theme';
interface ReportTableProps {
type ReportTableProps = {
data: IChartData; data: IChartData;
visibleSeries: string[]; visibleSeries: string[];
setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>; setVisibleSeries: React.Dispatch<React.SetStateAction<string[]>>;
}; }
export function ReportTable({ export function ReportTable({
data, data,
@@ -21,7 +20,7 @@ export function ReportTable({
}: ReportTableProps) { }: ReportTableProps) {
const interval = useSelector((state) => state.report.interval); const interval = useSelector((state) => state.report.interval);
const formatDate = useFormatDateInterval(interval); const formatDate = useFormatDateInterval(interval);
const getLabel = useMappings() const getLabel = useMappings();
function handleChange(name: string, checked: boolean) { function handleChange(name: string, checked: boolean) {
setVisibleSeries((prev) => { setVisibleSeries((prev) => {
@@ -33,11 +32,12 @@ export function ReportTable({
}); });
} }
const row = "flex border-b border-border last:border-b-0 flex-1"; const row = 'flex border-b border-border last:border-b-0 flex-1';
const cell = "p-2 last:pr-8 last:w-[8rem]"; const cell = 'p-2 last:pr-8 last:w-[8rem]';
const value = "min-w-[6rem] text-right"; const value = 'min-w-[6rem] text-right';
const header = "text-sm font-medium"; const header = 'text-sm font-medium';
const total = 'bg-gray-50 text-emerald-600 font-medium border-r border-border' const total =
'bg-gray-50 text-emerald-600 font-medium border-r border-border';
return ( return (
<div className="flex w-fit max-w-full rounded-md border border-border"> <div className="flex w-fit max-w-full rounded-md border border-border">
{/* Labels */} {/* Labels */}
@@ -49,7 +49,7 @@ export function ReportTable({
return ( return (
<div <div
key={serie.name} key={serie.name}
className={cn("flex max-w-[200px] items-center gap-2", row, cell)} className={cn('flex max-w-[200px] items-center gap-2', row, cell)}
> >
<Checkbox <Checkbox
onCheckedChange={(checked) => onCheckedChange={(checked) =>
@@ -76,7 +76,7 @@ export function ReportTable({
{/* ScrollView for all values */} {/* ScrollView for all values */}
<div className="w-full overflow-auto"> <div className="w-full overflow-auto">
{/* Header */} {/* Header */}
<div className={cn("w-max", row)}> <div className={cn('w-max', row)}>
<div className={cn(header, value, cell, total)}>Total</div> <div className={cn(header, value, cell, total)}>Total</div>
{data.series[0]?.data.map((serie) => ( {data.series[0]?.data.map((serie) => (
<div <div
@@ -91,8 +91,10 @@ export function ReportTable({
{/* Values */} {/* Values */}
{data.series.map((serie) => { {data.series.map((serie) => {
return ( return (
<div className={cn("w-max", row)} key={serie.name}> <div className={cn('w-max', row)} key={serie.name}>
<div className={cn(header, value, cell, total)}>{serie.totalCount}</div> <div className={cn(header, value, cell, total)}>
{serie.totalCount}
</div>
{serie.data.map((item) => { {serie.data.map((item) => {
return ( return (
<div key={item.date} className={cn(value, cell)}> <div key={item.date} className={cn(value, cell)}>

View File

@@ -1,64 +1,67 @@
import { api } from "@/utils/api"; import type { IChartInput } from '@/types';
import { type IChartInput } from "@/types"; import { api } from '@/utils/api';
import { ReportBarChart } from "./ReportBarChart";
import { ReportLineChart } from "./ReportLineChart";
import { withChartProivder } from "./ChartProvider";
type ReportLineChartProps = IChartInput import { withChartProivder } from './ChartProvider';
import { ReportBarChart } from './ReportBarChart';
import { ReportLineChart } from './ReportLineChart';
export const Chart = withChartProivder(({ type ReportLineChartProps = IChartInput;
interval,
events,
breakdowns,
chartType,
name,
range,
}: ReportLineChartProps) => {
const hasEmptyFilters = events.some((event) => event.filters.some((filter) => filter.value.length === 0));
const chart = api.chart.chart.useQuery(
{
interval,
chartType,
events,
breakdowns,
name,
range,
startDate: null,
endDate: null,
},
{
keepPreviousData: true,
enabled: events.length > 0 && !hasEmptyFilters,
},
);
const anyData = Boolean(chart.data?.series?.[0]?.data) export const Chart = withChartProivder(
({
interval,
events,
breakdowns,
chartType,
name,
range,
}: ReportLineChartProps) => {
const hasEmptyFilters = events.some((event) =>
event.filters.some((filter) => filter.value.length === 0)
);
const chart = api.chart.chart.useQuery(
{
interval,
chartType,
events,
breakdowns,
name,
range,
startDate: null,
endDate: null,
},
{
keepPreviousData: true,
enabled: events.length > 0 && !hasEmptyFilters,
}
);
if(chart.isFetching && !anyData) { const anyData = Boolean(chart.data?.series?.[0]?.data);
return (<p>Loading...</p>)
if (chart.isFetching && !anyData) {
return <p>Loading...</p>;
}
if (chart.isError) {
return <p>Error</p>;
}
if (!chart.isSuccess) {
return <p>Loading...</p>;
}
if (!anyData) {
return <p>No data</p>;
}
if (chartType === 'bar') {
return <ReportBarChart data={chart.data} />;
}
if (chartType === 'linear') {
return <ReportLineChart interval={interval} data={chart.data} />;
}
return <p>Chart type &quot;{chartType}&quot; is not supported yet.</p>;
} }
);
if(chart.isError) {
return (<p>Error</p>)
}
if(!chart.isSuccess) {
return (<p>Loading...</p>)
}
if(!anyData) {
return (<p>No data</p>)
}
if(chartType === 'bar') {
return <ReportBarChart data={chart.data} />
}
if(chartType === 'linear') {
return <ReportLineChart interval={interval} data={chart.data} />
}
return <p>Chart type &quot;{chartType}&quot; is not supported yet.</p>
})

View File

@@ -1,13 +1,14 @@
import { import type {
type IChartBreakdown, IChartBreakdown,
type IChartEvent, IChartEvent,
type IChartInput, IChartInput,
type IChartRange, IChartRange,
type IChartType, IChartType,
type IInterval, IInterval,
} from '@/types'; } from '@/types';
import { alphabetIds } from '@/utils/constants'; import { alphabetIds } from '@/utils/constants';
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
type InitialState = IChartInput & { type InitialState = IChartInput & {
startDate: string | null; startDate: string | null;

View File

@@ -1,48 +1,41 @@
import * as React from "react" import * as React from 'react';
import { Filter, MoreHorizontal, Tags, Trash } from "lucide-react" import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu';
import { MoreHorizontal, Trash } from 'lucide-react';
export type ReportBreakdownMoreProps = {
onClick: (action: 'remove') => void export interface ReportBreakdownMoreProps {
onClick: (action: 'remove') => void;
} }
export function ReportBreakdownMore({ onClick }: ReportBreakdownMoreProps) { export function ReportBreakdownMore({ onClick }: ReportBreakdownMoreProps) {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false);
return ( return (
<DropdownMenu open={open} onOpenChange={setOpen}> <DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm"> <Button variant="ghost" size="sm">
<MoreHorizontal /> <MoreHorizontal />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[200px]"> <DropdownMenuContent align="start" className="w-[200px]">
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem className="text-red-600" onClick={() => onClick('remove')}> <DropdownMenuItem
<Trash className="mr-2 h-4 w-4" /> className="text-red-600"
Delete onClick={() => onClick('remove')}
<DropdownMenuShortcut></DropdownMenuShortcut> >
</DropdownMenuItem> <Trash className="mr-2 h-4 w-4" />
</DropdownMenuGroup> Delete
</DropdownMenuContent> <DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenu> </DropdownMenuItem>
) </DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
} }

View File

@@ -1,12 +1,13 @@
import { api } from "@/utils/api"; import { ColorSquare } from '@/components/ColorSquare';
import { Combobox } from "@/components/ui/combobox"; import { Combobox } from '@/components/ui/combobox';
import { useDispatch, useSelector } from "@/redux"; import { RenderDots } from '@/components/ui/RenderDots';
import { addBreakdown, changeBreakdown, removeBreakdown } from "../reportSlice"; import { useDispatch, useSelector } from '@/redux';
import { type ReportEventMoreProps } from "./ReportEventMore"; import type { IChartBreakdown } from '@/types';
import { type IChartBreakdown } from "@/types"; import { api } from '@/utils/api';
import { ReportBreakdownMore } from "./ReportBreakdownMore";
import { RenderDots } from "@/components/ui/RenderDots"; import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
import { ColorSquare } from "@/components/ColorSquare"; import { ReportBreakdownMore } from './ReportBreakdownMore';
import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportBreakdowns() { export function ReportBreakdowns() {
const selectedBreakdowns = useSelector((state) => state.report.breakdowns); const selectedBreakdowns = useSelector((state) => state.report.breakdowns);
@@ -18,9 +19,9 @@ export function ReportBreakdowns() {
})); }));
const handleMore = (breakdown: IChartBreakdown) => { const handleMore = (breakdown: IChartBreakdown) => {
const callback: ReportEventMoreProps["onClick"] = (action) => { const callback: ReportEventMoreProps['onClick'] = (action) => {
switch (action) { switch (action) {
case "remove": { case 'remove': {
return dispatch(removeBreakdown(breakdown)); return dispatch(removeBreakdown(breakdown));
} }
} }
@@ -37,9 +38,7 @@ export function ReportBreakdowns() {
return ( return (
<div key={item.name} className="rounded-lg border"> <div key={item.name} className="rounded-lg border">
<div className="flex items-center gap-2 p-2 px-4"> <div className="flex items-center gap-2 p-2 px-4">
<ColorSquare> <ColorSquare>{index}</ColorSquare>
{index}
</ColorSquare>
<Combobox <Combobox
value={item.name} value={item.name}
onChange={(value) => { onChange={(value) => {
@@ -47,7 +46,7 @@ export function ReportBreakdowns() {
changeBreakdown({ changeBreakdown({
...item, ...item,
name: value, name: value,
}), })
); );
}} }}
items={propertiesCombobox} items={propertiesCombobox}
@@ -61,12 +60,12 @@ export function ReportBreakdowns() {
{selectedBreakdowns.length === 0 && ( {selectedBreakdowns.length === 0 && (
<Combobox <Combobox
value={""} value={''}
onChange={(value) => { onChange={(value) => {
dispatch( dispatch(
addBreakdown({ addBreakdown({
name: value, name: value,
}), })
); );
}} }}
items={propertiesCombobox} items={propertiesCombobox}

View File

@@ -1,10 +1,8 @@
import { api } from "@/utils/api"; import type { Dispatch } from 'react';
import { import { ColorSquare } from '@/components/ColorSquare';
type IChartEvent, import { Dropdown } from '@/components/Dropdown';
type IChartEventFilterValue, import { Button } from '@/components/ui/button';
type IChartEventFilter, import { ComboboxMulti } from '@/components/ui/combobox-multi';
} from "@/types";
import { CreditCard, SlidersHorizontal, Trash } from "lucide-react";
import { import {
CommandDialog, CommandDialog,
CommandEmpty, CommandEmpty,
@@ -13,23 +11,26 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
CommandSeparator, CommandSeparator,
} from "@/components/ui/command"; } from '@/components/ui/command';
import { type Dispatch } from "react"; import { RenderDots } from '@/components/ui/RenderDots';
import { RenderDots } from "@/components/ui/RenderDots"; import { useMappings } from '@/hooks/useMappings';
import { useDispatch } from "@/redux"; import { useDispatch } from '@/redux';
import { changeEvent } from "../reportSlice"; import type {
import { Button } from "@/components/ui/button"; IChartEvent,
import { ComboboxMulti } from "@/components/ui/combobox-multi"; IChartEventFilter,
import { Dropdown } from "@/components/Dropdown"; IChartEventFilterValue,
import { operators } from "@/utils/constants"; } from '@/types';
import { useMappings } from "@/hooks/useMappings"; import { api } from '@/utils/api';
import { ColorSquare } from "@/components/ColorSquare"; import { operators } from '@/utils/constants';
import { CreditCard, SlidersHorizontal, Trash } from 'lucide-react';
type ReportEventFiltersProps = { import { changeEvent } from '../reportSlice';
interface ReportEventFiltersProps {
event: IChartEvent; event: IChartEvent;
isCreating: boolean; isCreating: boolean;
setIsCreating: Dispatch<boolean>; setIsCreating: Dispatch<boolean>;
}; }
export function ReportEventFilters({ export function ReportEventFilters({
event, event,
@@ -43,7 +44,7 @@ export function ReportEventFilters({
}, },
{ {
enabled: !!event.name, enabled: !!event.name,
}, }
); );
return ( return (
@@ -71,11 +72,11 @@ export function ReportEventFilters({
{ {
id: (event.filters.length + 1).toString(), id: (event.filters.length + 1).toString(),
name: item, name: item,
operator: "is", operator: 'is',
value: [], value: [],
}, },
], ],
}), })
); );
}} }}
> >
@@ -92,13 +93,13 @@ export function ReportEventFilters({
); );
} }
type FilterProps = { interface FilterProps {
event: IChartEvent; event: IChartEvent;
filter: IChartEvent["filters"][number]; filter: IChartEvent['filters'][number];
}; }
function Filter({ filter, event }: FilterProps) { function Filter({ filter, event }: FilterProps) {
const getLabel = useMappings() const getLabel = useMappings();
const dispatch = useDispatch(); const dispatch = useDispatch();
const potentialValues = api.chart.values.useQuery({ const potentialValues = api.chart.values.useQuery({
event: event.name, event: event.name,
@@ -116,12 +117,12 @@ function Filter({ filter, event }: FilterProps) {
changeEvent({ changeEvent({
...event, ...event,
filters: event.filters.filter((item) => item.id !== filter.id), filters: event.filters.filter((item) => item.id !== filter.id),
}), })
); );
}; };
const changeFilterValue = ( const changeFilterValue = (
value: IChartEventFilterValue | IChartEventFilterValue[], value: IChartEventFilterValue | IChartEventFilterValue[]
) => { ) => {
dispatch( dispatch(
changeEvent({ changeEvent({
@@ -136,11 +137,11 @@ function Filter({ filter, event }: FilterProps) {
return item; return item;
}), }),
}), })
); );
}; };
const changeFilterOperator = (operator: IChartEventFilter["operator"]) => { const changeFilterOperator = (operator: IChartEventFilter['operator']) => {
dispatch( dispatch(
changeEvent({ changeEvent({
...event, ...event,
@@ -154,7 +155,7 @@ function Filter({ filter, event }: FilterProps) {
return item; return item;
}), }),
}), })
); );
}; };
@@ -168,7 +169,7 @@ function Filter({ filter, event }: FilterProps) {
<SlidersHorizontal size={10} /> <SlidersHorizontal size={10} />
</ColorSquare> </ColorSquare>
<div className="flex flex-1 text-sm"> <div className="flex flex-1 text-sm">
<RenderDots truncate>{filter.name}</RenderDots> <RenderDots truncate>{filter.name}</RenderDots>
</div> </div>
<Button variant="ghost" size="sm" onClick={removeFilter}> <Button variant="ghost" size="sm" onClick={removeFilter}>
<Trash size={16} /> <Trash size={16} />
@@ -178,12 +179,12 @@ function Filter({ filter, event }: FilterProps) {
<Dropdown <Dropdown
onChange={changeFilterOperator} onChange={changeFilterOperator}
items={Object.entries(operators).map(([key, value]) => ({ items={Object.entries(operators).map(([key, value]) => ({
value: key as IChartEventFilter["operator"], value: key as IChartEventFilter['operator'],
label: value, label: value,
}))} }))}
label="Segment" label="Segment"
> >
<Button variant={"ghost"} className="whitespace-nowrap"> <Button variant={'ghost'} className="whitespace-nowrap">
{operators[filter.operator]} {operators[filter.operator]}
</Button> </Button>
</Dropdown> </Dropdown>
@@ -191,16 +192,16 @@ function Filter({ filter, event }: FilterProps) {
placeholder="Select values" placeholder="Select values"
items={valuesCombobox} items={valuesCombobox}
selected={filter.value.map((item) => ({ selected={filter.value.map((item) => ({
value: item?.toString() ?? "__filter_value_null__", value: item?.toString() ?? '__filter_value_null__',
label: getLabel(item?.toString() ?? "__filter_value_null__"), label: getLabel(item?.toString() ?? '__filter_value_null__'),
}))} }))}
setSelected={(setFn) => { setSelected={(setFn) => {
if(typeof setFn === "function") { if (typeof setFn === 'function') {
const newValues = setFn( const newValues = setFn(
filter.value.map((item) => ({ filter.value.map((item) => ({
value: item?.toString() ?? "__filter_value_null__", value: item?.toString() ?? '__filter_value_null__',
label: getLabel(item?.toString() ?? "__filter_value_null__"), label: getLabel(item?.toString() ?? '__filter_value_null__'),
})), }))
); );
changeFilterValue(newValues.map((item) => item.value)); changeFilterValue(newValues.map((item) => item.value));
} else { } else {

View File

@@ -1,7 +1,5 @@
import * as React from "react" import * as React from 'react';
import { Filter, MoreHorizontal, Tags, Trash } from "lucide-react" import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button"
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -9,7 +7,7 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/components/ui/command" } from '@/components/ui/command';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -22,47 +20,50 @@ import {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu';
import { Filter, MoreHorizontal, Tags, Trash } from 'lucide-react';
const labels = [ const labels = [
"feature", 'feature',
"bug", 'bug',
"enhancement", 'enhancement',
"documentation", 'documentation',
"design", 'design',
"question", 'question',
"maintenance", 'maintenance',
] ];
export type ReportEventMoreProps = { export interface ReportEventMoreProps {
onClick: (action: 'createFilter' | 'remove') => void onClick: (action: 'createFilter' | 'remove') => void;
} }
export function ReportEventMore({ onClick }: ReportEventMoreProps) { export function ReportEventMore({ onClick }: ReportEventMoreProps) {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false);
return (
<DropdownMenu open={open} onOpenChange={setOpen}> return (
<DropdownMenuTrigger asChild> <DropdownMenu open={open} onOpenChange={setOpen}>
<Button variant="ghost" size="sm"> <DropdownMenuTrigger asChild>
<MoreHorizontal /> <Button variant="ghost" size="sm">
</Button> <MoreHorizontal />
</DropdownMenuTrigger> </Button>
<DropdownMenuContent align="end" className="w-[200px]"> </DropdownMenuTrigger>
<DropdownMenuGroup> <DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={() => onClick('createFilter')}> <DropdownMenuGroup>
<Filter className="mr-2 h-4 w-4" /> <DropdownMenuItem onClick={() => onClick('createFilter')}>
Add filter <Filter className="mr-2 h-4 w-4" />
</DropdownMenuItem> Add filter
<DropdownMenuSeparator /> </DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => onClick('remove')}> <DropdownMenuSeparator />
<Trash className="mr-2 h-4 w-4" /> <DropdownMenuItem
Delete className="text-red-600"
<DropdownMenuShortcut></DropdownMenuShortcut> onClick={() => onClick('remove')}
</DropdownMenuItem> >
</DropdownMenuGroup> <Trash className="mr-2 h-4 w-4" />
</DropdownMenuContent> Delete
</DropdownMenu> <DropdownMenuShortcut></DropdownMenuShortcut>
) </DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
} }

View File

@@ -1,14 +1,16 @@
import { api } from "@/utils/api"; import { useState } from 'react';
import { Combobox } from "@/components/ui/combobox"; import { ColorSquare } from '@/components/ColorSquare';
import { useDispatch, useSelector } from "@/redux"; import { Dropdown } from '@/components/Dropdown';
import { addEvent, changeEvent, removeEvent } from "../reportSlice"; import { Combobox } from '@/components/ui/combobox';
import { ReportEventFilters } from "./ReportEventFilters"; import { useDispatch, useSelector } from '@/redux';
import { useState } from "react"; import type { IChartEvent } from '@/types';
import { ReportEventMore, type ReportEventMoreProps } from "./ReportEventMore"; import { api } from '@/utils/api';
import { type IChartEvent } from "@/types"; import { Filter, GanttChart, Users } from 'lucide-react';
import { Filter, GanttChart, Users } from "lucide-react";
import { Dropdown } from "@/components/Dropdown"; import { addEvent, changeEvent, removeEvent } from '../reportSlice';
import { ColorSquare } from "@/components/ColorSquare"; import { ReportEventFilters } from './ReportEventFilters';
import { ReportEventMore } from './ReportEventMore';
import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportEvents() { export function ReportEvents() {
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
@@ -21,12 +23,12 @@ export function ReportEvents() {
})); }));
const handleMore = (event: IChartEvent) => { const handleMore = (event: IChartEvent) => {
const callback: ReportEventMoreProps["onClick"] = (action) => { const callback: ReportEventMoreProps['onClick'] = (action) => {
switch (action) { switch (action) {
case "createFilter": { case 'createFilter': {
return setIsCreating(true); return setIsCreating(true);
} }
case "remove": { case 'remove': {
return dispatch(removeEvent(event)); return dispatch(removeEvent(event));
} }
} }
@@ -43,9 +45,7 @@ export function ReportEvents() {
return ( return (
<div key={event.name} className="rounded-lg border"> <div key={event.name} className="rounded-lg border">
<div className="flex items-center gap-2 p-2"> <div className="flex items-center gap-2 p-2">
<ColorSquare> <ColorSquare>{event.id}</ColorSquare>
{event.id}
</ColorSquare>
<Combobox <Combobox
value={event.name} value={event.name}
onChange={(value) => { onChange={(value) => {
@@ -54,7 +54,7 @@ export function ReportEvents() {
...event, ...event,
name: value, name: value,
filters: [], filters: [],
}), })
); );
}} }}
items={eventsCombobox} items={eventsCombobox}
@@ -71,32 +71,36 @@ export function ReportEvents() {
changeEvent({ changeEvent({
...event, ...event,
segment, segment,
}), })
); );
}} }}
items={[ items={[
{ {
value: "event", value: 'event',
label: "All events", label: 'All events',
}, },
{ {
value: "user", value: 'user',
label: "Unique users", label: 'Unique users',
}, },
]} ]}
label="Segment" label="Segment"
> >
<button className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs"> <button className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs">
{event.segment === "user" ? ( {event.segment === 'user' ? (
<><Users size={12} /> Unique users</> <>
) : ( <Users size={12} /> Unique users
<><GanttChart size={12} /> All events</> </>
) : (
<>
<GanttChart size={12} /> All events
</>
)} )}
</button> </button>
</Dropdown> </Dropdown>
<button <button
onClick={() => { onClick={() => {
handleMore(event)("createFilter"); handleMore(event)('createFilter');
}} }}
className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs" className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs"
> >
@@ -111,14 +115,14 @@ export function ReportEvents() {
})} })}
<Combobox <Combobox
value={""} value={''}
onChange={(value) => { onChange={(value) => {
dispatch( dispatch(
addEvent({ addEvent({
name: value, name: value,
segment: "event", segment: 'event',
filters: [], filters: [],
}), })
); );
}} }}
items={eventsCombobox} items={eventsCombobox}

View File

@@ -1,39 +1,47 @@
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { useReportId } from "../hooks/useReportId"; import { toast } from '@/components/ui/use-toast';
import { api, handleError } from "@/utils/api"; import { pushModal } from '@/modals';
import { useSelector } from "@/redux"; import { useSelector } from '@/redux';
import { pushModal } from "@/modals"; import { api, handleError } from '@/utils/api';
import { toast } from "@/components/ui/use-toast";
import { useReportId } from '../hooks/useReportId';
export function ReportSaveButton() { export function ReportSaveButton() {
const { reportId } = useReportId(); const { reportId } = useReportId();
const update = api.report.update.useMutation({ const update = api.report.update.useMutation({
onSuccess() { onSuccess() {
toast({ toast({
title: "Success", title: 'Success',
description: "Report updated.", description: 'Report updated.',
}); });
}, },
onError: handleError onError: handleError,
}); });
const report = useSelector((state) => state.report); const report = useSelector((state) => state.report);
if (reportId) { if (reportId) {
return <Button loading={update.isLoading} onClick={() => { return (
update.mutate({ <Button
reportId, loading={update.isLoading}
report, onClick={() => {
dashboardId: "9227feb4-ad59-40f3-b887-3501685733dd", update.mutate({
projectId: "f7eabf0c-e0b0-4ac0-940f-1589715b0c3d", reportId,
}); report,
}}>Update</Button>; dashboardId: '9227feb4-ad59-40f3-b887-3501685733dd',
projectId: 'f7eabf0c-e0b0-4ac0-940f-1589715b0c3d',
});
}}
>
Update
</Button>
);
} else { } else {
return ( return (
<Button <Button
onClick={() => { onClick={() => {
pushModal('SaveReport', { pushModal('SaveReport', {
report, report,
}) });
}} }}
> >
Create Create

View File

@@ -1,6 +1,6 @@
import { ReportEvents } from "./ReportEvents"; import { ReportBreakdowns } from './ReportBreakdowns';
import { ReportBreakdowns } from "./ReportBreakdowns"; import { ReportEvents } from './ReportEvents';
import { ReportSaveButton } from "./ReportSaveButton"; import { ReportSaveButton } from './ReportSaveButton';
export function ReportSidebar() { export function ReportSidebar() {
return ( return (

View File

@@ -1,6 +1,7 @@
import { cn } from "@/utils/cn"; import { cn } from '@/utils/cn';
import { Asterisk, ChevronRight } from "lucide-react"; import { Asterisk, ChevronRight } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
interface RenderDotsProps extends React.HTMLAttributes<HTMLDivElement> { interface RenderDotsProps extends React.HTMLAttributes<HTMLDivElement> {
children: string; children: string;
@@ -13,27 +14,27 @@ export function RenderDots({
truncate, truncate,
...props ...props
}: RenderDotsProps) { }: RenderDotsProps) {
const parts = children.split("."); const parts = children.split('.');
const sliceAt = truncate && parts.length > 3 ? 3 : 0; const sliceAt = truncate && parts.length > 3 ? 3 : 0;
return ( return (
<Tooltip disableHoverableContent={true} open={sliceAt === 0 ? false : undefined}> <Tooltip
disableHoverableContent={true}
open={sliceAt === 0 ? false : undefined}
>
<TooltipTrigger> <TooltipTrigger>
<div <div {...props} className={cn('flex items-center gap-1', className)}>
{...props}
className={cn("flex items-center gap-1", className)}
>
{parts.slice(-sliceAt).map((str, index) => { {parts.slice(-sliceAt).map((str, index) => {
return ( return (
<div className="flex items-center gap-1" key={str + index}> <div className="flex items-center gap-1" key={str + index}>
{index !== 0 && ( {index !== 0 && (
<ChevronRight className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" /> <ChevronRight className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
)} )}
{str.includes("[*]") ? ( {str.includes('[*]') ? (
<> <>
{str.replace("[*]", "")} {str.replace('[*]', '')}
<Asterisk className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" /> <Asterisk className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
</> </>
) : str === "*" ? ( ) : str === '*' ? (
<Asterisk className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" /> <Asterisk className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
) : ( ) : (
str str

View File

@@ -1,14 +1,13 @@
import * as React from "react" import * as React from 'react';
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from "@/utils/cn" const AlertDialog = AlertDialogPrimitive.Root;
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef< const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
@@ -16,14 +15,14 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className className
)} )}
{...props} {...props}
ref={ref} ref={ref}
/> />
)) ));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef< const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>, React.ElementRef<typeof AlertDialogPrimitive.Content>,
@@ -34,14 +33,14 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full", 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',
className className
)} )}
{...props} {...props}
/> />
</AlertDialogPortal> </AlertDialogPortal>
)) ));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ const AlertDialogHeader = ({
className, className,
@@ -49,13 +48,13 @@ const AlertDialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col space-y-2 text-center sm:text-left", 'flex flex-col space-y-2 text-center sm:text-left',
className className
)} )}
{...props} {...props}
/> />
) );
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({ const AlertDialogFooter = ({
className, className,
@@ -63,13 +62,13 @@ const AlertDialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className className
)} )}
{...props} {...props}
/> />
) );
AlertDialogFooter.displayName = "AlertDialogFooter" AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, React.ElementRef<typeof AlertDialogPrimitive.Title>,
@@ -77,11 +76,11 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold", className)} className={cn('text-lg font-semibold', className)}
{...props} {...props}
/> />
)) ));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef< const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, React.ElementRef<typeof AlertDialogPrimitive.Description>,
@@ -89,12 +88,12 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
AlertDialogDescription.displayName = AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef< const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>, React.ElementRef<typeof AlertDialogPrimitive.Action>,
@@ -105,8 +104,8 @@ const AlertDialogAction = React.forwardRef<
className={cn(buttonVariants(), className)} className={cn(buttonVariants(), className)}
{...props} {...props}
/> />
)) ));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef< const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@@ -115,14 +114,14 @@ const AlertDialogCancel = React.forwardRef<
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
ref={ref} ref={ref}
className={cn( className={cn(
buttonVariants({ variant: "outline" }), buttonVariants({ variant: 'outline' }),
"mt-2 sm:mt-0", 'mt-2 sm:mt-0',
className className
)} )}
{...props} {...props}
/> />
)) ));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export { export {
AlertDialog, AlertDialog,
@@ -136,4 +135,4 @@ export {
AlertDialogDescription, AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
} };

View File

@@ -1,5 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
const AspectRatio = AspectRatioPrimitive.Root const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio } export { AspectRatio };

View File

@@ -1,7 +1,6 @@
import * as React from "react" import * as React from 'react';
import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from '@/utils/cn';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from "@/utils/cn"
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -10,13 +9,13 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full", 'relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full',
className className
)} )}
{...props} {...props}
/> />
)) ));
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -24,11 +23,11 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
className={cn("aspect-square h-full w-full", className)} className={cn('aspect-square h-full w-full', className)}
{...props} {...props}
/> />
)) ));
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -37,12 +36,12 @@ 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-primary text-white", 'flex h-full w-full items-center justify-center rounded-full bg-primary text-white',
className className
)} )}
{...props} {...props}
/> />
)) ));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,27 +1,27 @@
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cn } from '@/utils/cn';
import { cva } from 'class-variance-authority';
import { cn } from "@/utils/cn" import type { VariantProps } from 'class-variance-authority';
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: "text-foreground", outline: 'text-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} }
) );
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant }), className)} {...props} />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@@ -1,37 +1,37 @@
import * as React from "react"; import * as React from 'react';
import { Slot } from "@radix-ui/react-slot"; import { cn } from '@/utils/cn';
import { cva, type VariantProps } from "class-variance-authority"; import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { cn } from "@/utils/cn"; import type { VariantProps } from 'class-variance-authority';
import { Loader2 } from "lucide-react"; import { Loader2 } from 'lucide-react';
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: 'hover:bg-accent hover:text-accent-foreground',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
}, }
); );
interface ButtonProps interface ButtonProps
@@ -43,10 +43,19 @@ interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
( (
{ className, variant, size, asChild = false, children, loading, disabled, ...props }, {
ref, className,
variant,
size,
asChild = false,
children,
loading,
disabled,
...props
},
ref
) => { ) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : 'button';
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
@@ -57,11 +66,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{loading ? <Loader2 className="animate-spin" /> : <>{children}</>} {loading ? <Loader2 className="animate-spin" /> : <>{children}</>}
</Comp> </Comp>
); );
}, }
); );
Button.displayName = "Button"; Button.displayName = 'Button';
Button.defaultProps = { Button.defaultProps = {
type: "button", type: 'button',
}; };
export { Button, buttonVariants }; export { Button, buttonVariants };

View File

@@ -1,8 +1,7 @@
import * as React from "react" import * as React from 'react';
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import { cn } from '@/utils/cn';
import { Check } from "lucide-react" import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import { cn } from "@/utils/cn"
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -11,18 +10,18 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", 'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className className
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")} className={cn('flex items-center justify-center text-current')}
> >
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
)) ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };

View File

@@ -1,58 +1,62 @@
import * as React from "react"; import * as React from 'react';
import { X } from "lucide-react"; import { Badge } from '@/components/ui/badge';
import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
import { Command as CommandPrimitive } from 'cmdk';
import { X } from 'lucide-react';
import { Badge } from "@/components/ui/badge"; type Item = Record<'value' | 'label', string>;
import {
Command,
CommandGroup,
CommandItem,
} from "@/components/ui/command";
import { Command as CommandPrimitive } from "cmdk";
type Item = Record<"value" | "label", string>; interface ComboboxMultiProps {
type ComboboxMultiProps = {
selected: Item[]; selected: Item[];
setSelected: React.Dispatch<React.SetStateAction<Item[]>>; setSelected: React.Dispatch<React.SetStateAction<Item[]>>;
items: Item[]; items: Item[];
placeholder: string placeholder: string;
} }
export function ComboboxMulti({ items, selected, setSelected, placeholder, ...props }: ComboboxMultiProps) { export function ComboboxMulti({
items,
selected,
setSelected,
placeholder,
...props
}: ComboboxMultiProps) {
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState(""); const [inputValue, setInputValue] = React.useState('');
const handleUnselect = React.useCallback((item: Item) => { const handleUnselect = React.useCallback((item: Item) => {
setSelected(prev => prev.filter(s => s.value !== item.value)); setSelected((prev) => prev.filter((s) => s.value !== item.value));
}, []); }, []);
const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLDivElement>) => { const handleKeyDown = React.useCallback(
const input = inputRef.current (e: React.KeyboardEvent<HTMLDivElement>) => {
if (input) { const input = inputRef.current;
if (e.key === "Delete" || e.key === "Backspace") { if (input) {
if (input.value === "") { if (e.key === 'Delete' || e.key === 'Backspace') {
setSelected(prev => { if (input.value === '') {
const newSelected = [...prev]; setSelected((prev) => {
newSelected.pop(); const newSelected = [...prev];
return newSelected; newSelected.pop();
}) return newSelected;
});
}
}
// This is not a default behaviour of the <input /> field
if (e.key === 'Escape') {
input.blur();
} }
} }
// This is not a default behaviour of the <input /> field },
if (e.key === "Escape") { []
input.blur(); );
}
} const selectables = items.filter(
}, []); (item) => !selected.find((s) => s.value === item.value)
);
const selectables = items.filter(item => !selected.find(s => s.value === item.value));
return ( return (
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-white"> <Command onKeyDown={handleKeyDown} className="overflow-visible bg-white">
<div <div className="group border border-input px-3 py-2 text-sm ring-offset-background rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
className="group border border-input px-3 py-2 text-sm ring-offset-background rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
>
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{selected.map((item) => { {selected.map((item) => {
return ( return (
@@ -61,7 +65,7 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
<button <button
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === 'Enter') {
handleUnselect(item); handleUnselect(item);
} }
}} }}
@@ -74,7 +78,7 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" /> <X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button> </button>
</Badge> </Badge>
) );
})} })}
{/* Avoid having the "Search" Icon */} {/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input <CommandPrimitive.Input
@@ -89,7 +93,7 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
</div> </div>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
{open && selectables.length > 0 ? {open && selectables.length > 0 ? (
<div className="absolute w-full z-10 top-0 rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in"> <div className="absolute w-full z-10 top-0 rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandGroup className="h-full overflow-auto"> <CommandGroup className="h-full overflow-auto">
{selectables.map((item) => { {selectables.map((item) => {
@@ -101,10 +105,10 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
e.stopPropagation(); e.stopPropagation();
}} }}
onSelect={(value) => { onSelect={(value) => {
setInputValue("") setInputValue('');
setSelected(prev => [...prev, item]) setSelected((prev) => [...prev, item]);
}} }}
className={"cursor-pointer"} className={'cursor-pointer'}
> >
{item.label} {item.label}
</CommandItem> </CommandItem>
@@ -112,8 +116,8 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
})} })}
</CommandGroup> </CommandGroup>
</div> </div>
: null} ) : null}
</div> </div>
</Command > </Command>
) );
} }

View File

@@ -1,32 +1,31 @@
import * as React from "react"; import * as React from 'react';
import { Check, ChevronsUpDown } from "lucide-react"; import { Button } from '@/components/ui/button';
import { cn } from "@/utils/cn";
import { Button } from "@/components/ui/button";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandInput, CommandInput,
CommandItem, CommandItem,
} from "@/components/ui/command"; } from '@/components/ui/command';
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from '@/components/ui/popover';
import { cn } from '@/utils/cn';
import { Check, ChevronsUpDown } from 'lucide-react';
type ComboboxProps = { interface ComboboxProps {
placeholder: string; placeholder: string;
items: Array<{ items: {
value: string; value: string;
label: string; label: string;
}>; }[];
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
children?: React.ReactNode; children?: React.ReactNode;
onCreate?: (value: string) => void; onCreate?: (value: string) => void;
}; }
export function Combobox({ export function Combobox({
placeholder, placeholder,
@@ -37,10 +36,10 @@ export function Combobox({
onCreate, onCreate,
}: ComboboxProps) { }: ComboboxProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState('');
function find(value: string) { function find(value: string) {
return items.find( return items.find(
(item) => item.value.toLowerCase() === value.toLowerCase(), (item) => item.value.toLowerCase() === value.toLowerCase()
); );
} }
@@ -55,7 +54,7 @@ export function Combobox({
className="w-full min-w-0 justify-between" className="w-full min-w-0 justify-between"
> >
<span className="overflow-hidden text-ellipsis"> <span className="overflow-hidden text-ellipsis">
{value ? find(value)?.label ?? "No match" : placeholder} {value ? find(value)?.label ?? 'No match' : placeholder}
</span> </span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@@ -63,14 +62,22 @@ export function Combobox({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full min-w-0 p-0" align="start"> <PopoverContent className="w-full min-w-0 p-0" align="start">
<Command> <Command>
<CommandInput placeholder="Search item..." value={search} onValueChange={setSearch} /> <CommandInput
{typeof onCreate === "function" && search ? ( placeholder="Search item..."
value={search}
onValueChange={setSearch}
/>
{typeof onCreate === 'function' && search ? (
<CommandEmpty className="p-2"> <CommandEmpty className="p-2">
<Button onClick={() => { <Button
onCreate(search) onClick={() => {
setSearch('') onCreate(search);
setOpen(false) setSearch('');
}}>Create &quot;{search}&quot;</Button> setOpen(false);
}}
>
Create &quot;{search}&quot;
</Button>
</CommandEmpty> </CommandEmpty>
) : ( ) : (
<CommandEmpty>Nothing selected</CommandEmpty> <CommandEmpty>Nothing selected</CommandEmpty>
@@ -88,8 +95,8 @@ export function Combobox({
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4 flex-shrink-0", 'mr-2 h-4 w-4 flex-shrink-0',
value === item.value ? "opacity-100" : "opacity-0", value === item.value ? 'opacity-100' : 'opacity-0'
)} )}
/> />
{item.label} {item.label}

View File

@@ -1,10 +1,9 @@
import * as React from "react" import * as React from 'react';
import { type DialogProps } from "@radix-ui/react-dialog" import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Command as CommandPrimitive } from "cmdk" import { cn } from '@/utils/cn';
import { Search } from "lucide-react" import type { DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk';
import { cn } from "@/utils/cn" import { Search } from 'lucide-react';
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,
@@ -13,15 +12,15 @@ const Command = React.forwardRef<
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", 'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
className className
)} )}
{...props} {...props}
/> />
)) ));
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (
@@ -32,8 +31,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} };
const CommandInput = React.forwardRef< const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
@@ -44,15 +43,15 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", 'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
{...props} {...props}
/> />
</div> </div>
)) ));
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef< const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>, React.ElementRef<typeof CommandPrimitive.List>,
@@ -60,12 +59,12 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...props} {...props}
/> />
)) ));
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef< const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, React.ElementRef<typeof CommandPrimitive.Empty>,
@@ -76,9 +75,9 @@ const CommandEmpty = React.forwardRef<
className="py-6 text-center text-sm" className="py-6 text-center text-sm"
{...props} {...props}
/> />
)) ));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef< const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>, React.ElementRef<typeof CommandPrimitive.Group>,
@@ -87,14 +86,14 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", 'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className className
)} )}
{...props} {...props}
/> />
)) ));
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
@@ -102,11 +101,11 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 h-px bg-border", className)} className={cn('-mx-1 h-px bg-border', className)}
{...props} {...props}
/> />
)) ));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef< const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>, React.ElementRef<typeof CommandPrimitive.Item>,
@@ -115,14 +114,14 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className
)} )}
{...props} {...props}
/> />
)) ));
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ const CommandShortcut = ({
className, className,
@@ -131,14 +130,14 @@ const CommandShortcut = ({
return ( return (
<span <span
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground", 'ml-auto text-xs tracking-widest text-muted-foreground',
className className
)} )}
{...props} {...props}
/> />
) );
} };
CommandShortcut.displayName = "CommandShortcut" CommandShortcut.displayName = 'CommandShortcut';
export { export {
Command, Command,
@@ -150,4 +149,4 @@ export {
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} };

View File

@@ -1,16 +1,15 @@
import * as React from "react" import * as React from 'react';
import * as DialogPrimitive from "@radix-ui/react-dialog" import { cn } from '@/utils/cn';
import { X } from "lucide-react" import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from "@/utils/cn" const Dialog = DialogPrimitive.Root;
const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger;
const DialogTrigger = DialogPrimitive.Trigger const DialogPortal = DialogPrimitive.Portal;
const DialogPortal = DialogPrimitive.Portal const DialogClose = DialogPrimitive.Close;
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -19,13 +18,13 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className className
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@@ -36,7 +35,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full", 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',
className className
)} )}
{...props} {...props}
@@ -48,8 +47,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({
className, className,
@@ -57,13 +56,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", 'flex flex-col space-y-1.5 text-center sm:text-left',
className className
)} )}
{...props} {...props}
/> />
) );
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ const DialogFooter = ({
className, className,
@@ -71,13 +70,13 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className className
)} )}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@@ -86,13 +85,13 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", 'text-lg font-semibold leading-none tracking-tight',
className className
)} )}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@@ -100,11 +99,11 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn('text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@@ -117,4 +116,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} };

View File

@@ -1,32 +1,31 @@
import * as React from "react" import * as React from 'react';
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { cn } from '@/utils/cn';
import { Check, ChevronRight, Circle } from "lucide-react" import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from "@/utils/cn" const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && "pl-8", inset && 'pl-8',
className className
)} )}
{...props} {...props}
@@ -34,9 +33,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -45,14 +44,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -63,32 +62,32 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && "pl-8", inset && 'pl-8',
className className
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -97,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className
)} )}
checked={checked} checked={checked}
@@ -110,9 +109,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -121,7 +120,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className className
)} )}
{...props} {...props}
@@ -133,26 +132,26 @@ const DropdownMenuRadioItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", 'px-2 py-1.5 text-sm font-semibold',
inset && "pl-8", inset && 'pl-8',
className className
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -160,11 +159,11 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props} {...props}
/> />
)) ));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({
className, className,
@@ -172,12 +171,12 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props} {...props}
/> />
) );
} };
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export { export {
DropdownMenu, DropdownMenu,
@@ -195,4 +194,4 @@ export {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} };

View File

@@ -1,8 +1,7 @@
import * as React from "react" import * as React from 'react';
import { cn } from '@/utils/cn';
import { cn } from "@/utils/cn" export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
@@ -10,15 +9,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Input.displayName = "Input" Input.displayName = 'Input';
export { Input } export { Input };

View File

@@ -1,12 +1,12 @@
import * as React from "react" import * as React from 'react';
import * as LabelPrimitive from "@radix-ui/react-label" import { cn } from '@/utils/cn';
import { cva, type VariantProps } from "class-variance-authority" import * as LabelPrimitive from '@radix-ui/react-label';
import { cva } from 'class-variance-authority';
import { cn } from "@/utils/cn" import type { VariantProps } from 'class-variance-authority';
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-2" 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-2'
) );
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
@@ -18,7 +18,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)} className={cn(labelVariants(), className)}
{...props} {...props}
/> />
)) ));
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };

View File

@@ -1,29 +1,28 @@
import * as React from "react" import * as React from 'react';
import * as PopoverPrimitive from "@radix-ui/react-popover" import { cn } from '@/utils/cn';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from "@/utils/cn" const Popover = PopoverPrimitive.Root;
const Popover = PopoverPrimitive.Root const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent } export { Popover, PopoverTrigger, PopoverContent };

View File

@@ -1,34 +1,45 @@
import * as React from "react" import * as React from 'react';
import { cn } from '@/utils/cn';
import { cn } from "@/utils/cn" export type RadioGroupProps = React.InputHTMLAttributes<HTMLDivElement>;
export type RadioGroupItemProps =
export type RadioGroupProps = React.InputHTMLAttributes<HTMLDivElement> React.InputHTMLAttributes<HTMLButtonElement> & {
export type RadioGroupItemProps = React.InputHTMLAttributes<HTMLButtonElement> & { active?: boolean;
active?: boolean };
}
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>( const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
return ( return (
<div <div
className={cn( className={cn(
"flex h-10 divide-x rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 'flex h-10 divide-x rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
const RadioGroupItem = React.forwardRef<HTMLButtonElement, RadioGroupItemProps>(({className, active, ...props}, ref) => { const RadioGroupItem = React.forwardRef<HTMLButtonElement, RadioGroupItemProps>(
return ( ({ className, active, ...props }, ref) => {
<button {...props} className={cn('flex-1 px-3 whitespace-nowrap leading-none hover:bg-slate-100 transition-colors font-medium', className, active && 'bg-slate-100')} type="button" ref={ref} /> return (
) <button
}) {...props}
className={cn(
'flex-1 px-3 whitespace-nowrap leading-none hover:bg-slate-100 transition-colors font-medium',
className,
active && 'bg-slate-100'
)}
type="button"
ref={ref}
/>
);
}
);
RadioGroup.displayName = "RadioGroup" RadioGroup.displayName = 'RadioGroup';
RadioGroupItem.displayName = "RadioGroupItem" RadioGroupItem.displayName = 'RadioGroupItem';
export { RadioGroup, RadioGroupItem } export { RadioGroup, RadioGroupItem };

View File

@@ -1,31 +1,32 @@
import * as React from "react" import * as React from 'react';
import { cn } from '@/utils/cn';
import { cn } from "@/utils/cn"
const Table = React.forwardRef< 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="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 [&.mini]:text-xs", className)} className={cn(
{...props} 'w-full caption-bottom text-sm [&.mini]:text-xs',
/> className
)}
{...props}
/>
</div>
</div> </div>
));
</div> Table.displayName = 'Table';
))
Table.displayName = "Table"
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<thead ref={ref} className={className} {...props} /> <thead ref={ref} className={className} {...props} />
)) ));
TableHeader.displayName = "TableHeader" TableHeader.displayName = 'TableHeader';
const TableBody = React.forwardRef< const TableBody = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@@ -33,11 +34,11 @@ const TableBody = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tbody <tbody
ref={ref} ref={ref}
className={cn("[&_tr:last-child]:border-0", className)} className={cn('[&_tr:last-child]:border-0', className)}
{...props} {...props}
/> />
)) ));
TableBody.displayName = "TableBody" TableBody.displayName = 'TableBody';
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
@@ -45,11 +46,11 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)} className={cn('bg-primary font-medium text-primary-foreground', className)}
{...props} {...props}
/> />
)) ));
TableFooter.displayName = "TableFooter" TableFooter.displayName = 'TableFooter';
const TableRow = React.forwardRef< const TableRow = React.forwardRef<
HTMLTableRowElement, HTMLTableRowElement,
@@ -58,13 +59,13 @@ const TableRow = React.forwardRef<
<tr <tr
ref={ref} ref={ref}
className={cn( className={cn(
"transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", 'transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
className className
)} )}
{...props} {...props}
/> />
)) ));
TableRow.displayName = "TableRow" TableRow.displayName = 'TableRow';
const TableHead = React.forwardRef< const TableHead = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@@ -73,13 +74,13 @@ const TableHead = React.forwardRef<
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"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", '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}
/> />
)) ));
TableHead.displayName = "TableHead" TableHead.displayName = 'TableHead';
const TableCell = React.forwardRef< const TableCell = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
@@ -87,11 +88,14 @@ 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 shadow-[0_0_0_0.5px] shadow-border [.mini_&]:p-2", 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}
/> />
)) ));
TableCell.displayName = "TableCell" TableCell.displayName = 'TableCell';
const TableCaption = React.forwardRef< const TableCaption = React.forwardRef<
HTMLTableCaptionElement, HTMLTableCaptionElement,
@@ -99,11 +103,11 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption
ref={ref} ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)} className={cn('mt-4 text-sm text-muted-foreground', className)}
{...props} {...props}
/> />
)) ));
TableCaption.displayName = "TableCaption" TableCaption.displayName = 'TableCaption';
export { export {
Table, Table,
@@ -114,4 +118,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@@ -1,11 +1,11 @@
import * as React from "react" import * as React from 'react';
import * as ToastPrimitives from "@radix-ui/react-toast" import { cn } from '@/utils/cn';
import { cva, type VariantProps } from "class-variance-authority" import * as ToastPrimitives from '@radix-ui/react-toast';
import { X } from "lucide-react" import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import { cn } from "@/utils/cn" const ToastProvider = ToastPrimitives.Provider;
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef< const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>, React.ElementRef<typeof ToastPrimitives.Viewport>,
@@ -14,29 +14,29 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport <ToastPrimitives.Viewport
ref={ref} ref={ref}
className={cn( className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", 'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className className
)} )}
{...props} {...props}
/> />
)) ));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{ {
variants: { variants: {
variant: { variant: {
default: "border bg-background text-foreground", default: 'border bg-background text-foreground',
destructive: destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground", 'destructive group border-destructive bg-destructive text-destructive-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} }
) );
const Toast = React.forwardRef< const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>, React.ElementRef<typeof ToastPrimitives.Root>,
@@ -49,9 +49,9 @@ const Toast = React.forwardRef<
className={cn(toastVariants({ variant }), className)} className={cn(toastVariants({ variant }), className)}
{...props} {...props}
/> />
) );
}) });
Toast.displayName = ToastPrimitives.Root.displayName Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef< const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>, React.ElementRef<typeof ToastPrimitives.Action>,
@@ -60,13 +60,13 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action <ToastPrimitives.Action
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", 'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className className
)} )}
{...props} {...props}
/> />
)) ));
ToastAction.displayName = ToastPrimitives.Action.displayName ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef< const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>, React.ElementRef<typeof ToastPrimitives.Close>,
@@ -75,7 +75,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={ref}
className={cn( className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", 'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className className
)} )}
toast-close="" toast-close=""
@@ -83,8 +83,8 @@ const ToastClose = React.forwardRef<
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</ToastPrimitives.Close> </ToastPrimitives.Close>
)) ));
ToastClose.displayName = ToastPrimitives.Close.displayName ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef< const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>, React.ElementRef<typeof ToastPrimitives.Title>,
@@ -92,11 +92,11 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Title <ToastPrimitives.Title
ref={ref} ref={ref}
className={cn("text-sm font-semibold", className)} className={cn('text-sm font-semibold', className)}
{...props} {...props}
/> />
)) ));
ToastTitle.displayName = ToastPrimitives.Title.displayName ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef< const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>, React.ElementRef<typeof ToastPrimitives.Description>,
@@ -104,15 +104,15 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Description <ToastPrimitives.Description
ref={ref} ref={ref}
className={cn("text-sm opacity-90", className)} className={cn('text-sm opacity-90', className)}
{...props} {...props}
/> />
)) ));
ToastDescription.displayName = ToastPrimitives.Description.displayName ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction> type ToastActionElement = React.ReactElement<typeof ToastAction>;
export { export {
type ToastProps, type ToastProps,
@@ -124,4 +124,4 @@ export {
ToastDescription, ToastDescription,
ToastClose, ToastClose,
ToastAction, ToastAction,
} };

View File

@@ -5,11 +5,11 @@ import {
ToastProvider, ToastProvider,
ToastTitle, ToastTitle,
ToastViewport, ToastViewport,
} from "@/components/ui/toast" } from '@/components/ui/toast';
import { useToast } from "@/components/ui/use-toast" import { useToast } from '@/components/ui/use-toast';
export function Toaster() { export function Toaster() {
const { toasts } = useToast() const { toasts } = useToast();
return ( return (
<ToastProvider> <ToastProvider>
@@ -25,9 +25,9 @@ export function Toaster() {
{action} {action}
<ToastClose /> <ToastClose />
</Toast> </Toast>
) );
})} })}
<ToastViewport /> <ToastViewport />
</ToastProvider> </ToastProvider>
) );
} }

View File

@@ -1,13 +1,12 @@
import * as React from "react" import * as React from 'react';
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import { cn } from '@/utils/cn';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from "@/utils/cn" const TooltipProvider = TooltipPrimitive.Provider;
const TooltipProvider = TooltipPrimitive.Provider const Tooltip = TooltipPrimitive.Root;
const Tooltip = TooltipPrimitive.Root const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -17,12 +16,12 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className className
)} )}
{...props} {...props}
/> />
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -123,7 +123,7 @@ export const reducer = (state: State, action: Action): State => {
} }
}; };
const listeners: Array<(state: State) => void> = []; const listeners: ((state: State) => void)[] = [];
let memoryState: State = { toasts: [] }; let memoryState: State = { toasts: [] };

View File

@@ -1,12 +1,13 @@
import { api, handleError } from "@/utils/api"; import { ContentHeader, ContentSection } from '@/components/Content';
import { Input } from "@/components/ui/input"; import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button"; import { Input } from '@/components/ui/input';
import { ContentHeader, ContentSection } from "@/components/Content"; import { toast } from '@/components/ui/use-toast';
import { useForm } from "react-hook-form"; import { api, handleError } from '@/utils/api';
import { toast } from "@/components/ui/use-toast"; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from "zod"; import { useForm } from 'react-hook-form';
import { zodResolver } from "@hookform/resolvers/zod"; import { z } from 'zod';
import { InputError } from "../forms/InputError";
import { InputError } from '../forms/InputError';
const validator = z const validator = z
.object({ .object({
@@ -17,9 +18,9 @@ const validator = z
.superRefine(({ confirmPassword, password }, ctx) => { .superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) { if (confirmPassword !== password) {
ctx.addIssue({ ctx.addIssue({
path: ["confirmPassword"], path: ['confirmPassword'],
code: "custom", code: 'custom',
message: "The passwords did not match", message: 'The passwords did not match',
}); });
} }
}); });
@@ -30,8 +31,8 @@ export function ChangePassword() {
const mutation = api.user.changePassword.useMutation({ const mutation = api.user.changePassword.useMutation({
onSuccess() { onSuccess() {
toast({ toast({
title: "Success", title: 'Success',
description: "You have updated your password", description: 'You have updated your password',
}); });
}, },
onError: handleError, onError: handleError,
@@ -40,16 +41,16 @@ export function ChangePassword() {
const { register, handleSubmit, formState } = useForm<IForm>({ const { register, handleSubmit, formState } = useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
oldPassword: "", oldPassword: '',
password: "", password: '',
confirmPassword: "", confirmPassword: '',
}, },
}); });
return ( return (
<form <form
onSubmit={handleSubmit((values) => { onSubmit={handleSubmit((values) => {
mutation.mutate(values) mutation.mutate(values);
})} })}
className="flex flex-col divide-y divide-border" className="flex flex-col divide-y divide-border"
> >
@@ -57,26 +58,37 @@ export function ChangePassword() {
title="Change password" title="Change password"
text="Need to change your password?" text="Need to change your password?"
> >
<Button type="submit" disabled={!formState.isDirty}>Change it!</Button> <Button type="submit" disabled={!formState.isDirty}>
Change it!
</Button>
</ContentHeader> </ContentHeader>
<ContentSection title="Old password" text={<InputError {...formState.errors.oldPassword}/>}> <ContentSection
title="Old password"
text={<InputError {...formState.errors.oldPassword} />}
>
<Input <Input
type="password" type="password"
{...register("oldPassword")} {...register('oldPassword')}
placeholder="Old password" placeholder="Old password"
/> />
</ContentSection> </ContentSection>
<ContentSection title="New password" text={<InputError {...formState.errors.password}/>}> <ContentSection
title="New password"
text={<InputError {...formState.errors.password} />}
>
<Input <Input
type="password" type="password"
{...register("password")} {...register('password')}
placeholder="New password" placeholder="New password"
/> />
</ContentSection> </ContentSection>
<ContentSection title="Confirm password" text={<InputError {...formState.errors.confirmPassword}/>}> <ContentSection
title="Confirm password"
text={<InputError {...formState.errors.confirmPassword} />}
>
<Input <Input
type="password" type="password"
{...register("confirmPassword")} {...register('confirmPassword')}
placeholder="Confirm password" placeholder="Confirm password"
/> />
</ContentSection> </ContentSection>

View File

@@ -1,4 +1,4 @@
import { type IInterval } from '@/types'; import type { IInterval } from '@/types';
export function formatDateInterval(interval: IInterval, date: Date): string { export function formatDateInterval(interval: IInterval, date: Date): string {
if (interval === 'hour' || interval === 'minute') { if (interval === 'hour' || interval === 'minute') {

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { type z } from 'zod'; import type { z } from 'zod';
export function useQueryParams<Z extends z.ZodTypeAny = z.ZodNever>(zod: Z) { export function useQueryParams<Z extends z.ZodTypeAny = z.ZodNever>(zod: Z) {
const router = useRouter(); const router = useRouter();

View File

@@ -1,32 +1,33 @@
import { api, handleError } from "@/utils/api"; import { ButtonContainer } from '@/components/ButtonContainer';
import { ModalContent, ModalHeader } from "./Modal/Container"; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Controller, useForm } from "react-hook-form"; import { Button } from '@/components/ui/button';
import { z } from "zod"; import { Combobox } from '@/components/ui/combobox';
import { zodResolver } from "@hookform/resolvers/zod"; import { Label } from '@/components/ui/label';
import { Button } from "@/components/ui/button"; import { toast } from '@/components/ui/use-toast';
import { ButtonContainer } from "@/components/ButtonContainer"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { popModal } from "."; import { useRefetchActive } from '@/hooks/useRefetchActive';
import { toast } from "@/components/ui/use-toast"; import { api, handleError } from '@/utils/api';
import { InputWithLabel } from "@/components/forms/InputWithLabel"; import { clipboard } from '@/utils/clipboard';
import { useRefetchActive } from "@/hooks/useRefetchActive"; import { zodResolver } from '@hookform/resolvers/zod';
import { Combobox } from "@/components/ui/combobox"; import { Copy } from 'lucide-react';
import { Label } from "@/components/ui/label"; import dynamic from 'next/dynamic';
import { Copy } from "lucide-react"; import { Controller, useForm } from 'react-hook-form';
import { clipboard } from "@/utils/clipboard"; import { z } from 'zod';
import dynamic from "next/dynamic";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
const Syntax = dynamic(import('@/components/Syntax')) import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
const Syntax = dynamic(import('@/components/Syntax'));
const validator = z.object({ const validator = z.object({
name: z.string().min(1, "Required"), name: z.string().min(1, 'Required'),
projectId: z.string().min(1, "Required"), projectId: z.string().min(1, 'Required'),
}); });
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
export default function CreateProject() { export default function CreateProject() {
const params = useOrganizationParams() const params = useOrganizationParams();
const refetch = useRefetchActive(); const refetch = useRefetchActive();
const query = api.project.list.useQuery({ const query = api.project.list.useQuery({
organizationSlug: params.organization, organizationSlug: params.organization,
@@ -35,8 +36,8 @@ export default function CreateProject() {
onError: handleError, onError: handleError,
onSuccess() { onSuccess() {
toast({ toast({
title: "Success", title: 'Success',
description: "Client created!", description: 'Client created!',
}); });
refetch(); refetch();
}, },
@@ -44,8 +45,8 @@ export default function CreateProject() {
const { register, handleSubmit, formState, control } = useForm<IForm>({ const { register, handleSubmit, formState, control } = useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
name: "", name: '',
projectId: "", projectId: '',
}, },
}); });
@@ -110,7 +111,7 @@ export default function CreateProject() {
<InputWithLabel <InputWithLabel
label="Name" label="Name"
placeholder="Name" placeholder="Name"
{...register("name")} {...register('name')}
className="mb-4" className="mb-4"
/> />
<Controller <Controller

View File

@@ -1,15 +1,16 @@
import { api, handleError } from "@/utils/api"; import { ButtonContainer } from '@/components/ButtonContainer';
import { ModalContent, ModalHeader } from "./Modal/Container"; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { useForm } from "react-hook-form"; import { Button } from '@/components/ui/button';
import { z } from "zod"; import { toast } from '@/components/ui/use-toast';
import { zodResolver } from "@hookform/resolvers/zod"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { Button } from "@/components/ui/button"; import { useRefetchActive } from '@/hooks/useRefetchActive';
import { ButtonContainer } from "@/components/ButtonContainer"; import { api, handleError } from '@/utils/api';
import { popModal } from "."; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from "@/components/ui/use-toast"; import { useForm } from 'react-hook-form';
import { InputWithLabel } from "@/components/forms/InputWithLabel"; import { z } from 'zod';
import { useRefetchActive } from "@/hooks/useRefetchActive";
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
const validator = z.object({ const validator = z.object({
name: z.string().min(1), name: z.string().min(1),
@@ -18,23 +19,23 @@ const validator = z.object({
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
export default function AddProject() { export default function AddProject() {
const params = useOrganizationParams() const params = useOrganizationParams();
const refetch = useRefetchActive() const refetch = useRefetchActive();
const mutation = api.project.create.useMutation({ const mutation = api.project.create.useMutation({
onError: handleError, onError: handleError,
onSuccess() { onSuccess() {
toast({ toast({
title: 'Success', title: 'Success',
description: 'Project created! Lets create a client for it 🤘', description: 'Project created! Lets create a client for it 🤘',
}) });
refetch() refetch();
popModal() popModal();
} },
}); });
const { register, handleSubmit, formState } = useForm<IForm>({ const { register, handleSubmit, formState } = useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
name: "", name: '',
}, },
}); });
@@ -51,8 +52,12 @@ export default function AddProject() {
> >
<InputWithLabel label="Name" placeholder="Name" {...register('name')} /> <InputWithLabel label="Name" placeholder="Name" {...register('name')} />
<ButtonContainer> <ButtonContainer>
<Button type="button" variant="outline" onClick={() => popModal()}>Cancel</Button> <Button type="button" variant="outline" onClick={() => popModal()}>
<Button type="submit" disabled={!formState.isDirty}>Create</Button> Cancel
</Button>
<Button type="submit" disabled={!formState.isDirty}>
Create
</Button>
</ButtonContainer> </ButtonContainer>
</form> </form>
</ModalContent> </ModalContent>

View File

@@ -1,14 +1,15 @@
import { ModalContent, ModalHeader } from "./Modal/Container"; import { ButtonContainer } from '@/components/ButtonContainer';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { ButtonContainer } from "@/components/ButtonContainer";
import { popModal } from ".";
export type ConfirmProps = { import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
export interface ConfirmProps {
title: string; title: string;
text: string; text: string;
onConfirm: () => void; onConfirm: () => void;
onCancel?: () => void; onCancel?: () => void;
}; }
export default function Confirm({ export default function Confirm({
title, title,
@@ -24,7 +25,7 @@ export default function Confirm({
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
popModal("Confirm"); popModal('Confirm');
onCancel?.(); onCancel?.();
}} }}
> >
@@ -32,7 +33,7 @@ export default function Confirm({
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
popModal("Confirm"); popModal('Confirm');
onConfirm(); onConfirm();
}} }}
> >

View File

@@ -1,19 +1,20 @@
import { api, handleError } from "@/utils/api"; import { useEffect } from 'react';
import { ModalContent, ModalHeader } from "./Modal/Container"; import { ButtonContainer } from '@/components/ButtonContainer';
import { useForm } from "react-hook-form"; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { z } from "zod"; import { Button } from '@/components/ui/button';
import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from '@/components/ui/use-toast';
import { useEffect } from "react"; import { useRefetchActive } from '@/hooks/useRefetchActive';
import { Button } from "@/components/ui/button"; import { api, handleError } from '@/utils/api';
import { ButtonContainer } from "@/components/ButtonContainer"; import { zodResolver } from '@hookform/resolvers/zod';
import { popModal } from "."; import { useForm } from 'react-hook-form';
import { toast } from "@/components/ui/use-toast"; import { z } from 'zod';
import { InputWithLabel } from "@/components/forms/InputWithLabel";
import { useRefetchActive } from "@/hooks/useRefetchActive";
type EditClientProps = { import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
interface EditClientProps {
id: string; id: string;
}; }
const validator = z.object({ const validator = z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -23,25 +24,25 @@ const validator = z.object({
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
export default function EditClient({ id }: EditClientProps) { export default function EditClient({ id }: EditClientProps) {
const refetch = useRefetchActive() const refetch = useRefetchActive();
const mutation = api.client.update.useMutation({ const mutation = api.client.update.useMutation({
onError: handleError, onError: handleError,
onSuccess() { onSuccess() {
toast({ toast({
title: 'Success', title: 'Success',
description: 'Client updated.', description: 'Client updated.',
}) });
popModal() popModal();
refetch() refetch();
} },
}); });
const query = api.client.get.useQuery({ id }); const query = api.client.get.useQuery({ id });
const data = query.data; const data = query.data;
const { register, handleSubmit, reset, formState } = useForm<IForm>({ const { register, handleSubmit, reset, formState } = useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
id: "", id: '',
name: "", name: '',
}, },
}); });
@@ -61,8 +62,12 @@ export default function EditClient({ id }: EditClientProps) {
> >
<InputWithLabel label="Name" placeholder="Name" {...register('name')} /> <InputWithLabel label="Name" placeholder="Name" {...register('name')} />
<ButtonContainer> <ButtonContainer>
<Button type="button" variant="outline" onClick={() => popModal()}>Cancel</Button> <Button type="button" variant="outline" onClick={() => popModal()}>
<Button type="submit" disabled={!formState.isDirty}>Save</Button> Cancel
</Button>
<Button type="submit" disabled={!formState.isDirty}>
Save
</Button>
</ButtonContainer> </ButtonContainer>
</form> </form>
</ModalContent> </ModalContent>

View File

@@ -1,19 +1,20 @@
import { api, handleError } from "@/utils/api"; import { useEffect } from 'react';
import { ModalContent, ModalHeader } from "./Modal/Container"; import { ButtonContainer } from '@/components/ButtonContainer';
import { useForm } from "react-hook-form"; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { z } from "zod"; import { Button } from '@/components/ui/button';
import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from '@/components/ui/use-toast';
import { useEffect } from "react"; import { useRefetchActive } from '@/hooks/useRefetchActive';
import { Button } from "@/components/ui/button"; import { api, handleError } from '@/utils/api';
import { ButtonContainer } from "@/components/ButtonContainer"; import { zodResolver } from '@hookform/resolvers/zod';
import { popModal } from "."; import { useForm } from 'react-hook-form';
import { toast } from "@/components/ui/use-toast"; import { z } from 'zod';
import { InputWithLabel } from "@/components/forms/InputWithLabel";
import { useRefetchActive } from "@/hooks/useRefetchActive";
type EditProjectProps = { import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
interface EditProjectProps {
id: string; id: string;
}; }
const validator = z.object({ const validator = z.object({
id: z.string().min(1), id: z.string().min(1),
@@ -23,25 +24,25 @@ const validator = z.object({
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
export default function EditProject({ id }: EditProjectProps) { export default function EditProject({ id }: EditProjectProps) {
const refetch = useRefetchActive() const refetch = useRefetchActive();
const mutation = api.project.update.useMutation({ const mutation = api.project.update.useMutation({
onError: handleError, onError: handleError,
onSuccess() { onSuccess() {
toast({ toast({
title: 'Success', title: 'Success',
description: 'Project updated.', description: 'Project updated.',
}) });
popModal() popModal();
refetch() refetch();
} },
}); });
const query = api.project.get.useQuery({ id }); const query = api.project.get.useQuery({ id });
const data = query.data; const data = query.data;
const { register, handleSubmit, reset,formState } = useForm<IForm>({ const { register, handleSubmit, reset, formState } = useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
id: "", id: '',
name: "", name: '',
}, },
}); });
@@ -61,8 +62,12 @@ export default function EditProject({ id }: EditProjectProps) {
> >
<InputWithLabel label="Name" placeholder="Name" {...register('name')} /> <InputWithLabel label="Name" placeholder="Name" {...register('name')} />
<ButtonContainer> <ButtonContainer>
<Button type="button" variant="outline" onClick={() => popModal()}>Cancel</Button> <Button type="button" variant="outline" onClick={() => popModal()}>
<Button type="submit" disabled={!formState.isDirty}>Save</Button> Cancel
</Button>
<Button type="submit" disabled={!formState.isDirty}>
Save
</Button>
</ButtonContainer> </ButtonContainer>
</form> </form>
</ModalContent> </ModalContent>

View File

@@ -1,10 +1,11 @@
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { X } from "lucide-react"; import { X } from 'lucide-react';
import { popModal } from "..";
type ModalContentProps = { import { popModal } from '..';
interface ModalContentProps {
children: React.ReactNode; children: React.ReactNode;
}; }
export function ModalContent({ children }: ModalContentProps) { export function ModalContent({ children }: ModalContentProps) {
return ( return (
@@ -14,9 +15,9 @@ export function ModalContent({ children }: ModalContentProps) {
); );
} }
type ModalHeaderProps = { interface ModalHeaderProps {
title: string | React.ReactNode; title: string | React.ReactNode;
}; }
export function ModalHeader({ title }: ModalHeaderProps) { export function ModalHeader({ title }: ModalHeaderProps) {
return ( return (

View File

@@ -1,47 +1,48 @@
import { api, handleError } from "@/utils/api"; import { ButtonContainer } from '@/components/ButtonContainer';
import { ModalContent, ModalHeader } from "./Modal/Container"; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Controller, useForm, useWatch } from "react-hook-form"; import { Button } from '@/components/ui/button';
import { z } from "zod"; import { Combobox } from '@/components/ui/combobox';
import { zodResolver } from "@hookform/resolvers/zod"; import { Label } from '@/components/ui/label';
import { Button } from "@/components/ui/button"; import { toast } from '@/components/ui/use-toast';
import { ButtonContainer } from "@/components/ButtonContainer"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { popModal } from "."; import { useRefetchActive } from '@/hooks/useRefetchActive';
import { toast } from "@/components/ui/use-toast"; import type { IChartInput } from '@/types';
import { InputWithLabel } from "@/components/forms/InputWithLabel"; import { api, handleError } from '@/utils/api';
import { useRefetchActive } from "@/hooks/useRefetchActive"; import { zodResolver } from '@hookform/resolvers/zod';
import { type IChartInput } from "@/types"; import { useRouter } from 'next/router';
import { Label } from "@/components/ui/label"; import { Controller, useForm, useWatch } from 'react-hook-form';
import { Combobox } from "@/components/ui/combobox"; import { z } from 'zod';
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import { useRouter } from "next/router";
type SaveReportProps = { import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
interface SaveReportProps {
report: IChartInput; report: IChartInput;
reportId?: string; reportId?: string;
}; }
const validator = z.object({ const validator = z.object({
name: z.string().min(1, "Required"), name: z.string().min(1, 'Required'),
projectId: z.string().min(1, "Required"), projectId: z.string().min(1, 'Required'),
dashboardId: z.string().min(1, "Required"), dashboardId: z.string().min(1, 'Required'),
}); });
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
export default function SaveReport({ report }: SaveReportProps) { export default function SaveReport({ report }: SaveReportProps) {
const router = useRouter() const router = useRouter();
const { organization } = useOrganizationParams(); const { organization } = useOrganizationParams();
const refetch = useRefetchActive(); const refetch = useRefetchActive();
const save = api.report.save.useMutation({ const save = api.report.save.useMutation({
onError: handleError, onError: handleError,
onSuccess(res) { onSuccess(res) {
toast({ toast({
title: "Success", title: 'Success',
description: "Report saved.", description: 'Report saved.',
}); });
popModal(); popModal();
refetch(); refetch();
router.push(`/${organization}/reports/${res.id}`) router.push(`/${organization}/reports/${res.id}`);
}, },
}); });
@@ -49,26 +50,26 @@ export default function SaveReport({ report }: SaveReportProps) {
useForm<IForm>({ useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
name: "", name: '',
projectId: "", projectId: '',
dashboardId: "", dashboardId: '',
}, },
}); });
const dashboardMutation = api.dashboard.create.useMutation({ const dashboardMutation = api.dashboard.create.useMutation({
onError: handleError, onError: handleError,
onSuccess(res) { onSuccess(res) {
setValue("dashboardId", res.id); setValue('dashboardId', res.id);
dashboasrdQuery.refetch(); dashboasrdQuery.refetch();
toast({ toast({
title: "Success", title: 'Success',
description: "Dashboard created.", description: 'Dashboard created.',
}); });
}, },
}); });
const projectId = useWatch({ const projectId = useWatch({
name: "projectId", name: 'projectId',
control, control,
}); });
@@ -82,7 +83,7 @@ export default function SaveReport({ report }: SaveReportProps) {
}, },
{ {
enabled: !!projectId, enabled: !!projectId,
}, }
); );
const projects = (projectQuery.data ?? []).map((item) => ({ const projects = (projectQuery.data ?? []).map((item) => ({
@@ -113,7 +114,7 @@ export default function SaveReport({ report }: SaveReportProps) {
<InputWithLabel <InputWithLabel
label="Report name" label="Report name"
placeholder="Name" placeholder="Name"
{...register("name")} {...register('name')}
defaultValue={report.name} defaultValue={report.name}
/> />
<Controller <Controller

View File

@@ -1,14 +1,14 @@
import { Suspense, useEffect, useRef, useState } from 'react'; import { Suspense, useEffect, useRef, useState } from 'react';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import mitt from 'mitt'; import mitt from 'mitt';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useOnClickOutside } from 'usehooks-ts'; import { useOnClickOutside } from 'usehooks-ts';
import { type ConfirmProps } from './Confirm';
import type { ConfirmProps } from './Confirm';
const Loading = () => ( const Loading = () => (
<div className='fixed top-0 z-50 flex h-screen w-screen items-center justify-center overflow-auto bg-backdrop'> <div className="fixed top-0 z-50 flex h-screen w-screen items-center justify-center overflow-auto bg-backdrop">
<Loader className='mb-8 animate-spin' size={40} /> <Loader className="mb-8 animate-spin" size={40} />
</div> </div>
); );
@@ -48,26 +48,29 @@ const emitter = mitt<{
type ModalRoutes = keyof typeof modals; type ModalRoutes = keyof typeof modals;
type StateItem = { interface StateItem {
key: string; key: string;
name: ModalRoutes; name: ModalRoutes;
props: Record<string, unknown>; props: Record<string, unknown>;
}; }
type ModalWrapperProps = { interface ModalWrapperProps {
children: React.ReactNode; children: React.ReactNode;
name: ModalRoutes; name: ModalRoutes;
isOnTop: boolean; isOnTop: boolean;
}; }
function ModalWrapper({ children, name, isOnTop }: ModalWrapperProps) { function ModalWrapper({ children, name, isOnTop }: ModalWrapperProps) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, (event) => { useOnClickOutside(ref, (event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const isPortal = typeof target.closest === 'function' ? !!target.closest('[data-radix-popper-content-wrapper]') : false const isPortal =
typeof target.closest === 'function'
if (isOnTop && !isPortal) { ? !!target.closest('[data-radix-popper-content-wrapper]')
: false;
if (isOnTop && !isPortal) {
emitter.emit('pop', { emitter.emit('pop', {
name, name,
}); });
@@ -75,8 +78,8 @@ function ModalWrapper({ children, name, isOnTop }: ModalWrapperProps) {
}); });
return ( return (
<div className='fixed top-0 z-50 flex h-screen w-screen items-center justify-center overflow-auto'> <div className="fixed top-0 z-50 flex h-screen w-screen items-center justify-center overflow-auto">
<div ref={ref} className='w-inherit m-auto py-4'> <div ref={ref} className="w-inherit m-auto py-4">
<Suspense>{children}</Suspense> <Suspense>{children}</Suspense>
</div> </div>
</div> </div>
@@ -141,7 +144,9 @@ export function ModalProvider() {
}); });
return ( return (
<> <>
{!!state.length && <div className="fixed top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.2)]"></div>} {!!state.length && (
<div className="fixed top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.2)]"></div>
)}
{state.map((item, index) => { {state.map((item, index) => {
const Modal = modals[item.name]; const Modal = modals[item.name];
return ( return (
@@ -168,7 +173,7 @@ type OrUndefined<T> = T extends Record<string, never> ? undefined : T;
export const pushModal = < export const pushModal = <
T extends StateItem['name'], T extends StateItem['name'],
B extends OrUndefined<GetComponentProps<(typeof modals)[T]>> B extends OrUndefined<GetComponentProps<(typeof modals)[T]>>,
>( >(
name: T, name: T,
...rest: B extends undefined ? [] : [B] ...rest: B extends undefined ? [] : [B]
@@ -179,7 +184,7 @@ export const pushModal = <
}); });
export const replaceModal = < export const replaceModal = <
T extends StateItem['name'], T extends StateItem['name'],
B extends OrUndefined<GetComponentProps<(typeof modals)[T]>> B extends OrUndefined<GetComponentProps<(typeof modals)[T]>>,
>( >(
name: T, name: T,
...rest: B extends undefined ? [] : [B] ...rest: B extends undefined ? [] : [B]
@@ -197,4 +202,4 @@ export const unshiftModal = (name: StateItem['name']) =>
name, name,
}); });
export const showConfirm = (props: ConfirmProps) => pushModal('Confirm', props); export const showConfirm = (props: ConfirmProps) => pushModal('Confirm', props);

View File

@@ -1,16 +1,16 @@
import { MainLayout } from "@/components/layouts/MainLayout"; import { Suspense, useMemo, useState } from 'react';
import { Container } from "@/components/Container"; import { Container } from '@/components/Container';
import { api } from "@/utils/api"; import { MainLayout } from '@/components/layouts/MainLayout';
import Link from "next/link"; import { PageTitle } from '@/components/PageTitle';
import { PageTitle } from "@/components/PageTitle"; import { Chart } from '@/components/report/chart';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Suspense, useMemo, useState } from "react"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { createServerSideProps } from "@/server/getServerSideProps"; import { createServerSideProps } from '@/server/getServerSideProps';
import { Chart } from "@/components/report/chart"; import type { IChartRange } from '@/types';
import { timeRanges } from "@/utils/constants"; import { api } from '@/utils/api';
import { type IChartRange } from "@/types"; import { timeRanges } from '@/utils/constants';
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { getRangeLabel } from '@/utils/getRangeLabel';
import { getRangeLabel } from "@/utils/getRangeLabel"; import Link from 'next/link';
export const getServerSideProps = createServerSideProps(); export const getServerSideProps = createServerSideProps();
@@ -66,7 +66,9 @@ export default function Dashboard() {
<div className="font-medium">{report.name}</div> <div className="font-medium">{report.name}</div>
{chartRange && ( {chartRange && (
<div className="mt-2 text-sm flex gap-2"> <div className="mt-2 text-sm flex gap-2">
<span className={range ? "line-through" : ""}>{chartRange}</span> <span className={range ? 'line-through' : ''}>
{chartRange}
</span>
{range && <span>{getRangeLabel(range)}</span>} {range && <span>{getRangeLabel(range)}</span>}
</div> </div>
)} )}

View File

@@ -1,12 +1,11 @@
import { Container } from "@/components/Container"; import { useMemo } from 'react';
import { PageTitle } from "@/components/PageTitle"; import { Container } from '@/components/Container';
import { usePagination } from "@/components/Pagination"; import { EventsTable } from '@/components/events/EventsTable';
import { EventsTable } from "@/components/events/EventsTable"; import { MainLayout } from '@/components/layouts/MainLayout';
import { MainLayout } from "@/components/layouts/MainLayout"; import { PageTitle } from '@/components/PageTitle';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { usePagination } from '@/components/Pagination';
import { api } from "@/utils/api"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { api } from '@/utils/api';
import { useMemo } from "react";
export default function Events() { export default function Events() {
const pagination = usePagination(); const pagination = usePagination();
@@ -18,7 +17,7 @@ export default function Events() {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
}, }
); );
const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]); const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);

View File

@@ -1,21 +1,24 @@
import { MainLayout } from "@/components/layouts/MainLayout"; import { Card } from '@/components/Card';
import { Container } from "@/components/Container"; import { Container } from '@/components/Container';
import { api } from "@/utils/api"; import { MainLayout } from '@/components/layouts/MainLayout';
import Link from "next/link"; import { PageTitle } from '@/components/PageTitle';
import { PageTitle } from "@/components/PageTitle"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { Card } from "@/components/Card"; import { createServerSideProps } from '@/server/getServerSideProps';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { api } from '@/utils/api';
import { createServerSideProps } from "@/server/getServerSideProps"; import Link from 'next/link';
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();
export default function Home() { export default function Home() {
const params = useOrganizationParams(); const params = useOrganizationParams();
const query = api.dashboard.list.useQuery({ const query = api.dashboard.list.useQuery(
projectSlug: params.project, {
}, { projectSlug: params.project,
enabled: Boolean(params.organization && params.project), },
}); {
enabled: Boolean(params.organization && params.project),
}
);
const dashboards = query.data ?? []; const dashboards = query.data ?? [];
return ( return (
@@ -25,13 +28,13 @@ export default function Home() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{dashboards.map((item) => ( {dashboards.map((item) => (
<Card key={item.id}> <Card key={item.id}>
<Link <Link
href={`/${params.organization}/${params.project}/${item.slug}`} href={`/${params.organization}/${params.project}/${item.slug}`}
className="block p-4 font-medium leading-none hover:underline" className="block p-4 font-medium leading-none hover:underline"
> >
{item.name} {item.name}
</Link> </Link>
</Card> </Card>
))} ))}
</div> </div>
</Container> </Container>

View File

@@ -1,14 +1,13 @@
import { Container } from "@/components/Container"; import { useMemo } from 'react';
import { PageTitle } from "@/components/PageTitle"; import { Container } from '@/components/Container';
import { usePagination } from "@/components/Pagination"; import { EventsTable } from '@/components/events/EventsTable';
import { EventsTable } from "@/components/events/EventsTable"; import { MainLayout } from '@/components/layouts/MainLayout';
import { MainLayout } from "@/components/layouts/MainLayout"; import { PageTitle } from '@/components/PageTitle';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { usePagination } from '@/components/Pagination';
import { useQueryParams } from "@/hooks/useQueryParams"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { api } from "@/utils/api"; import { useQueryParams } from '@/hooks/useQueryParams';
import { api } from '@/utils/api';
import { useMemo } from "react"; import { z } from 'zod';
import { z } from "zod";
export default function ProfileId() { export default function ProfileId() {
const pagination = usePagination(); const pagination = usePagination();
@@ -16,7 +15,7 @@ export default function ProfileId() {
const { profileId } = useQueryParams( const { profileId } = useQueryParams(
z.object({ z.object({
profileId: z.string(), profileId: z.string(),
}), })
); );
const eventsQuery = api.event.list.useQuery( const eventsQuery = api.event.list.useQuery(
{ {
@@ -26,7 +25,7 @@ export default function ProfileId() {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
}, }
); );
const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]); const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);

View File

@@ -1,21 +1,22 @@
import { Container } from "@/components/Container"; import { useMemo } from 'react';
import { DataTable } from "@/components/DataTable"; import { Container } from '@/components/Container';
import { PageTitle } from "@/components/PageTitle"; import { DataTable } from '@/components/DataTable';
import { Pagination, usePagination } from "@/components/Pagination"; import { MainLayout } from '@/components/layouts/MainLayout';
import { MainLayout } from "@/components/layouts/MainLayout"; import { PageTitle } from '@/components/PageTitle';
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Pagination, usePagination } from '@/components/Pagination';
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
import { type RouterOutputs, api } from "@/utils/api"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { formatDateTime } from "@/utils/date"; import { api } from '@/utils/api';
import { toDots } from "@/utils/object"; import type { RouterOutputs } from '@/utils/api';
import { AvatarImage } from "@radix-ui/react-avatar"; import { formatDateTime } from '@/utils/date';
import { createColumnHelper } from "@tanstack/react-table"; import { toDots } from '@/utils/object';
import Link from "next/link"; import { AvatarImage } from '@radix-ui/react-avatar';
import { useMemo } from "react"; import { createColumnHelper } from '@tanstack/react-table';
import Link from 'next/link';
const columnHelper = const columnHelper =
createColumnHelper<RouterOutputs["profile"]["list"][number]>(); createColumnHelper<RouterOutputs['profile']['list'][number]>();
export default function Events() { export default function Events() {
const pagination = usePagination(); const pagination = usePagination();
@@ -27,42 +28,45 @@ export default function Events() {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
}, }
); );
const profiles = useMemo(() => eventsQuery.data ?? [], [eventsQuery]); const profiles = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
columnHelper.accessor((row) => row.createdAt, { columnHelper.accessor((row) => row.createdAt, {
id: "createdAt", id: 'createdAt',
header: () => "Created At", header: () => 'Created At',
cell(info) { cell(info) {
return formatDateTime(info.getValue()); return formatDateTime(info.getValue());
}, },
}), }),
columnHelper.accessor('first_name', { columnHelper.accessor('first_name', {
id: "name", id: 'name',
header: () => "Name", header: () => 'Name',
cell(info) { cell(info) {
const profile = info.row.original; const profile = info.row.original;
return ( return (
<Link href={`/${params.organization}/${params.project}/profiles/${profile?.id}`} className="flex items-center gap-2"> <Link
href={`/${params.organization}/${params.project}/profiles/${profile?.id}`}
className="flex items-center gap-2"
>
<Avatar className="h-6 w-6"> <Avatar className="h-6 w-6">
{profile?.avatar && <AvatarImage src={profile.avatar} />} {profile?.avatar && <AvatarImage src={profile.avatar} />}
<AvatarFallback className="text-xs"> <AvatarFallback className="text-xs">
{profile?.first_name?.at(0)} {profile?.first_name?.at(0)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
{`${profile?.first_name} ${profile?.last_name ?? ""}`} {`${profile?.first_name} ${profile?.last_name ?? ''}`}
</Link> </Link>
); );
}, },
}), }),
columnHelper.accessor((row) => row.properties, { columnHelper.accessor((row) => row.properties, {
id: "properties", id: 'properties',
header: () => "Properties", header: () => 'Properties',
cell(info) { cell(info) {
const dots = toDots(info.getValue() as Record<string, any>); const dots = toDots(info.getValue() as Record<string, any>);
if(Object.keys(dots).length === 0) return 'No properties'; if (Object.keys(dots).length === 0) return 'No properties';
return ( return (
<Table className="mini"> <Table className="mini">
<TableBody> <TableBody>
@@ -71,10 +75,10 @@ export default function Events() {
<TableRow key={key}> <TableRow key={key}>
<TableCell className="font-medium">{key}</TableCell> <TableCell className="font-medium">{key}</TableCell>
<TableCell> <TableCell>
{typeof dots[key] === "boolean" {typeof dots[key] === 'boolean'
? dots[key] ? dots[key]
? "true" ? 'true'
: "false" : 'false'
: dots[key]} : dots[key]}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -1,24 +1,26 @@
import { MainLayout } from "@/components/layouts/MainLayout"; import { Card } from '@/components/Card';
import { Container } from "@/components/Container"; import { Container } from '@/components/Container';
import { api } from "@/utils/api"; import { MainLayout } from '@/components/layouts/MainLayout';
import Link from "next/link"; import { PageTitle } from '@/components/PageTitle';
import { PageTitle } from "@/components/PageTitle"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { createServerSideProps } from '@/server/getServerSideProps';
import { Card } from "@/components/Card"; import { api } from '@/utils/api';
import { createServerSideProps } from "@/server/getServerSideProps"; import Link from 'next/link';
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();
export default function Home() { export default function Home() {
const params = useOrganizationParams( const params = useOrganizationParams();
const query = api.project.list.useQuery(
{
organizationSlug: params.organization,
},
{
enabled: !!params.organization,
}
); );
const query = api.project.list.useQuery({
organizationSlug: params.organization,
}, {
enabled: !!params.organization,
});
const projects = query.data ?? []; const projects = query.data ?? [];
return ( return (

View File

@@ -1,5 +1,5 @@
export { default } from "./index"; import { createServerSideProps } from '@/server/getServerSideProps';
import { createServerSideProps } from "@/server/getServerSideProps"; export { default } from './index';
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();

View File

@@ -1,37 +1,42 @@
import { ReportSidebar } from "@/components/report/sidebar/ReportSidebar"; import { useCallback, useEffect } from 'react';
import { Chart } from "@/components/report/chart"; import { MainLayout } from '@/components/layouts/MainLayout';
import { useDispatch, useSelector } from "@/redux"; import { Chart } from '@/components/report/chart';
import { MainLayout } from "@/components/layouts/MainLayout"; import { useReportId } from '@/components/report/hooks/useReportId';
import { ReportDateRange } from "@/components/report/ReportDateRange"; import { ReportChartType } from '@/components/report/ReportChartType';
import { useCallback, useEffect } from "react"; import { ReportDateRange } from '@/components/report/ReportDateRange';
import { reset, setReport } from "@/components/report/reportSlice"; import { reset, setReport } from '@/components/report/reportSlice';
import { useReportId } from "@/components/report/hooks/useReportId"; import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
import { api } from "@/utils/api"; import { useRouterBeforeLeave } from '@/hooks/useRouterBeforeLeave';
import { useRouterBeforeLeave } from "@/hooks/useRouterBeforeLeave"; import { useDispatch, useSelector } from '@/redux';
import { createServerSideProps } from "@/server/getServerSideProps"; import { createServerSideProps } from '@/server/getServerSideProps';
import { ReportChartType } from "@/components/report/ReportChartType"; import { api } from '@/utils/api';
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();
export default function Page() { export default function Page() {
const { reportId } = useReportId(); const { reportId } = useReportId();
const dispatch = useDispatch(); const dispatch = useDispatch();
const report = useSelector((state) => state.report); const report = useSelector((state) => state.report);
const reportQuery = api.report.get.useQuery({ id: String(reportId) }, { const reportQuery = api.report.get.useQuery(
enabled: Boolean(reportId), { id: String(reportId) },
}) {
enabled: Boolean(reportId),
}
);
// Reset report state before leaving // Reset report state before leaving
useRouterBeforeLeave(useCallback(() => { useRouterBeforeLeave(
dispatch(reset()) useCallback(() => {
}, [dispatch])) dispatch(reset());
}, [dispatch])
);
// Set report if reportId exists // Set report if reportId exists
useEffect(() => { useEffect(() => {
if(reportId && reportQuery.data) { if (reportId && reportQuery.data) {
dispatch(setReport(reportQuery.data)) dispatch(setReport(reportQuery.data));
} }
}, [reportId, reportQuery.data, dispatch]) }, [reportId, reportQuery.data, dispatch]);
return ( return (
<MainLayout className="grid min-h-screen grid-cols-[400px_minmax(0,1fr)] divide-x"> <MainLayout className="grid min-h-screen grid-cols-[400px_minmax(0,1fr)] divide-x">

View File

@@ -1,14 +1,14 @@
import { api } from "@/utils/api"; import { columns } from '@/components/clients/table';
import { ContentHeader } from "@/components/Content"; import { ContentHeader } from '@/components/Content';
import { SettingsLayout } from "@/components/layouts/SettingsLayout"; import { DataTable } from '@/components/DataTable';
import { DataTable } from "@/components/DataTable"; import { SettingsLayout } from '@/components/layouts/SettingsLayout';
import { columns } from "@/components/clients/table"; import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { pushModal } from "@/modals"; import { pushModal } from '@/modals';
import { createServerSideProps } from "@/server/getServerSideProps"; import { createServerSideProps } from '@/server/getServerSideProps';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { api } from '@/utils/api';
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();
export default function Clients() { export default function Clients() {
const params = useOrganizationParams(); const params = useOrganizationParams();
@@ -19,7 +19,7 @@ export default function Clients() {
return ( return (
<SettingsLayout> <SettingsLayout>
<ContentHeader title="Clients" text="List of your clients"> <ContentHeader title="Clients" text="List of your clients">
<Button onClick={() => pushModal("AddClient")}>Create</Button> <Button onClick={() => pushModal('AddClient')}>Create</Button>
</ContentHeader> </ContentHeader>
<DataTable data={data} columns={columns} /> <DataTable data={data} columns={columns} />
</SettingsLayout> </SettingsLayout>

View File

@@ -1,16 +1,16 @@
import { api, handleError } from "@/utils/api"; import { useEffect } from 'react';
import { Input } from "@/components/ui/input"; import { ContentHeader, ContentSection } from '@/components/Content';
import { Button } from "@/components/ui/button"; import { InputError } from '@/components/forms/InputError';
import { ContentHeader, ContentSection } from "@/components/Content"; import { SettingsLayout } from '@/components/layouts/SettingsLayout';
import { useForm } from "react-hook-form"; import { Button } from '@/components/ui/button';
import { useEffect } from "react"; import { Input } from '@/components/ui/input';
import { SettingsLayout } from "@/components/layouts/SettingsLayout"; import { toast } from '@/components/ui/use-toast';
import { toast } from "@/components/ui/use-toast"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { createServerSideProps } from "@/server/getServerSideProps"; import { createServerSideProps } from '@/server/getServerSideProps';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { api, handleError } from '@/utils/api';
import { z } from "zod"; import { useRouter } from 'next/router';
import { InputError } from "@/components/forms/InputError"; import { useForm } from 'react-hook-form';
import { useRouter } from "next/router"; import { z } from 'zod';
export const getServerSideProps = createServerSideProps(); export const getServerSideProps = createServerSideProps();
@@ -22,7 +22,7 @@ const validator = z.object({
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
export default function Organization() { export default function Organization() {
const router = useRouter() const router = useRouter();
const params = useOrganizationParams(); const params = useOrganizationParams();
const query = api.organization.get.useQuery({ const query = api.organization.get.useQuery({
slug: params.organization, slug: params.organization,
@@ -30,11 +30,11 @@ export default function Organization() {
const mutation = api.organization.update.useMutation({ const mutation = api.organization.update.useMutation({
onSuccess(res) { onSuccess(res) {
toast({ toast({
title: "Organization updated", title: 'Organization updated',
description: "Your organization has been updated.", description: 'Your organization has been updated.',
}); });
query.refetch(); query.refetch();
router.replace(`/${res.slug}/settings/organization`) router.replace(`/${res.slug}/settings/organization`);
}, },
onError: handleError, onError: handleError,
}); });
@@ -42,8 +42,8 @@ export default function Organization() {
const { register, handleSubmit, reset, formState } = useForm<IForm>({ const { register, handleSubmit, reset, formState } = useForm<IForm>({
defaultValues: { defaultValues: {
id: "", id: '',
name: "", name: '',
}, },
}); });
@@ -53,12 +53,11 @@ export default function Organization() {
} }
}, [data, reset]); }, [data, reset]);
return ( return (
<SettingsLayout> <SettingsLayout>
<form <form
onSubmit={handleSubmit((values) => { onSubmit={handleSubmit((values) => {
mutation.mutate(values) mutation.mutate(values);
})} })}
className="flex flex-col divide-y divide-border" className="flex flex-col divide-y divide-border"
> >
@@ -66,10 +65,18 @@ export default function Organization() {
title="Organization" title="Organization"
text="View and update your organization" text="View and update your organization"
> >
<Button type="submit" disabled={!formState.isDirty}>Save</Button> <Button type="submit" disabled={!formState.isDirty}>
Save
</Button>
</ContentHeader> </ContentHeader>
<ContentSection title="Name" text={["Notice. Changing name will result in a url change as well.", <InputError key="error" {...formState.errors.name} />]}> <ContentSection
<Input {...register("name")} /> title="Name"
text={[
'Notice. Changing name will result in a url change as well.',
<InputError key="error" {...formState.errors.name} />,
]}
>
<Input {...register('name')} />
</ContentSection> </ContentSection>
<ContentSection <ContentSection
title="Invite user" title="Invite user"

View File

@@ -1,23 +1,23 @@
import { api, handleError } from "@/utils/api"; import { useEffect } from 'react';
import { Input } from "@/components/ui/input"; import { ContentHeader, ContentSection } from '@/components/Content';
import { Button } from "@/components/ui/button"; import { InputError } from '@/components/forms/InputError';
import { ContentHeader, ContentSection } from "@/components/Content"; import { SettingsLayout } from '@/components/layouts/SettingsLayout';
import { useForm } from "react-hook-form"; import { Button } from '@/components/ui/button';
import { useEffect } from "react"; import { Input } from '@/components/ui/input';
import { SettingsLayout } from "@/components/layouts/SettingsLayout"; import { toast } from '@/components/ui/use-toast';
import { toast } from "@/components/ui/use-toast"; import { ChangePassword } from '@/components/user/ChangePassword';
import { createServerSideProps } from "@/server/getServerSideProps"; import { createServerSideProps } from '@/server/getServerSideProps';
import { ChangePassword } from "@/components/user/ChangePassword"; import { api, handleError } from '@/utils/api';
import { z } from "zod"; import { zodResolver } from '@hookform/resolvers/zod';
import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from 'react-hook-form';
import { InputError } from "@/components/forms/InputError"; import { z } from 'zod';
export const getServerSideProps = createServerSideProps(); export const getServerSideProps = createServerSideProps();
const validator = z.object({ const validator = z.object({
name: z.string().min(2), name: z.string().min(2),
email: z.string().email(), email: z.string().email(),
}) });
type IForm = z.infer<typeof validator>; type IForm = z.infer<typeof validator>;
@@ -26,8 +26,8 @@ export default function Profile() {
const mutation = api.user.update.useMutation({ const mutation = api.user.update.useMutation({
onSuccess() { onSuccess() {
toast({ toast({
title: "Profile updated", title: 'Profile updated',
description: "Your profile has been updated.", description: 'Your profile has been updated.',
}); });
query.refetch(); query.refetch();
}, },
@@ -38,8 +38,8 @@ export default function Profile() {
const { register, handleSubmit, reset, formState } = useForm<IForm>({ const { register, handleSubmit, reset, formState } = useForm<IForm>({
resolver: zodResolver(validator), resolver: zodResolver(validator),
defaultValues: { defaultValues: {
name: "", name: '',
email: "", email: '',
}, },
}); });
@@ -60,14 +60,23 @@ export default function Profile() {
Save Save
</Button> </Button>
</ContentHeader> </ContentHeader>
<ContentSection title="Name" text={[ <ContentSection
"Your full name", title="Name"
<InputError key="error" {...formState.errors.name} /> text={[
]}> 'Your full name',
<Input {...register("name")} /> <InputError key="error" {...formState.errors.name} />,
]}
>
<Input {...register('name')} />
</ContentSection> </ContentSection>
<ContentSection title="Mail" text={["Your email address", <InputError key="error" {...formState.errors.email} />]}> <ContentSection
<Input {...register("email")} /> title="Mail"
text={[
'Your email address',
<InputError key="error" {...formState.errors.email} />,
]}
>
<Input {...register('email')} />
</ContentSection> </ContentSection>
</form> </form>

View File

@@ -1,26 +1,26 @@
import { api } from "@/utils/api"; import { ContentHeader } from '@/components/Content';
import { ContentHeader } from "@/components/Content"; import { DataTable } from '@/components/DataTable';
import { SettingsLayout } from "@/components/layouts/SettingsLayout"; import { SettingsLayout } from '@/components/layouts/SettingsLayout';
import { DataTable } from "@/components/DataTable"; import { columns } from '@/components/projects/table';
import { columns } from "@/components/projects/table"; import { Button } from '@/components/ui/button';
import { Button } from "@/components/ui/button"; import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { pushModal } from "@/modals"; import { pushModal } from '@/modals';
import { useOrganizationParams } from "@/hooks/useOrganizationParams"; import { createServerSideProps } from '@/server/getServerSideProps';
import { createServerSideProps } from "@/server/getServerSideProps"; import { api } from '@/utils/api';
export const getServerSideProps = createServerSideProps() export const getServerSideProps = createServerSideProps();
export default function Projects() { export default function Projects() {
const params = useOrganizationParams() const params = useOrganizationParams();
const query = api.project.list.useQuery({ const query = api.project.list.useQuery({
organizationSlug: params.organization organizationSlug: params.organization,
}); });
const data = query.data ?? []; const data = query.data ?? [];
return ( return (
<SettingsLayout> <SettingsLayout>
<ContentHeader title="Projects" text="List of your projects"> <ContentHeader title="Projects" text="List of your projects">
<Button onClick={() => pushModal('AddProject')}>Create</Button> <Button onClick={() => pushModal('AddProject')}>Create</Button>
</ContentHeader> </ContentHeader>
<DataTable data={data} columns={columns} /> <DataTable data={data} columns={columns} />
</SettingsLayout> </SettingsLayout>
); );

View File

@@ -2,9 +2,9 @@ import { Suspense } from 'react';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
import store from '@/redux'; import store from '@/redux';
import { api } from '@/utils/api'; import { api } from '@/utils/api';
import { type Session } from 'next-auth'; import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react'; import { SessionProvider } from 'next-auth/react';
import { type AppType } from 'next/app'; import type { AppType } from 'next/app';
import { Space_Grotesk } from 'next/font/google'; import { Space_Grotesk } from 'next/font/google';
import { Provider as ReduxProvider } from 'react-redux'; import { Provider as ReduxProvider } from 'react-redux';

View File

@@ -3,10 +3,10 @@ import { db } from '@/server/db';
import { createError, handleError } from '@/server/exceptions'; import { createError, handleError } from '@/server/exceptions';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { type EventPayload } from '@mixan/types'; import type { EventPayload } from '@mixan/types';
interface Request extends NextApiRequest { interface Request extends NextApiRequest {
body: Array<EventPayload>; body: EventPayload[];
} }
export default async function handler(req: Request, res: NextApiResponse) { export default async function handler(req: Request, res: NextApiResponse) {

View File

@@ -3,7 +3,7 @@ import { createError, handleError } from '@/server/exceptions';
import { tickProfileProperty } from '@/server/services/profile.service'; import { tickProfileProperty } from '@/server/services/profile.service';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { type ProfileIncrementPayload } from '@mixan/types'; import type { ProfileIncrementPayload } from '@mixan/types';
interface Request extends NextApiRequest { interface Request extends NextApiRequest {
body: ProfileIncrementPayload; body: ProfileIncrementPayload;

View File

@@ -3,7 +3,7 @@ import { createError, handleError } from '@/server/exceptions';
import { tickProfileProperty } from '@/server/services/profile.service'; import { tickProfileProperty } from '@/server/services/profile.service';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { type ProfileIncrementPayload } from '@mixan/types'; import type { ProfileIncrementPayload } from '@mixan/types';
interface Request extends NextApiRequest { interface Request extends NextApiRequest {
body: ProfileIncrementPayload; body: ProfileIncrementPayload;

View File

@@ -4,7 +4,7 @@ import { createError, handleError } from '@/server/exceptions';
import { getProfile } from '@/server/services/profile.service'; import { getProfile } from '@/server/services/profile.service';
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { type ProfilePayload } from '@mixan/types'; import type { ProfilePayload } from '@mixan/types';
interface Request extends NextApiRequest { interface Request extends NextApiRequest {
body: ProfilePayload; body: ProfilePayload;

View File

@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
import { db } from '@/server/db'; import { db } from '@/server/db';
import { handleError } from '@/server/exceptions'; import { handleError } from '@/server/exceptions';
import { hashPassword } from '@/server/services/hash.service'; import { hashPassword } from '@/server/services/hash.service';
import { type NextApiRequest, type NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,

View File

@@ -3,8 +3,8 @@ import { configureStore } from '@reduxjs/toolkit';
import { import {
useDispatch as useBaseDispatch, useDispatch as useBaseDispatch,
useSelector as useBaseSelector, useSelector as useBaseSelector,
type TypedUseSelectorHook,
} from 'react-redux'; } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
const store = configureStore({ const store = configureStore({
reducer: { reducer: {

View File

@@ -1,10 +1,6 @@
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc'; import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db'; import { db } from '@/server/db';
import { import type { IChartEvent, IChartInputWithDates, IChartRange } from '@/types';
type IChartEvent,
type IChartInputWithDates,
type IChartRange,
} from '@/types';
import { getDaysOldDate } from '@/utils/date'; import { getDaysOldDate } from '@/utils/date';
import { toDots } from '@/utils/object'; import { toDots } from '@/utils/object';
import { zChartInputWithDates } from '@/utils/validation'; import { zChartInputWithDates } from '@/utils/validation';
@@ -157,11 +153,11 @@ function isJsonPath(property: string) {
return property.startsWith('properties'); return property.startsWith('properties');
} }
type ResultItem = { interface ResultItem {
label: string | null; label: string | null;
count: number; count: number;
date: string; date: string;
}; }
function propertyNameToSql(name: string) { function propertyNameToSql(name: string) {
if (name.includes('.')) { if (name.includes('.')) {

View File

@@ -2,16 +2,16 @@ import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db'; import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service'; import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service'; import { getProjectBySlug } from '@/server/services/project.service';
import { import type {
type IChartBreakdown, IChartBreakdown,
type IChartEvent, IChartEvent,
type IChartEventFilter, IChartEventFilter,
type IChartInput, IChartInput,
type IChartRange, IChartRange,
} from '@/types'; } from '@/types';
import { alphabetIds } from '@/utils/constants'; import { alphabetIds } from '@/utils/constants';
import { zChartInput } from '@/utils/validation'; import { zChartInput } from '@/utils/validation';
import { type Report as DbReport } from '@prisma/client'; import type { Report as DbReport } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
function transformFilter( function transformFilter(

View File

@@ -10,8 +10,8 @@
import { getServerAuthSession } from '@/server/auth'; import { getServerAuthSession } from '@/server/auth';
import { db } from '@/server/db'; import { db } from '@/server/db';
import { initTRPC, TRPCError } from '@trpc/server'; import { initTRPC, TRPCError } from '@trpc/server';
import { type CreateNextContextOptions } from '@trpc/server/adapters/next'; import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { type Session } from 'next-auth'; import type { Session } from 'next-auth';
import superjson from 'superjson'; import superjson from 'superjson';
import { ZodError } from 'zod'; import { ZodError } from 'zod';

Some files were not shown because too many files have changed in this diff Show More