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

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 \
&& 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
COPY . /app
WORKDIR /app/apps/web
RUN pnpm install --frozen-lockfile --ignore-scripts
COPY . /app
RUN pnpm dlx prisma generate
RUN pnpm run build
FROM base AS prod
COPY . /app
WORKDIR /app
WORKDIR /app/apps/web
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
RUN pnpm dlx prisma generate
FROM base AS runner
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=prod /app/apps/web/node_modules /app/apps/web/node_modules
WORKDIR /app/apps/web
RUN pnpm dlx prisma generate
EXPOSE 3000
CMD [ "pnpm", "start" ]

View File

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

View File

@@ -8,7 +8,7 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
"format": "prettier --write \"**/*.{tsx,mjs,ts,md,json}\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {
@@ -62,20 +62,23 @@
"devDependencies": {
"@mixan/eslint-config": "workspace:*",
"@mixan/prettier-config": "workspace:*",
"@mixan/tsconfig": "workspace:*",
"@types/bcrypt": "^5.0.0",
"@types/node": "^18.16.0",
"@types/ramda": "^0.29.6",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/react-syntax-highlighter": "^15.5.9",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.48.0",
"eslint-config-next": "^13.5.4",
"postcss": "^8.4.27",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.1",
"prisma": "^5.1.1",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.0"
"typescript": "^5.2.2"
},
"ct3aMetadata": {
"initVersion": "7.21.0"
@@ -84,6 +87,7 @@
"root": true,
"extends": [
"@mixan/eslint-config/base",
"@mixan/eslint-config/nextjs",
"@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 { cn } from "@/utils/cn";
import type { HtmlProps } from '@/types';
import { cn } from '@/utils/cn';
export function ButtonContainer({className,...props}: HtmlProps<HTMLDivElement>) {
export function ButtonContainer({
className,
...props
}: HtmlProps<HTMLDivElement>) {
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>) {
return (
<div className="border border-border rounded">
{children}
</div>
)
}
export function Card({ children }: HtmlProps<HTMLDivElement>) {
return <div className="border border-border rounded">{children}</div>;
}

View File

@@ -1,5 +1,5 @@
import { type HtmlProps } from "@/types";
import { cn } from "@/utils/cn";
import type { HtmlProps } from '@/types';
import { cn } from '@/utils/cn';
type ColorSquareProps = HtmlProps<HTMLDivElement>;
@@ -7,8 +7,8 @@ export function ColorSquare({ children, className }: ColorSquareProps) {
return (
<div
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]",
className,
'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
)}
>
{children}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { Button } from "./ui/button";
import { useMemo, useState } from 'react';
import { Button } from './ui/button';
export function usePagination(take = 100) {
const [skip, setSkip] = useState(0);
@@ -12,11 +13,11 @@ export function usePagination(take = 100) {
canPrev: skip > 0,
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) {
return (

View File

@@ -1,12 +1,12 @@
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import ts from "react-syntax-highlighter/dist/cjs/languages/hljs/typescript";
import docco from "react-syntax-highlighter/dist/cjs/styles/hljs/docco";
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript';
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;
};
}
export default function Syntax({ code }: SyntaxProps) {
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 (
<div className="grid grid-cols-[200px_minmax(0,1fr)] gap-8">
{children}
</div>
)
<div className="grid grid-cols-[200px_minmax(0,1fr)] gap-8">{children}</div>
);
}
type SidebarProps = HtmlProps<HTMLDivElement>
type SidebarProps = HtmlProps<HTMLDivElement>;
export function Sidebar({children}: SidebarProps) {
return (
<div className="flex flex-col gap-1">
{children}
</div>
)
}
export function Sidebar({ children }: SidebarProps) {
return <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 {
DropdownMenu,
DropdownMenuContent,
@@ -5,27 +13,20 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
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";
} from '../ui/dropdown-menu';
import { toast } from '../ui/use-toast';
export function ClientActions({ id }: IClientWithProject) {
const refetch = useRefetchActive()
const refetch = useRefetchActive();
const deletion = api.client.remove.useMutation({
onSuccess() {
toast({
title: 'Success',
description: 'Client revoked, incoming requests will be rejected.',
})
refetch()
}
})
});
refetch();
},
});
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -41,7 +42,7 @@ export function ClientActions({ id }: IClientWithProject) {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
pushModal("EditClient", { id });
pushModal('EditClient', { id });
}}
>
Edit
@@ -56,9 +57,9 @@ export function ClientActions({ id }: IClientWithProject) {
onConfirm() {
deletion.mutate({
id,
})
}
})
});
},
});
}}
>
Revoke

View File

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

View File

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

View File

@@ -1,18 +1,24 @@
import { forwardRef } from "react";
import { Input, type InputProps } from "../ui/input";
import { Label } from "../ui/label";
import { forwardRef } from 'react';
import { Input } from '../ui/input';
import type { InputProps } from '../ui/input';
import { Label } from '../ui/label';
type InputWithLabelProps = InputProps & {
label: string;
};
export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(({ label, ...props }, ref) => {
return (
<div>
<Label htmlFor={label} className="block mb-2">{label}</Label>
<Input ref={ref} id={label} {...props} />
</div>
);
});
export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
({ label, ...props }, ref) => {
return (
<div>
<Label htmlFor={label} className="block mb-2">
{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 { NavbarMenu } from "../navbar/NavbarMenu";
import { Container } from "../Container";
import Link from "next/link";
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;
className?: string;
};
}
export function MainLayout({ children, className }: MainLayoutProps) {
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>
<nav className="border-b border-border">
<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">
<NavbarMenu />
<div>

View File

@@ -1,25 +1,29 @@
import { Container } from "../Container";
import { MainLayout } from "./MainLayout";
import { usePathname } from "next/navigation";
import { Sidebar, WithSidebar } from "../WithSidebar";
import Link from "next/link";
import { cn } from "@/utils/cn";
import { PageTitle } from "../PageTitle";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { cn } from '@/utils/cn';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
type SettingsLayoutProps = {
import { Container } from '../Container';
import { PageTitle } from '../PageTitle';
import { Sidebar, WithSidebar } from '../WithSidebar';
import { MainLayout } from './MainLayout';
interface SettingsLayoutProps {
children: React.ReactNode;
className?: string;
};
}
export function SettingsLayout({ children, className }: SettingsLayoutProps) {
const params = useOrganizationParams();
const pathname = usePathname();
const links = [
{ href: `/${params.organization}/settings/organization`, label: "Organization" },
{ href: `/${params.organization}/settings/projects`, label: "Projects" },
{ href: `/${params.organization}/settings/clients`, label: "Clients" },
{ href: `/${params.organization}/settings/profile`, label: "Profile" },
{
href: `/${params.organization}/settings/organization`,
label: 'Organization',
},
{ href: `/${params.organization}/settings/projects`, label: 'Projects' },
{ href: `/${params.organization}/settings/clients`, label: 'Clients' },
{ href: `/${params.organization}/settings/profile`, label: 'Profile' },
];
return (
<MainLayout>
@@ -32,19 +36,17 @@ export function SettingsLayout({ children, className }: SettingsLayoutProps) {
key={href}
href={href}
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)
? "bg-slate-100"
: "hover:bg-slate-100",
? 'bg-slate-100'
: 'hover:bg-slate-100'
)}
>
{label}
</Link>
))}
</Sidebar>
<div className={cn('flex flex-col', className)}>
{children}
</div>
<div className={cn('flex flex-col', className)}>{children}</div>
</WithSidebar>
</Container>
</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 {
DropdownMenu,
DropdownMenuContent,
@@ -8,9 +7,10 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import Link from "next/link";
} from '@/components/ui/dropdown-menu';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { LineChart } from 'lucide-react';
import Link from 'next/link';
export function NavbarCreate() {
const params = useOrganizationParams();

View File

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

View File

@@ -1,4 +1,4 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
@@ -6,11 +6,11 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import { User } from "lucide-react";
import { signOut } from "next-auth/react";
import Link from "next/link";
} from '@/components/ui/dropdown-menu';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { User } from 'lucide-react';
import { signOut } from 'next-auth/react';
import Link from 'next/link';
export function NavbarUserDropdown() {
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 {
DropdownMenu,
DropdownMenuContent,
@@ -5,27 +13,20 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
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";
} from '../ui/dropdown-menu';
import { toast } from '../ui/use-toast';
export function ProjectActions({ id }: IProject) {
const refetch = useRefetchActive()
const refetch = useRefetchActive();
const deletion = api.project.remove.useMutation({
onSuccess() {
toast({
title: 'Success',
description: 'Project deleted successfully.',
})
refetch()
}
})
});
refetch();
},
});
return (
<DropdownMenu>
@@ -42,7 +43,7 @@ export function ProjectActions({ id }: IProject) {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
pushModal("EditProject", { id });
pushModal('EditProject', { id });
}}
>
Edit
@@ -57,9 +58,9 @@ export function ProjectActions({ id }: IProject) {
onConfirm() {
deletion.mutate({
id,
})
}
})
});
},
});
}}
>
Delete

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import { useDispatch, useSelector } from "@/redux";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import { changeDateRanges, changeInterval } from "./reportSlice";
import { Combobox } from "../ui/combobox";
import { type IInterval } from "@/types";
import { intervals, timeRanges } from "@/utils/constants";
import { useDispatch, useSelector } from '@/redux';
import type { IInterval } from '@/types';
import { intervals, timeRanges } from '@/utils/constants';
import { Combobox } from '../ui/combobox';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import { changeDateRanges, changeInterval } from './reportSlice';
export function ReportDateRange() {
const dispatch = useDispatch();
@@ -28,7 +29,7 @@ export function ReportDateRange() {
);
})}
</RadioGroup>
{chartType === "linear" && (
{chartType === 'linear' && (
<div className="w-full max-w-[200px]">
<Combobox
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;
};
}
type ChartProviderProps = {
children: React.ReactNode;
@@ -20,7 +20,7 @@ export function ChartProvider({ children, editMode }: ChartProviderProps) {
() => ({
editMode,
}),
[editMode],
[editMode]
)}
>
{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) => {
return (
<ChartProvider {...props}>
<WrappedComponent {...props} />
</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() {
return useContext(ChartContext);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,13 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from 'react';
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"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
@@ -16,14 +15,14 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
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
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
@@ -34,14 +33,14 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
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
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
@@ -49,13 +48,13 @@ const AlertDialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
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
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
@@ -63,13 +62,13 @@ const AlertDialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
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
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
@@ -77,11 +76,11 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
className={cn('text-lg font-semibold', className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
@@ -89,12 +88,12 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
@@ -105,8 +104,8 @@ const AlertDialogAction = React.forwardRef<
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@@ -115,14 +114,14 @@ const AlertDialogCancel = React.forwardRef<
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
@@ -136,4 +135,4 @@ export {
AlertDialogDescription,
AlertDialogAction,
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 AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/utils/cn"
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -10,13 +9,13 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
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
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -24,11 +23,11 @@ const AvatarImage = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -37,12 +36,12 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
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
)}
{...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 { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/utils/cn"
import * as React from 'react';
import { cn } from '@/utils/cn';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
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: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<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 { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/cn";
import { Loader2 } from "lucide-react";
import * as React from 'react';
import { cn } from '@/utils/cn';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import { Loader2 } from 'lucide-react';
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: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
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:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
},
}
);
interface ButtonProps
@@ -43,10 +43,19 @@ interface 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 (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
@@ -57,11 +66,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{loading ? <Loader2 className="animate-spin" /> : <>{children}</>}
</Comp>
);
},
}
);
Button.displayName = "Button";
Button.displayName = 'Button';
Button.defaultProps = {
type: "button",
type: 'button',
};
export { Button, buttonVariants };

View File

@@ -1,8 +1,7 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/utils/cn"
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -11,18 +10,18 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
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
)}
{...props}
>
<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" />
</CheckboxPrimitive.Indicator>
</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 { X } from "lucide-react";
import * as React from '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";
import {
Command,
CommandGroup,
CommandItem,
} from "@/components/ui/command";
import { Command as CommandPrimitive } from "cmdk";
type Item = Record<'value' | 'label', string>;
type Item = Record<"value" | "label", string>;
type ComboboxMultiProps = {
interface ComboboxMultiProps {
selected: Item[];
setSelected: React.Dispatch<React.SetStateAction<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 [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
const [inputValue, setInputValue] = React.useState('');
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 input = inputRef.current
if (input) {
if (e.key === "Delete" || e.key === "Backspace") {
if (input.value === "") {
setSelected(prev => {
const newSelected = [...prev];
newSelected.pop();
return newSelected;
})
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current;
if (input) {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (input.value === '') {
setSelected((prev) => {
const newSelected = [...prev];
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 (
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-white">
<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"
>
<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">
<div className="flex gap-1 flex-wrap">
{selected.map((item) => {
return (
@@ -61,7 +65,7 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
<button
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.key === 'Enter') {
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" />
</button>
</Badge>
)
);
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
@@ -89,7 +93,7 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
</div>
</div>
<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">
<CommandGroup className="h-full overflow-auto">
{selectables.map((item) => {
@@ -101,10 +105,10 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
e.stopPropagation();
}}
onSelect={(value) => {
setInputValue("")
setSelected(prev => [...prev, item])
setInputValue('');
setSelected((prev) => [...prev, item]);
}}
className={"cursor-pointer"}
className={'cursor-pointer'}
>
{item.label}
</CommandItem>
@@ -112,8 +116,8 @@ export function ComboboxMulti({ items, selected, setSelected, placeholder, ...pr
})}
</CommandGroup>
</div>
: null}
) : null}
</div>
</Command >
)
}
</Command>
);
}

View File

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

View File

@@ -1,10 +1,9 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/utils/cn"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import * as React from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { cn } from '@/utils/cn';
import type { DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react';
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -13,15 +12,15 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
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
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
));
Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps
type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
@@ -32,8 +31,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
</Command>
</DialogContent>
</Dialog>
)
}
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
@@ -44,15 +43,15 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input
ref={ref}
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
)}
{...props}
/>
</div>
))
));
CommandInput.displayName = CommandPrimitive.Input.displayName
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
@@ -60,12 +59,12 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
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}
/>
))
));
CommandList.displayName = CommandPrimitive.List.displayName
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
@@ -76,9 +75,9 @@ const CommandEmpty = React.forwardRef<
className="py-6 text-center text-sm"
{...props}
/>
))
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
@@ -87,14 +86,14 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
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
)}
{...props}
/>
))
));
CommandGroup.displayName = CommandPrimitive.Group.displayName
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
@@ -102,11 +101,11 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
@@ -115,14 +114,14 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
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
)}
{...props}
/>
))
));
CommandItem.displayName = CommandPrimitive.Item.displayName
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
@@ -131,14 +130,14 @@ const CommandShortcut = ({
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
);
};
CommandShortcut.displayName = 'CommandShortcut';
export {
Command,
@@ -150,4 +149,4 @@ export {
CommandItem,
CommandShortcut,
CommandSeparator,
}
};

View File

@@ -1,16 +1,15 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from 'react';
import { cn } from '@/utils/cn';
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<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -19,13 +18,13 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
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
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -36,7 +35,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
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
)}
{...props}
@@ -48,8 +47,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -57,13 +56,13 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
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
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({
className,
@@ -71,13 +70,13 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
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
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -86,13 +85,13 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
'text-lg font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -100,11 +99,11 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -117,4 +116,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
}
};

View File

@@ -1,32 +1,31 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from 'react';
import { cn } from '@/utils/cn';
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<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
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",
inset && "pl-8",
'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',
className
)}
{...props}
@@ -34,9 +33,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -45,14 +44,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
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
)}
{...props}
/>
))
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -63,32 +62,32 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
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
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
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",
inset && "pl-8",
'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',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -97,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
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
)}
checked={checked}
@@ -110,9 +109,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -121,7 +120,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
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
)}
{...props}
@@ -133,26 +132,26 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -160,11 +159,11 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
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}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@@ -172,12 +171,12 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
@@ -195,4 +194,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
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>(
({ className, type, ...props }, ref) => {
@@ -10,15 +9,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
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
)}
ref={ref}
{...props}
/>
)
);
}
)
Input.displayName = "Input"
);
Input.displayName = 'Input';
export { Input }
export { Input };

