mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-13 15:35:00 +02:00
add credenza
This commit is contained in:
parent
a6baebb216
commit
2635443105
9 changed files with 332 additions and 29 deletions
13
package.json
13
package.json
|
@ -62,18 +62,15 @@
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"tailwind-merge": "2.5.3",
|
"tailwind-merge": "2.5.3",
|
||||||
"tailwindcss-animate": "1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
|
"vaul": "1.1.1",
|
||||||
"winston": "3.14.2",
|
"winston": "3.14.2",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"zod": "3.23.8",
|
"zod": "3.23.8",
|
||||||
"zod-validation-error": "3.4.0"
|
"zod-validation-error": "3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"drizzle-kit": "0.24.2",
|
|
||||||
"esbuild": "0.20.1",
|
|
||||||
"esbuild-node-externals": "1.13.0",
|
|
||||||
"yargs": "17.7.2",
|
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
|
||||||
"@dotenvx/dotenvx": "1.14.2",
|
"@dotenvx/dotenvx": "1.14.2",
|
||||||
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@types/better-sqlite3": "7.6.11",
|
"@types/better-sqlite3": "7.6.11",
|
||||||
"@types/cookie-parser": "1.4.7",
|
"@types/cookie-parser": "1.4.7",
|
||||||
"@types/cors": "2.8.17",
|
"@types/cors": "2.8.17",
|
||||||
|
@ -84,6 +81,9 @@
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
|
"drizzle-kit": "0.24.2",
|
||||||
|
"esbuild": "0.20.1",
|
||||||
|
"esbuild-node-externals": "1.13.0",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "15.0.1",
|
"eslint-config-next": "15.0.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
@ -91,7 +91,8 @@
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
"tsx": "4.19.1",
|
"tsx": "4.19.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"yargs": "17.7.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default function InviteUserForm() {
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
validForHours: "24",
|
validForHours: "168",
|
||||||
roleId: "4",
|
roleId: "4",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -189,15 +189,14 @@ export default function InviteUserForm() {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-center">
|
<Button
|
||||||
<Button
|
type="submit"
|
||||||
type="submit"
|
className="w-full"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={inviteLink !== null}
|
disabled={inviteLink !== null}
|
||||||
>
|
>
|
||||||
Invite User
|
Invite User
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
@ -216,7 +215,7 @@ export default function InviteUserForm() {
|
||||||
</b>
|
</b>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
{/* <CopyTextBox text={inviteLink} wrapText={false} /> */}
|
<CopyTextBox text={inviteLink} wrapText={false} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from "@app/components/ui/dialog";
|
} from "@app/components/ui/dialog";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import InviteUserForm from "./InviteUserForm";
|
import InviteUserForm from "./InviteUserForm";
|
||||||
|
import { Credenza, CredenzaTitle, CredenzaDescription, CredenzaHeader, CredenzaClose, CredenzaFooter, CredenzaContent, CredenzaBody } from "@app/components/Credenza";
|
||||||
|
|
||||||
export type UserRow = {
|
export type UserRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -73,17 +74,19 @@ export default function UsersTable({ users }: UsersTableProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Credenza open={isInviteModalOpen} onOpenChange={setIsInviteModalOpen}>
|
||||||
open={isInviteModalOpen}
|
<CredenzaContent>
|
||||||
onOpenChange={setIsInviteModalOpen}
|
<CredenzaHeader>
|
||||||
>
|
<CredenzaTitle>Invite User</CredenzaTitle>
|
||||||
<DialogContent>
|
<CredenzaDescription>
|
||||||
<DialogHeader>
|
Give new users access to your organization
|
||||||
<DialogTitle>Invite User</DialogTitle>
|
</CredenzaDescription>
|
||||||
</DialogHeader>
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody>
|
||||||
<InviteUserForm />
|
<InviteUserForm />
|
||||||
</DialogContent>
|
</CredenzaBody>
|
||||||
</Dialog>
|
</CredenzaContent>
|
||||||
|
</Credenza>
|
||||||
|
|
||||||
<UsersDataTable
|
<UsersDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
|
@ -42,6 +42,7 @@ export default function LoginForm({ redirect }: LoginFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
|
@ -53,6 +54,9 @@ export default function LoginForm({ redirect }: LoginFormProps) {
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
const { email, password } = values;
|
const { email, password } = values;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
.post<AxiosResponse<LoginResponse>>("/auth/login", {
|
.post<AxiosResponse<LoginResponse>>("/auth/login", {
|
||||||
email,
|
email,
|
||||||
|
@ -86,6 +90,8 @@ export default function LoginForm({ redirect }: LoginFormProps) {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -140,7 +146,7 @@ export default function LoginForm({ redirect }: LoginFormProps) {
|
||||||
<AlertDescription>{error}</AlertDescription>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" className="w-full">
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -46,6 +46,7 @@ const formSchema = z
|
||||||
export default function SignupForm({ redirect }: SignupFormProps) {
|
export default function SignupForm({ redirect }: SignupFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
@ -59,6 +60,8 @@ export default function SignupForm({ redirect }: SignupFormProps) {
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
const { email, password } = values;
|
const { email, password } = values;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
const res = await api
|
const res = await api
|
||||||
.put<AxiosResponse<SignUpResponse>>("/auth/signup", {
|
.put<AxiosResponse<SignUpResponse>>("/auth/signup", {
|
||||||
email,
|
email,
|
||||||
|
@ -92,6 +95,8 @@ export default function SignupForm({ redirect }: SignupFormProps) {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
152
src/components/Credenza.tsx
Normal file
152
src/components/Credenza.tsx
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useMediaQuery } from "@app/hooks/useMediaQuery";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RootCredenzaProps extends BaseProps {
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CredenzaProps extends BaseProps {
|
||||||
|
className?: string;
|
||||||
|
asChild?: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktop = "(min-width: 768px)";
|
||||||
|
|
||||||
|
const Credenza = ({ children, ...props }: RootCredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const Credenza = isDesktop ? Dialog : Drawer;
|
||||||
|
|
||||||
|
return <Credenza {...props}>{children}</Credenza>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaTrigger className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaClose className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaClose>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaContent className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaDescription = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaDescription = isDesktop
|
||||||
|
? DialogDescription
|
||||||
|
: DrawerDescription;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaDescription className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaDescription>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaHeader className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaTitle className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaTitle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("px-4 md:px-0 mb-4", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CredenzaFooter className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</CredenzaFooter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Credenza,
|
||||||
|
CredenzaTrigger,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaFooter,
|
||||||
|
};
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[30%] 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",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
118
src/components/ui/drawer.tsx
Normal file
118
src/components/ui/drawer.tsx
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Drawer = ({
|
||||||
|
shouldScaleBackground = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||||
|
<DrawerPrimitive.Root
|
||||||
|
shouldScaleBackground={shouldScaleBackground}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Drawer.displayName = "Drawer"
|
||||||
|
|
||||||
|
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||||
|
|
||||||
|
const DrawerPortal = DrawerPrimitive.Portal
|
||||||
|
|
||||||
|
const DrawerClose = DrawerPrimitive.Close
|
||||||
|
|
||||||
|
const DrawerOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DrawerContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DrawerPortal>
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
))
|
||||||
|
DrawerContent.displayName = "DrawerContent"
|
||||||
|
|
||||||
|
const DrawerHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DrawerHeader.displayName = "DrawerHeader"
|
||||||
|
|
||||||
|
const DrawerFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DrawerFooter.displayName = "DrawerFooter"
|
||||||
|
|
||||||
|
const DrawerTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DrawerDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
19
src/hooks/useMediaQuery.ts
Normal file
19
src/hooks/useMediaQuery.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function useMediaQuery(query: string) {
|
||||||
|
const [value, setValue] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function onChange(event: MediaQueryListEvent) {
|
||||||
|
setValue(event.matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = matchMedia(query);
|
||||||
|
result.addEventListener("change", onChange);
|
||||||
|
setValue(result.matches);
|
||||||
|
|
||||||
|
return () => result.removeEventListener("change", onChange);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue