add onboarding and reports improvements
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
"@mixan/queue": "workspace:^",
|
"@mixan/queue": "workspace:^",
|
||||||
"@mixan/types": "workspace:*",
|
"@mixan/types": "workspace:*",
|
||||||
"@mixan/validation": "workspace:^",
|
"@mixan/validation": "workspace:^",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
@@ -34,6 +35,8 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
"@t3-oss/env-nextjs": "^0.7.3",
|
"@t3-oss/env-nextjs": "^0.7.3",
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"embla-carousel-react": "8.0.0-rc22",
|
"embla-carousel-react": "8.0.0-rc22",
|
||||||
"hamburger-react": "^2.5.0",
|
"hamburger-react": "^2.5.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-animated-numbers": "^0.18.0",
|
"react-animated-numbers": "^0.18.0",
|
||||||
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.50.1",
|
"react-hook-form": "^7.50.1",
|
||||||
"react-in-viewport": "1.0.0-alpha.30",
|
"react-in-viewport": "1.0.0-alpha.30",
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox, CheckboxInput } from '@/components/ui/checkbox';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
import { clipboard } from '@/utils/clipboard';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Copy, SaveIcon } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { api, handleError } from '../../../_trpc/client';
|
||||||
|
|
||||||
|
const validation = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
domain: z.string().optional(),
|
||||||
|
withSecret: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IForm = z.infer<typeof validation>;
|
||||||
|
|
||||||
|
export function CreateClient() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { organizationId, projectId } = useAppParams();
|
||||||
|
const clients = api.client.list.useQuery({
|
||||||
|
organizationId,
|
||||||
|
});
|
||||||
|
const clientsCount = clients.data?.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clientsCount === 0) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}, [clientsCount]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<IForm>({
|
||||||
|
resolver: zodResolver(validation),
|
||||||
|
defaultValues: {
|
||||||
|
withSecret: false,
|
||||||
|
name: '',
|
||||||
|
domain: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mutation = api.client.create2.useMutation({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess() {
|
||||||
|
toast.success('Client created');
|
||||||
|
router.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const onSubmit: SubmitHandler<IForm> = (values) => {
|
||||||
|
mutation.mutate({
|
||||||
|
name: values.name,
|
||||||
|
domain: values.withSecret ? undefined : values.domain,
|
||||||
|
organizationId,
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const watch = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'withSecret',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open}>
|
||||||
|
{mutation.isSuccess ? (
|
||||||
|
<>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-[425px]"
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Success</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{mutation.data.clientSecret
|
||||||
|
? 'Use your client id and secret with our SDK to send events to us. '
|
||||||
|
: 'Use your client id with our SDK to send events to us. '}
|
||||||
|
See our{' '}
|
||||||
|
<Link href="https//openpanel.dev/docs" className="underline">
|
||||||
|
documentation
|
||||||
|
</Link>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<button
|
||||||
|
className="mt-4 text-left"
|
||||||
|
onClick={() => clipboard(mutation.data.clientId)}
|
||||||
|
>
|
||||||
|
<Label>Client ID</Label>
|
||||||
|
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
||||||
|
{mutation.data.clientId}
|
||||||
|
<Copy size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{mutation.data.clientSecret ? (
|
||||||
|
<button
|
||||||
|
className="mt-4 text-left"
|
||||||
|
onClick={() => clipboard(mutation.data.clientId)}
|
||||||
|
>
|
||||||
|
<Label>Secret</Label>
|
||||||
|
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
||||||
|
{mutation.data.clientSecret}
|
||||||
|
<Copy size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="mt-4 text-left">
|
||||||
|
<Label>Cors settings</Label>
|
||||||
|
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
||||||
|
{mutation.data.cors}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm italic mt-1">
|
||||||
|
You can update cors settings{' '}
|
||||||
|
<Link className="underline" href="/qwe/qwe/">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'secondary'}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-[425px]"
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Let's connect</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a client so you can start send events to us 🚀
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div>
|
||||||
|
<Label>Client name</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Eg. My App Client"
|
||||||
|
error={form.formState.errors.name?.message}
|
||||||
|
{...form.register('name')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="withSecret"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<CheckboxInput
|
||||||
|
defaultChecked={field.value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(!checked);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
This is a website
|
||||||
|
</CheckboxInput>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label>Your domain name</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="https://...."
|
||||||
|
error={form.formState.errors.domain?.message}
|
||||||
|
{...form.register('domain')}
|
||||||
|
disabled={watch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'secondary'}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
icon={SaveIcon}
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// <div>
|
||||||
|
// <div className="text-lg">
|
||||||
|
// Select your framework and we'll generate a client for you.
|
||||||
|
// </div>
|
||||||
|
// <div className="flex flex-wrap gap-2 mt-8">
|
||||||
|
// <FeatureButton
|
||||||
|
// name="React"
|
||||||
|
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="React Native"
|
||||||
|
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="Next.js"
|
||||||
|
// logo="https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="Remix"
|
||||||
|
// logo="https://www.datocms-assets.com/205/1642515307-square-logo.svg"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="Vue"
|
||||||
|
// logo="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ0UhQnp6TUPCwAr3ruTEwBDiTN5HLAWaoUD3AJIgtepQ&s"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="HTML"
|
||||||
|
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
@@ -9,10 +9,12 @@ import OverviewTopEvents from '@/components/overview/overview-top-events';
|
|||||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
|
import { Dialog } from '@/components/ui/dialog';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
|
||||||
import { db } from '@mixan/db';
|
import { db } from '@mixan/db';
|
||||||
|
|
||||||
|
import { CreateClient } from './create-client';
|
||||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||||
import OverviewMetrics from './overview-metrics';
|
import OverviewMetrics from './overview-metrics';
|
||||||
import { OverviewReportRange } from './overview-sticky-header';
|
import { OverviewReportRange } from './overview-sticky-header';
|
||||||
@@ -38,6 +40,7 @@ export default async function Page({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Overview" organizationSlug={organizationId}>
|
<PageLayout title="Overview" organizationSlug={organizationId}>
|
||||||
|
<CreateClient />
|
||||||
<StickyBelowHeader>
|
<StickyBelowHeader>
|
||||||
<div className="p-4 flex gap-2 justify-between">
|
<div className="p-4 flex gap-2 justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
113
apps/web/src/app/(app)/[organizationId]/create-project.tsx
Normal file
113
apps/web/src/app/(app)/[organizationId]/create-project.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { LogoSquare } from '@/components/Logo';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { SaveIcon } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { api, handleError } from '../../_trpc/client';
|
||||||
|
|
||||||
|
const validation = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IForm = z.infer<typeof validation>;
|
||||||
|
|
||||||
|
export function CreateProject() {
|
||||||
|
const params = useAppParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<IForm>({
|
||||||
|
resolver: zodResolver(validation),
|
||||||
|
});
|
||||||
|
const mutation = api.project.create.useMutation({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess() {
|
||||||
|
toast.success('Project created');
|
||||||
|
router.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const onSubmit: SubmitHandler<IForm> = (values) => {
|
||||||
|
mutation.mutate({
|
||||||
|
name: values.name,
|
||||||
|
organizationId: params.organizationId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<LogoSquare className="w-20 md:w-28 mb-8" />
|
||||||
|
<h1 className="font-medium text-3xl">Create your first project</h1>
|
||||||
|
<div className="text-lg">
|
||||||
|
A project is just a container for your events. You can create as many
|
||||||
|
as you want.
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="mt-8 flex flex-col gap-4"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label>Project name</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="My App"
|
||||||
|
size="large"
|
||||||
|
error={form.formState.errors.name?.message}
|
||||||
|
{...form.register('name')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
icon={SaveIcon}
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
>
|
||||||
|
Create project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// <div>
|
||||||
|
// <div className="text-lg">
|
||||||
|
// Select your framework and we'll generate a client for you.
|
||||||
|
// </div>
|
||||||
|
// <div className="flex flex-wrap gap-2 mt-8">
|
||||||
|
// <FeatureButton
|
||||||
|
// name="React"
|
||||||
|
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="React Native"
|
||||||
|
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="Next.js"
|
||||||
|
// logo="https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="Remix"
|
||||||
|
// logo="https://www.datocms-assets.com/205/1642515307-square-logo.svg"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="Vue"
|
||||||
|
// logo="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ0UhQnp6TUPCwAr3ruTEwBDiTN5HLAWaoUD3AJIgtepQ&s"
|
||||||
|
// />
|
||||||
|
// <FeatureButton
|
||||||
|
// name="HTML"
|
||||||
|
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getOrganizationBySlug } from '@mixan/db';
|
import { LogoSquare } from '@/components/Logo';
|
||||||
import { getProjectWithMostEvents } from '@mixan/db';
|
|
||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
|
|
||||||
import PageLayout from './[projectId]/page-layout';
|
import { getOrganizationBySlug, getProjectWithMostEvents } from '@mixan/db';
|
||||||
|
|
||||||
|
import { CreateProject } from './create-project';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
@@ -20,15 +21,30 @@ export default async function Page({ params: { organizationId } }: PageProps) {
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.BLOCK) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className="max-w-lg w-full">
|
||||||
|
<LogoSquare className="w-20 md:w-28 mb-8" />
|
||||||
|
<h1 className="font-medium text-3xl">Not quite there yet</h1>
|
||||||
|
<div className="text-lg">
|
||||||
|
We're still working on Openpanel, but we're not quite there yet.
|
||||||
|
We'll let you know when we're ready to go!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
return redirect(`/${organizationId}/${project.id}`);
|
return redirect(`/${organizationId}/${project.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Projects" organizationSlug={organizationId}>
|
<div className="flex items-center justify-center h-screen">
|
||||||
<div className="p-4">
|
<div className="max-w-lg w-full">
|
||||||
<h1>Create your first project</h1>
|
<CreateProject />
|
||||||
</div>
|
</div>
|
||||||
</PageLayout>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
86
apps/web/src/app/(app)/create-organization.tsx
Normal file
86
apps/web/src/app/(app)/create-organization.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { LogoSquare } from '@/components/Logo';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { SaveIcon } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { api, handleError } from '../_trpc/client';
|
||||||
|
|
||||||
|
const validation = z.object({
|
||||||
|
organization: z.string().min(4),
|
||||||
|
project: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IForm = z.infer<typeof validation>;
|
||||||
|
|
||||||
|
export function CreateOrganization() {
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<IForm>({
|
||||||
|
resolver: zodResolver(validation),
|
||||||
|
});
|
||||||
|
const mutation = api.onboarding.organziation.useMutation({
|
||||||
|
onError: handleError,
|
||||||
|
onSuccess({ organization, project }) {
|
||||||
|
let url = `/${organization.slug}`;
|
||||||
|
if (project) {
|
||||||
|
url += `/${project.id}`;
|
||||||
|
}
|
||||||
|
router.replace(url);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const onSubmit: SubmitHandler<IForm> = (values) => {
|
||||||
|
mutation.mutate(values);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<LogoSquare className="w-20 md:w-28 mb-8" />
|
||||||
|
<h1 className="font-medium text-3xl">Welcome to Openpanel</h1>
|
||||||
|
<div className="text-lg">
|
||||||
|
Create your organization below (can be personal or a company) and
|
||||||
|
optionally your first project 🤠
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="mt-8 flex flex-col gap-4"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label>Organization name *</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Organization name"
|
||||||
|
size="large"
|
||||||
|
error={form.formState.errors.organization?.message}
|
||||||
|
{...form.register('organization')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Project name</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Project name"
|
||||||
|
size="large"
|
||||||
|
error={form.formState.errors.project?.message}
|
||||||
|
{...form.register('project')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
icon={SaveIcon}
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,39 @@
|
|||||||
import { CreateOrganization } from '@clerk/nextjs';
|
// import { CreateOrganization } from '@clerk/nextjs';
|
||||||
|
|
||||||
|
import { LogoSquare } from '@/components/Logo';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { getCurrentOrganizations } from '@mixan/db';
|
import { getCurrentOrganizations } from '@mixan/db';
|
||||||
|
|
||||||
|
import { CreateOrganization } from './create-organization';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const organizations = await getCurrentOrganizations();
|
const organizations = await getCurrentOrganizations();
|
||||||
|
|
||||||
if (organizations.length === 0) {
|
if (process.env.BLOCK) {
|
||||||
return <CreateOrganization />;
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className="max-w-lg w-full">
|
||||||
|
<LogoSquare className="w-20 md:w-28 mb-8" />
|
||||||
|
<h1 className="font-medium text-3xl">Not quite there yet</h1>
|
||||||
|
<div className="text-lg">
|
||||||
|
We're still working on Openpanel, but we're not quite there yet.
|
||||||
|
We'll let you know when we're ready to go!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(`/${organizations[0]?.slug}`);
|
if (organizations.length > 0) {
|
||||||
|
return redirect(`/${organizations[0]?.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen">
|
||||||
|
<div className="max-w-lg w-full">
|
||||||
|
<CreateOrganization />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,22 @@ interface LogoProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LogoSquare({ className }: LogoProps) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
className={cn('rounded-md', className)}
|
||||||
|
alt="Openpanel logo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Logo({ className }: LogoProps) {
|
export function Logo({ className }: LogoProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('text-xl font-medium flex gap-2 items-center', className)}
|
className={cn('text-xl font-medium flex gap-2 items-center', className)}
|
||||||
>
|
>
|
||||||
<img
|
<LogoSquare className="max-h-8" />
|
||||||
src="/logo.svg"
|
|
||||||
className="max-h-8 rounded-md"
|
|
||||||
alt="Openpanel logo"
|
|
||||||
/>
|
|
||||||
openpanel.dev
|
openpanel.dev
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,115 @@
|
|||||||
import { CalendarIcon } from 'lucide-react';
|
import * as React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||||
|
import { pushModal } from '@/modals';
|
||||||
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
import { addDays, endOfDay, format, startOfDay, subDays } from 'date-fns';
|
||||||
|
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
|
||||||
|
import type { DateRange, SelectRangeEventHandler } from 'react-day-picker';
|
||||||
|
|
||||||
import { timeRanges } from '@mixan/constants';
|
import { timeRanges } from '@mixan/constants';
|
||||||
import type { IChartRange } from '@mixan/validation';
|
import type { IChartRange } from '@mixan/validation';
|
||||||
|
|
||||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
|
||||||
|
import { changeDates, changeEndDate, changeStartDate } from './reportSlice';
|
||||||
|
|
||||||
|
export function ReportRange({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ExtendedComboboxProps<IChartRange>) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const startDate = useSelector((state) => state.report.startDate);
|
||||||
|
const endDate = useSelector((state) => state.report.endDate);
|
||||||
|
|
||||||
|
const setDate: SelectRangeEventHandler = (val) => {
|
||||||
|
if (!val) return;
|
||||||
|
|
||||||
|
if (val.from && val.to) {
|
||||||
|
dispatch(
|
||||||
|
changeDates({
|
||||||
|
startDate: startOfDay(val.from).toISOString(),
|
||||||
|
endDate: endOfDay(val.to).toISOString(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (val.from) {
|
||||||
|
dispatch(changeStartDate(startOfDay(val.from).toISOString()));
|
||||||
|
} else if (val.to) {
|
||||||
|
dispatch(changeEndDate(endOfDay(val.to).toISOString()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isBelowSm } = useBreakpoint('sm');
|
||||||
|
|
||||||
export function ReportRange(props: ExtendedComboboxProps<IChartRange>) {
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<>
|
||||||
icon={CalendarIcon}
|
<Popover>
|
||||||
placeholder={'Range'}
|
<PopoverTrigger asChild>
|
||||||
items={Object.values(timeRanges).map((key) => ({
|
<Button
|
||||||
label: key,
|
id="date"
|
||||||
value: key,
|
variant={'outline'}
|
||||||
}))}
|
className={cn('justify-start text-left font-normal', className)}
|
||||||
{...props}
|
icon={CalendarIcon}
|
||||||
/>
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{startDate ? (
|
||||||
|
endDate ? (
|
||||||
|
<>
|
||||||
|
{format(startDate, 'LLL dd')} - {format(endDate, 'LLL dd')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
format(startDate, 'LLL dd, y')
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span>{value}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDownIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<div className="p-4 border-b border-border">
|
||||||
|
<ToggleGroup
|
||||||
|
value={value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) onChange(value);
|
||||||
|
}}
|
||||||
|
type="single"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-wrap max-sm:max-w-xs"
|
||||||
|
>
|
||||||
|
{Object.values(timeRanges).map((key) => (
|
||||||
|
<ToggleGroupItem value={key} aria-label={key} key={key}>
|
||||||
|
{key}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
))}
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
<Calendar
|
||||||
|
initialFocus
|
||||||
|
mode="range"
|
||||||
|
defaultMonth={startDate ? new Date(startDate) : new Date()}
|
||||||
|
selected={{
|
||||||
|
from: startDate ? new Date(startDate) : undefined,
|
||||||
|
to: endDate ? new Date(endDate) : undefined,
|
||||||
|
}}
|
||||||
|
onSelect={setDate}
|
||||||
|
numberOfMonths={isBelowSm ? 1 : 2}
|
||||||
|
className="[&_table]:mx-auto [&_table]:w-auto"
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export function Chart({
|
|||||||
unit,
|
unit,
|
||||||
metric,
|
metric,
|
||||||
projectId,
|
projectId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
}: ReportChartProps) {
|
}: ReportChartProps) {
|
||||||
const [data] = api.chart.chart.useSuspenseQuery(
|
const [data] = api.chart.chart.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
@@ -39,8 +41,8 @@ export function Chart({
|
|||||||
breakdowns,
|
breakdowns,
|
||||||
name,
|
name,
|
||||||
range,
|
range,
|
||||||
startDate: null,
|
startDate,
|
||||||
endDate: null,
|
endDate,
|
||||||
projectId,
|
projectId,
|
||||||
previous,
|
previous,
|
||||||
formula,
|
formula,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { cn } from '@/utils/cn';
|
|||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
import { ArrowRight, ArrowRightIcon } from 'lucide-react';
|
import { ArrowRight, ArrowRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useChartContext } from '../chart/ChartProvider';
|
||||||
|
|
||||||
function FunnelChart({ from, to }: { from: number; to: number }) {
|
function FunnelChart({ from, to }: { from: number; to: number }) {
|
||||||
const fromY = 100 - from;
|
const fromY = 100 - from;
|
||||||
const toY = 100 - to;
|
const toY = 100 - to;
|
||||||
@@ -82,15 +84,19 @@ export function FunnelSteps({
|
|||||||
steps,
|
steps,
|
||||||
totalSessions,
|
totalSessions,
|
||||||
}: RouterOutputs['chart']['funnel']) {
|
}: RouterOutputs['chart']['funnel']) {
|
||||||
|
const { editMode } = useChartContext();
|
||||||
return (
|
return (
|
||||||
<Carousel className="w-full py-4" opts={{ loop: false, dragFree: true }}>
|
<Carousel className="w-full" opts={{ loop: false, dragFree: true }}>
|
||||||
<CarouselContent>
|
<CarouselContent>
|
||||||
<CarouselItem className={'flex-[0_0_0px] pl-3'} />
|
<CarouselItem className={'flex-[0_0_0] pl-3'} />
|
||||||
{steps.map((step, index, list) => {
|
{steps.map((step, index, list) => {
|
||||||
const finalStep = index === list.length - 1;
|
const finalStep = index === list.length - 1;
|
||||||
return (
|
return (
|
||||||
<CarouselItem
|
<CarouselItem
|
||||||
className={'flex-[0_0_320px] max-w-full p-0 px-1'}
|
className={cn(
|
||||||
|
'flex-[0_0_250px] max-w-full p-0 px-1',
|
||||||
|
editMode && 'flex-[0_0_320px]'
|
||||||
|
)}
|
||||||
key={step.event.id}
|
key={step.event.id}
|
||||||
>
|
>
|
||||||
<div className="border border-border divide-y divide-border bg-white">
|
<div className="border border-border divide-y divide-border bg-white">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { start } from 'repl';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { isSameDay, isSameMonth } from 'date-fns';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
alphabetIds,
|
alphabetIds,
|
||||||
@@ -37,8 +39,8 @@ const initialState: InitialState = {
|
|||||||
breakdowns: [],
|
breakdowns: [],
|
||||||
events: [],
|
events: [],
|
||||||
range: '1m',
|
range: '1m',
|
||||||
startDate: null,
|
startDate: new Date('2024-02-24 00:00:00').toISOString(),
|
||||||
endDate: null,
|
endDate: new Date('2024-02-24 23:59:59').toISOString(),
|
||||||
previous: false,
|
previous: false,
|
||||||
formula: undefined,
|
formula: undefined,
|
||||||
unit: undefined,
|
unit: undefined,
|
||||||
@@ -66,6 +68,7 @@ export const reportSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setReport(state, action: PayloadAction<IChartInput>) {
|
setReport(state, action: PayloadAction<IChartInput>) {
|
||||||
return {
|
return {
|
||||||
|
...state,
|
||||||
...action.payload,
|
...action.payload,
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
@@ -176,21 +179,65 @@ export const reportSlice = createSlice({
|
|||||||
state.lineType = action.payload;
|
state.lineType = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Custom start and end date
|
||||||
|
changeDates: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
state.dirty = true;
|
||||||
|
state.startDate = action.payload.startDate;
|
||||||
|
state.endDate = action.payload.endDate;
|
||||||
|
|
||||||
|
if (isSameDay(state.startDate, state.endDate)) {
|
||||||
|
state.interval = 'hour';
|
||||||
|
} else if (isSameMonth(state.startDate, state.endDate)) {
|
||||||
|
state.interval = 'day';
|
||||||
|
} else {
|
||||||
|
state.interval = 'month';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
changeStartDate: (state, action: PayloadAction<string>) => {
|
changeStartDate: (state, action: PayloadAction<string>) => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
state.startDate = action.payload;
|
state.startDate = action.payload;
|
||||||
|
|
||||||
|
if (state.startDate && state.endDate) {
|
||||||
|
if (isSameDay(state.startDate, state.endDate)) {
|
||||||
|
state.interval = 'hour';
|
||||||
|
} else if (isSameMonth(state.startDate, state.endDate)) {
|
||||||
|
state.interval = 'day';
|
||||||
|
} else {
|
||||||
|
state.interval = 'month';
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
changeEndDate: (state, action: PayloadAction<string>) => {
|
changeEndDate: (state, action: PayloadAction<string>) => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
state.endDate = action.payload;
|
state.endDate = action.payload;
|
||||||
|
|
||||||
|
if (state.startDate && state.endDate) {
|
||||||
|
if (isSameDay(state.startDate, state.endDate)) {
|
||||||
|
state.interval = 'hour';
|
||||||
|
} else if (isSameMonth(state.startDate, state.endDate)) {
|
||||||
|
state.interval = 'day';
|
||||||
|
} else {
|
||||||
|
state.interval = 'month';
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
state.range = action.payload;
|
state.range = action.payload;
|
||||||
|
state.startDate = null;
|
||||||
|
state.endDate = null;
|
||||||
|
|
||||||
state.interval = getDefaultIntervalByRange(action.payload);
|
state.interval = getDefaultIntervalByRange(action.payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -215,6 +262,9 @@ export const {
|
|||||||
removeBreakdown,
|
removeBreakdown,
|
||||||
changeBreakdown,
|
changeBreakdown,
|
||||||
changeInterval,
|
changeInterval,
|
||||||
|
changeDates,
|
||||||
|
changeStartDate,
|
||||||
|
changeEndDate,
|
||||||
changeDateRanges,
|
changeDateRanges,
|
||||||
changeChartType,
|
changeChartType,
|
||||||
changeLineType,
|
changeLineType,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function ReportBreakdowns() {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{selectedBreakdowns.map((item, index) => {
|
{selectedBreakdowns.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={item.name} className="rounded-lg border">
|
<div key={item.name} className="rounded-lg border bg-slate-50">
|
||||||
<div className="flex items-center gap-2 p-2 px-4">
|
<div className="flex items-center gap-2 p-2 px-4">
|
||||||
<ColorSquare>{index}</ColorSquare>
|
<ColorSquare>{index}</ColorSquare>
|
||||||
<Combobox
|
<Combobox
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function ReportEvents() {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{selectedEvents.map((event) => {
|
{selectedEvents.map((event) => {
|
||||||
return (
|
return (
|
||||||
<div key={event.name} className="rounded-lg border">
|
<div key={event.name} className="rounded-lg border bg-slate-50">
|
||||||
<div className="flex items-center gap-2 p-2">
|
<div className="flex items-center gap-2 p-2">
|
||||||
<ColorSquare>{event.id}</ColorSquare>
|
<ColorSquare>{event.id}</ColorSquare>
|
||||||
<Combobox
|
<Combobox
|
||||||
@@ -131,7 +131,7 @@ export function ReportEvents() {
|
|||||||
]}
|
]}
|
||||||
label="Segment"
|
label="Segment"
|
||||||
>
|
>
|
||||||
<button className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs">
|
<button className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs bg-white">
|
||||||
{event.segment === 'user' ? (
|
{event.segment === 'user' ? (
|
||||||
<>
|
<>
|
||||||
<Users size={12} /> Unique users
|
<Users size={12} /> Unique users
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
|||||||
<ComboboxAdvanced
|
<ComboboxAdvanced
|
||||||
items={valuesCombobox}
|
items={valuesCombobox}
|
||||||
value={filter.value}
|
value={filter.value}
|
||||||
|
className="flex-1"
|
||||||
onChange={(setFn) => {
|
onChange={(setFn) => {
|
||||||
changeFilterValue(
|
changeFilterValue(
|
||||||
typeof setFn === 'function' ? setFn(filter.value) : setFn
|
typeof setFn === 'function' ? setFn(filter.value) : setFn
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs">
|
<button className="flex items-center gap-1 rounded-md border border-border p-1 px-2 font-medium leading-none text-xs bg-white">
|
||||||
<FilterIcon size={12} /> Add filter
|
<FilterIcon size={12} /> Add filter
|
||||||
</button>
|
</button>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
|||||||
56
apps/web/src/components/ui/accordion.tsx
Normal file
56
apps/web/src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn"
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
))
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
))
|
||||||
|
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
@@ -75,7 +75,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
{Icon && (
|
{Icon && (
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-4 w-4 mr-2',
|
'h-4 w-4 mr-2 flex-shrink-0',
|
||||||
responsive && 'mr-0 sm:mr-2',
|
responsive && 'mr-0 sm:mr-2',
|
||||||
loading && 'animate-spin'
|
loading && 'animate-spin'
|
||||||
)}
|
)}
|
||||||
|
|||||||
64
apps/web/src/components/ui/calendar.tsx
Normal file
64
apps/web/src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
|
import { DayPicker } from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
...props
|
||||||
|
}: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell:
|
||||||
|
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
||||||
|
),
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle:
|
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||||
|
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar"
|
||||||
|
|
||||||
|
export { Calendar }
|
||||||
@@ -26,4 +26,15 @@ const Checkbox = React.forwardRef<
|
|||||||
));
|
));
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Checkbox };
|
const CheckboxInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>((props, ref) => (
|
||||||
|
<label className="cursor-pointer flex items-center select-none border border-border rounded-md px-3 gap-4 min-h-10">
|
||||||
|
<Checkbox ref={ref} {...props} />
|
||||||
|
<div className="text-sm font-medium">{props.children}</div>
|
||||||
|
</label>
|
||||||
|
));
|
||||||
|
CheckboxInput.displayName = 'CheckboxInput';
|
||||||
|
|
||||||
|
export { Checkbox, CheckboxInput };
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
} from '@/components/ui/command';
|
} from '@/components/ui/command';
|
||||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import { useOnClickOutside } from 'usehooks-ts';
|
import { useOnClickOutside } from 'usehooks-ts';
|
||||||
|
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
import { Checkbox } from './checkbox';
|
import { Checkbox } from './checkbox';
|
||||||
import { Input } from './input';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||||
|
|
||||||
type IValue = any;
|
type IValue = any;
|
||||||
@@ -90,34 +87,33 @@ export function ComboboxAdvanced({
|
|||||||
>
|
>
|
||||||
<div className="flex gap-1 flex-wrap w-full">
|
<div className="flex gap-1 flex-wrap w-full">
|
||||||
{value.length === 0 && placeholder}
|
{value.length === 0 && placeholder}
|
||||||
{value.slice(0, 2).map((value) => {
|
{value.map((value) => {
|
||||||
const item = items.find((item) => item.value === value) ?? {
|
const item = items.find((item) => item.value === value) ?? {
|
||||||
value,
|
value,
|
||||||
label: value,
|
label: value,
|
||||||
};
|
};
|
||||||
return <Badge key={String(item.value)}>{item.label}</Badge>;
|
return <Badge key={String(item.value)}>{item.label}</Badge>;
|
||||||
})}
|
})}
|
||||||
{value.length > 2 && <Badge>+{value.length - 2} more</Badge>}
|
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-full max-w-md p-0" align="start">
|
<PopoverContent className="w-full max-w-md p-0" align="start">
|
||||||
<Command>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onValueChange={setInputValue}
|
onValueChange={setInputValue}
|
||||||
/>
|
/>
|
||||||
<CommandGroup>
|
<CommandList>
|
||||||
{inputValue === ''
|
{inputValue !== '' &&
|
||||||
? value.map(renderUnknownItem)
|
renderItem({
|
||||||
: renderItem({
|
value: inputValue,
|
||||||
value: inputValue,
|
label: `Pick '${inputValue}'`,
|
||||||
label: `Pick "${inputValue}"`,
|
})}
|
||||||
})}
|
{value.map(renderUnknownItem)}
|
||||||
{selectables.map(renderItem)}
|
{selectables.map(renderItem)}
|
||||||
</CommandGroup>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ export function Combobox<T extends string>({
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn('justify-between', className)}
|
className={cn('justify-between', className)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 gap-2 items-center">
|
<div className="flex min-w-0 items-center">
|
||||||
{Icon ? <Icon className="mr-2" size={16} /> : null}
|
{Icon ? <Icon size={16} className="mr-2 shrink-0" /> : null}
|
||||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{value ? find(value)?.label ?? 'No match' : placeholder}
|
{value ? find(value)?.label ?? 'No match' : placeholder}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
>(({ className, children, ...props }, ref) => (
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
>(({ className, children, onClose, ...props }, ref) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
@@ -43,7 +45,10 @@ const DialogContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||||
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
@@ -72,7 +77,7 @@ const DialogFooter = ({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
'flex flex-col-reverse sm:flex-row sm:justify-end gap-2',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -2,19 +2,36 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
const inputVariant = cva(
|
||||||
error?: string | undefined;
|
'flex w-full rounded-md border border-input bg-background 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-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
};
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
default: 'h-8 px-3 py-2 text-sm',
|
||||||
|
large: 'h-12 px-4 py-3 text-lg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InputProps = VariantProps<typeof inputVariant> &
|
||||||
|
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & {
|
||||||
|
error?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, error, type, ...props }, ref) => {
|
({ className, error, type, size, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 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-1 disabled:cursor-not-allowed disabled:opacity-50',
|
inputVariant({ size, className }),
|
||||||
className,
|
|
||||||
!!error && 'border-destructive'
|
!!error && 'border-destructive'
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||||
@@ -12,7 +10,7 @@ const PopoverContent = React.forwardRef<
|
|||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||||
<>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
@@ -21,9 +19,12 @@ const PopoverContent = React.forwardRef<
|
|||||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 0 0 9000px rgba(0, 0, 0, 0.05)',
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</>
|
</PopoverPrimitive.Portal>
|
||||||
));
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
|||||||
59
apps/web/src/components/ui/toggle-group.tsx
Normal file
59
apps/web/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||||
|
import { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn"
|
||||||
|
import { toggleVariants } from "@/components/ui/toggle"
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
const ToggleGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, children, ...props }, ref) => (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center justify-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
))
|
||||||
|
|
||||||
|
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ToggleGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, children, variant, size, ...props }, ref) => {
|
||||||
|
const context = React.useContext(ToggleGroupContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
43
apps/web/src/components/ui/toggle.tsx
Normal file
43
apps/web/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn"
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-3",
|
||||||
|
sm: "h-9 px-2.5",
|
||||||
|
lg: "h-11 px-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toggle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, ...props }, ref) => (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants }
|
||||||
@@ -4,6 +4,7 @@ import { chartRouter } from './routers/chart';
|
|||||||
import { clientRouter } from './routers/client';
|
import { clientRouter } from './routers/client';
|
||||||
import { dashboardRouter } from './routers/dashboard';
|
import { dashboardRouter } from './routers/dashboard';
|
||||||
import { eventRouter } from './routers/event';
|
import { eventRouter } from './routers/event';
|
||||||
|
import { onboardingRouter } from './routers/onboarding';
|
||||||
import { organizationRouter } from './routers/organization';
|
import { organizationRouter } from './routers/organization';
|
||||||
import { profileRouter } from './routers/profile';
|
import { profileRouter } from './routers/profile';
|
||||||
import { projectRouter } from './routers/project';
|
import { projectRouter } from './routers/project';
|
||||||
@@ -29,6 +30,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
profile: profileRouter,
|
profile: profileRouter,
|
||||||
ui: uiRouter,
|
ui: uiRouter,
|
||||||
share: shareRouter,
|
share: shareRouter,
|
||||||
|
onboarding: onboardingRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ import {
|
|||||||
} from '@/server/api/trpc';
|
} from '@/server/api/trpc';
|
||||||
import { getDaysOldDate } from '@/utils/date';
|
import { getDaysOldDate } from '@/utils/date';
|
||||||
import { average, max, min, round, sum } from '@/utils/math';
|
import { average, max, min, round, sum } from '@/utils/math';
|
||||||
import { flatten, map, pipe, prop, repeat, reverse, sort, uniq } from 'ramda';
|
import {
|
||||||
|
flatten,
|
||||||
|
map,
|
||||||
|
pick,
|
||||||
|
pipe,
|
||||||
|
prop,
|
||||||
|
repeat,
|
||||||
|
reverse,
|
||||||
|
sort,
|
||||||
|
uniq,
|
||||||
|
} from 'ramda';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { chQuery, createSqlBuilder } from '@mixan/db';
|
import { chQuery, createSqlBuilder } from '@mixan/db';
|
||||||
@@ -260,7 +270,10 @@ export const chartRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// TODO: Make this private
|
// TODO: Make this private
|
||||||
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
||||||
const current = getDatesFromRange(input.range);
|
const current =
|
||||||
|
input.startDate && input.endDate
|
||||||
|
? { startDate: input.startDate, endDate: input.endDate }
|
||||||
|
: getDatesFromRange(input.range);
|
||||||
let diff = 0;
|
let diff = 0;
|
||||||
|
|
||||||
switch (input.range) {
|
switch (input.range) {
|
||||||
|
|||||||
@@ -80,6 +80,33 @@ export const clientRouter = createTRPCRouter({
|
|||||||
cors: client.cors,
|
cors: client.cors,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
create2: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
projectId: z.string(),
|
||||||
|
organizationId: z.string(),
|
||||||
|
domain: z.string().nullish(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const secret = randomUUID();
|
||||||
|
const client = await db.client.create({
|
||||||
|
data: {
|
||||||
|
organization_slug: input.organizationId,
|
||||||
|
project_id: input.projectId,
|
||||||
|
name: input.name,
|
||||||
|
secret: input.domain ? undefined : await hashPassword(secret),
|
||||||
|
cors: input.domain || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientSecret: input.domain ? null : secret,
|
||||||
|
clientId: client.id,
|
||||||
|
cors: client.cors,
|
||||||
|
};
|
||||||
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
40
apps/web/src/server/api/routers/onboarding.ts
Normal file
40
apps/web/src/server/api/routers/onboarding.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
|
import { clerkClient } from '@clerk/nextjs';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { db } from '@mixan/db';
|
||||||
|
|
||||||
|
export const onboardingRouter = createTRPCRouter({
|
||||||
|
organziation: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
organization: z.string(),
|
||||||
|
project: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const org = await clerkClient.organizations.createOrganization({
|
||||||
|
name: input.organization,
|
||||||
|
createdBy: ctx.session.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (org.slug && input.project) {
|
||||||
|
const project = await db.project.create({
|
||||||
|
data: {
|
||||||
|
name: input.project,
|
||||||
|
organization_slug: org.slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
organization: org,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: null,
|
||||||
|
organization: org,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -113,6 +113,8 @@ export function getEventFiltersWhereClause(
|
|||||||
const id = `f${index}`;
|
const id = `f${index}`;
|
||||||
const { name, value, operator } = filter;
|
const { name, value, operator } = filter;
|
||||||
|
|
||||||
|
if (value.length === 0) return;
|
||||||
|
|
||||||
if (name.startsWith('properties.')) {
|
if (name.startsWith('properties.')) {
|
||||||
const whereFrom = `mapValues(mapExtractKeyLike(properties, '${name
|
const whereFrom = `mapValues(mapExtractKeyLike(properties, '${name
|
||||||
.replace(/^properties\./, '')
|
.replace(/^properties\./, '')
|
||||||
|
|||||||
136
pnpm-lock.yaml
generated
136
pnpm-lock.yaml
generated
@@ -338,6 +338,9 @@ importers:
|
|||||||
'@mixan/validation':
|
'@mixan/validation':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/validation
|
version: link:../../packages/validation
|
||||||
|
'@radix-ui/react-accordion':
|
||||||
|
specifier: ^1.1.2
|
||||||
|
version: 1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-alert-dialog':
|
'@radix-ui/react-alert-dialog':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -374,6 +377,12 @@ importers:
|
|||||||
'@radix-ui/react-toast':
|
'@radix-ui/react-toast':
|
||||||
specifier: ^1.1.5
|
specifier: ^1.1.5
|
||||||
version: 1.1.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.1.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-toggle':
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-toggle-group':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -413,6 +422,9 @@ importers:
|
|||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.1
|
||||||
version: 0.2.1(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 0.2.1(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
date-fns:
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1
|
||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: 8.0.0-rc22
|
specifier: 8.0.0-rc22
|
||||||
version: 8.0.0-rc22(react@18.2.0)
|
version: 8.0.0-rc22(react@18.2.0)
|
||||||
@@ -467,6 +479,9 @@ importers:
|
|||||||
react-animated-numbers:
|
react-animated-numbers:
|
||||||
specifier: ^0.18.0
|
specifier: ^0.18.0
|
||||||
version: 0.18.0(react-dom@18.2.0)(react@18.2.0)
|
version: 0.18.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-day-picker:
|
||||||
|
specifier: ^8.10.0
|
||||||
|
version: 8.10.0(date-fns@3.3.1)(react@18.2.0)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
@@ -4391,6 +4406,35 @@ packages:
|
|||||||
'@babel/runtime': 7.23.9
|
'@babel/runtime': 7.23.9
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-accordion@1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.9
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==}
|
resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4511,6 +4555,34 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.9
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5171,6 +5243,56 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.9
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.9
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7823,6 +7945,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/date-fns@3.3.1:
|
||||||
|
resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dayjs@1.11.10:
|
/dayjs@1.11.10:
|
||||||
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -12390,6 +12516,16 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-day-picker@8.10.0(date-fns@3.3.1)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==}
|
||||||
|
peerDependencies:
|
||||||
|
date-fns: ^2.28.0 || ^3.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
date-fns: 3.3.1
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-devtools-core@4.28.5:
|
/react-devtools-core@4.28.5:
|
||||||
resolution: {integrity: sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==}
|
resolution: {integrity: sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user