add credenza

This commit is contained in:
Milo Schwartz 2024-11-03 00:02:26 -04:00
parent a6baebb216
commit 2635443105
No known key found for this signature in database
9 changed files with 332 additions and 29 deletions

View file

@ -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",

View file

@ -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>
)} )}
</> </>

View file

@ -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}

View file

@ -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>

View file

@ -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
View 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,
};

View file

@ -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}

View 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,
}

View 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;
}