View File

@@ -1,12 +1,12 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/utils/cn"
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
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<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -18,7 +18,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...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 PopoverPrimitive from "@radix-ui/react-popover"
import * as React from 'react';
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<
React.ElementRef<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.Content
ref={ref}
align={align}
sideOffset={sideOffset}
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
)}
{...props}
/>
</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 = React.InputHTMLAttributes<HTMLButtonElement> & {
active?: boolean
}
export type RadioGroupProps = React.InputHTMLAttributes<HTMLDivElement>;
export type RadioGroupItemProps =
React.InputHTMLAttributes<HTMLButtonElement> & {
active?: boolean;
};
const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
({ className, type, ...props }, ref) => {
return (
<div
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
)}
ref={ref}
{...props}
/>
)
);
}
)
);
const RadioGroupItem = React.forwardRef<HTMLButtonElement, RadioGroupItemProps>(({className, active, ...props}, 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} />
)
})
const RadioGroupItem = React.forwardRef<HTMLButtonElement, RadioGroupItemProps>(
({ className, active, ...props }, 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"
RadioGroupItem.displayName = "RadioGroupItem"
RadioGroup.displayName = 'RadioGroup';
RadioGroupItem.displayName = 'RadioGroupItem';
export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem };

View File

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

View File

@@ -1,11 +1,11 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as ToastPrimitives from '@radix-ui/react-toast';
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<
React.ElementRef<typeof ToastPrimitives.Viewport>,
@@ -14,29 +14,29 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
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
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
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: {
variant: {
default: "border bg-background text-foreground",
default: 'border bg-background text-foreground',
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: "default",
variant: 'default',
},
}
)
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
@@ -49,9 +49,9 @@ const Toast = React.forwardRef<
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
@@ -60,13 +60,13 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
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
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
@@ -75,7 +75,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
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
)}
toast-close=""
@@ -83,8 +83,8 @@ const ToastClose = React.forwardRef<
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
@@ -92,11 +92,11 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
className={cn('text-sm font-semibold', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
@@ -104,15 +104,15 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
className={cn('text-sm opacity-90', className)}
{...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 {
type ToastProps,
@@ -124,4 +124,4 @@ export {
ToastDescription,
ToastClose,
ToastAction,
}
};

View File

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

View File

@@ -1,13 +1,12 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from 'react';
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<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -17,12 +16,12 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
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
)}
{...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: [] };

View File

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

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
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) {
const router = useRouter();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
import { Suspense, useEffect, useRef, useState } from 'react';
import { Loader } from 'lucide-react';
import mitt from 'mitt';
import dynamic from 'next/dynamic';
import { useOnClickOutside } from 'usehooks-ts';
import { type ConfirmProps } from './Confirm';
import type { ConfirmProps } from './Confirm';
const Loading = () => (
<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} />
<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} />
</div>
);
@@ -48,26 +48,29 @@ const emitter = mitt<{
type ModalRoutes = keyof typeof modals;
type StateItem = {
interface StateItem {
key: string;
name: ModalRoutes;
props: Record<string, unknown>;
};
}
type ModalWrapperProps = {
interface ModalWrapperProps {
children: React.ReactNode;
name: ModalRoutes;
isOnTop: boolean;
};
}
function ModalWrapper({ children, name, isOnTop }: ModalWrapperProps) {
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, (event) => {
const target = event.target as HTMLElement;
const isPortal = typeof target.closest === 'function' ? !!target.closest('[data-radix-popper-content-wrapper]') : false
if (isOnTop && !isPortal) {
const isPortal =
typeof target.closest === 'function'
? !!target.closest('[data-radix-popper-content-wrapper]')
: false;
if (isOnTop && !isPortal) {
emitter.emit('pop', {
name,
});
@@ -75,8 +78,8 @@ function ModalWrapper({ children, name, isOnTop }: ModalWrapperProps) {
});
return (
<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 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">
<Suspense>{children}</Suspense>
</div>
</div>
@@ -141,7 +144,9 @@ export function ModalProvider() {
});
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) => {
const Modal = modals[item.name];
return (
@@ -168,7 +173,7 @@ type OrUndefined<T> = T extends Record<string, never> ? undefined : T;
export const pushModal = <
T extends StateItem['name'],
B extends OrUndefined<GetComponentProps<(typeof modals)[T]>>
B extends OrUndefined<GetComponentProps<(typeof modals)[T]>>,
>(
name: T,
...rest: B extends undefined ? [] : [B]
@@ -179,7 +184,7 @@ export const pushModal = <
});
export const replaceModal = <
T extends StateItem['name'],
B extends OrUndefined<GetComponentProps<(typeof modals)[T]>>
B extends OrUndefined<GetComponentProps<(typeof modals)[T]>>,
>(
name: T,
...rest: B extends undefined ? [] : [B]
@@ -197,4 +202,4 @@ export const unshiftModal = (name: StateItem['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 { Container } from "@/components/Container";
import { api } from "@/utils/api";
import Link from "next/link";
import { PageTitle } from "@/components/PageTitle";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import { Suspense, useMemo, useState } from "react";
import { createServerSideProps } from "@/server/getServerSideProps";
import { Chart } from "@/components/report/chart";
import { timeRanges } from "@/utils/constants";
import { type IChartRange } from "@/types";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { getRangeLabel } from "@/utils/getRangeLabel";
import { Suspense, useMemo, useState } from 'react';
import { Container } from '@/components/Container';
import { MainLayout } from '@/components/layouts/MainLayout';
import { PageTitle } from '@/components/PageTitle';
import { Chart } from '@/components/report/chart';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { createServerSideProps } from '@/server/getServerSideProps';
import type { IChartRange } from '@/types';
import { api } from '@/utils/api';
import { timeRanges } from '@/utils/constants';
import { getRangeLabel } from '@/utils/getRangeLabel';
import Link from 'next/link';
export const getServerSideProps = createServerSideProps();
@@ -66,7 +66,9 @@ export default function Dashboard() {
<div className="font-medium">{report.name}</div>
{chartRange && (
<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>}
</div>
)}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,26 @@
import { MainLayout } from "@/components/layouts/MainLayout";
import { Container } from "@/components/Container";
import { api } from "@/utils/api";
import Link from "next/link";
import { PageTitle } from "@/components/PageTitle";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import { Card } from "@/components/Card";
import { createServerSideProps } from "@/server/getServerSideProps";
import { Card } from '@/components/Card';
import { Container } from '@/components/Container';
import { MainLayout } from '@/components/layouts/MainLayout';
import { PageTitle } from '@/components/PageTitle';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { createServerSideProps } from '@/server/getServerSideProps';
import { api } from '@/utils/api';
import Link from 'next/link';
export const getServerSideProps = createServerSideProps()
export const getServerSideProps = createServerSideProps();
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 ?? [];
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 { Chart } from "@/components/report/chart";
import { useDispatch, useSelector } from "@/redux";
import { MainLayout } from "@/components/layouts/MainLayout";
import { ReportDateRange } from "@/components/report/ReportDateRange";
import { useCallback, useEffect } from "react";
import { reset, setReport } from "@/components/report/reportSlice";
import { useReportId } from "@/components/report/hooks/useReportId";
import { api } from "@/utils/api";
import { useRouterBeforeLeave } from "@/hooks/useRouterBeforeLeave";
import { createServerSideProps } from "@/server/getServerSideProps";
import { ReportChartType } from "@/components/report/ReportChartType";
import { useCallback, useEffect } from 'react';
import { MainLayout } from '@/components/layouts/MainLayout';
import { Chart } from '@/components/report/chart';
import { useReportId } from '@/components/report/hooks/useReportId';
import { ReportChartType } from '@/components/report/ReportChartType';
import { ReportDateRange } from '@/components/report/ReportDateRange';
import { reset, setReport } from '@/components/report/reportSlice';
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
import { useRouterBeforeLeave } from '@/hooks/useRouterBeforeLeave';
import { useDispatch, useSelector } from '@/redux';
import { createServerSideProps } from '@/server/getServerSideProps';
import { api } from '@/utils/api';
export const getServerSideProps = createServerSideProps()
export const getServerSideProps = createServerSideProps();
export default function Page() {
const { reportId } = useReportId();
const dispatch = useDispatch();
const report = useSelector((state) => state.report);
const reportQuery = api.report.get.useQuery({ id: String(reportId) }, {
enabled: Boolean(reportId),
})
const reportQuery = api.report.get.useQuery(
{ id: String(reportId) },
{
enabled: Boolean(reportId),
}
);
// Reset report state before leaving
useRouterBeforeLeave(useCallback(() => {
dispatch(reset())
}, [dispatch]))
useRouterBeforeLeave(
useCallback(() => {
dispatch(reset());
}, [dispatch])
);
// Set report if reportId exists
useEffect(() => {
if(reportId && reportQuery.data) {
dispatch(setReport(reportQuery.data))
if (reportId && reportQuery.data) {
dispatch(setReport(reportQuery.data));
}
}, [reportId, reportQuery.data, dispatch])
}, [reportId, reportQuery.data, dispatch]);
return (
<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 { ContentHeader } from "@/components/Content";
import { SettingsLayout } from "@/components/layouts/SettingsLayout";
import { DataTable } from "@/components/DataTable";
import { columns } from "@/components/clients/table";
import { Button } from "@/components/ui/button";
import { pushModal } from "@/modals";
import { createServerSideProps } from "@/server/getServerSideProps";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
import { columns } from '@/components/clients/table';
import { ContentHeader } from '@/components/Content';
import { DataTable } from '@/components/DataTable';
import { SettingsLayout } from '@/components/layouts/SettingsLayout';
import { Button } from '@/components/ui/button';
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { pushModal } from '@/modals';
import { createServerSideProps } from '@/server/getServerSideProps';
import { api } from '@/utils/api';
export const getServerSideProps = createServerSideProps()
export const getServerSideProps = createServerSideProps();
export default function Clients() {
const params = useOrganizationParams();
@@ -19,7 +19,7 @@ export default function Clients() {
return (
<SettingsLayout>
<ContentHeader title="Clients" text="List of your clients">
<Button onClick={() => pushModal("AddClient")}>Create</Button>
<Button onClick={() => pushModal('AddClient')}>Create</Button>
</ContentHeader>
<DataTable data={data} columns={columns} />
</SettingsLayout>

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,9 @@ import { Suspense } from 'react';
import { Toaster } from '@/components/ui/toaster';
import store from '@/redux';
import { api } from '@/utils/api';
import { type Session } from 'next-auth';
import type { Session } from 'next-auth';
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 { Provider as ReduxProvider } from 'react-redux';

View File

@@ -3,10 +3,10 @@ import { db } from '@/server/db';
import { createError, handleError } from '@/server/exceptions';
import type { NextApiRequest, NextApiResponse } from 'next';
import { type EventPayload } from '@mixan/types';
import type { EventPayload } from '@mixan/types';
interface Request extends NextApiRequest {
body: Array<EventPayload>;
body: EventPayload[];
}
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 type { NextApiRequest, NextApiResponse } from 'next';
import { type ProfileIncrementPayload } from '@mixan/types';
import type { ProfileIncrementPayload } from '@mixan/types';
interface Request extends NextApiRequest {
body: ProfileIncrementPayload;

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
import { db } from '@/server/db';
import { handleError } from '@/server/exceptions';
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(
req: NextApiRequest,

View File

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

View File

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

View File

@@ -2,16 +2,16 @@ import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service';
import {
type IChartBreakdown,
type IChartEvent,
type IChartEventFilter,
type IChartInput,
type IChartRange,
import type {
IChartBreakdown,
IChartEvent,
IChartEventFilter,
IChartInput,
IChartRange,
} from '@/types';
import { alphabetIds } from '@/utils/constants';
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';
function transformFilter(

View File

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

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