diff --git a/app/components/icons.tsx b/app/components/icons.tsx index 72fab8a..64dd2af 100644 --- a/app/components/icons.tsx +++ b/app/components/icons.tsx @@ -7,8 +7,7 @@ export const InfoIcon = ({ className }: { className?: string }) => { viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { - ) -} + ); +}; export const AddIcon = ({ className }: { className?: string }) => { return ( @@ -38,8 +37,7 @@ export const AddIcon = ({ className }: { className?: string }) => { viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { - ) -} + ); +}; export const CloudIcon = ({ className }: { className?: string }) => { return ( @@ -64,15 +62,14 @@ export const CloudIcon = ({ className }: { className?: string }) => { viewBox="0 0 23 21" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const CloudDownloadIcon = ({ className }: { className?: string }) => { return ( @@ -83,8 +80,7 @@ export const CloudDownloadIcon = ({ className }: { className?: string }) => { viewBox="0 0 21 15" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { fill="currentColor" /> - ) -} + ); +}; export const CloudUploadIcon = ({ className }: { className?: string }) => { return ( @@ -104,15 +100,14 @@ export const CloudUploadIcon = ({ className }: { className?: string }) => { viewBox="-0.5 0 21 17" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const CloudUploadSolidIcon = ({ className }: { className?: string }) => { return ( @@ -123,15 +118,14 @@ export const CloudUploadSolidIcon = ({ className }: { className?: string }) => { viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const CrownIcon = ({ className }: { className?: string }) => { return ( @@ -142,15 +136,14 @@ export const CrownIcon = ({ className }: { className?: string }) => { viewBox="0 0 20 15" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const PersonIcon = ({ className }: { className?: string }) => { return ( @@ -161,15 +154,14 @@ export const PersonIcon = ({ className }: { className?: string }) => { viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const CheckRoundedIcon = ({ className }: { className?: string }) => { return ( @@ -180,15 +172,14 @@ export const CheckRoundedIcon = ({ className }: { className?: string }) => { viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const ClockIcon = ({ className }: { className?: string }) => { return ( @@ -199,15 +190,14 @@ export const ClockIcon = ({ className }: { className?: string }) => { viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const CircleLockIcon = ({ className }: { className?: string }) => { return ( @@ -218,15 +208,14 @@ export const CircleLockIcon = ({ className }: { className?: string }) => { viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const DriveIcon = ({ className }: { className?: string }) => { return ( @@ -237,15 +226,14 @@ export const DriveIcon = ({ className }: { className?: string }) => { viewBox="0 0 23 24" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const PageIcon = ({ className }: { className?: string }) => { return ( @@ -256,8 +244,7 @@ export const PageIcon = ({ className }: { className?: string }) => { viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { strokeLinejoin="round" /> - ) -} + ); +}; export const TrashIcon = ({ className }: { className?: string }) => { return ( @@ -307,8 +294,7 @@ export const TrashIcon = ({ className }: { className?: string }) => { viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { fill="currentColor" /> - ) -} + ); +}; export const CloudCheckIcon = ({ className }: { className?: string }) => { return ( @@ -346,8 +332,7 @@ export const CloudCheckIcon = ({ className }: { className?: string }) => { viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { fill="currentColor" /> - ) -} + ); +}; export const BoxCheckedIcon = ({ className }: { className?: string }) => { return ( @@ -367,8 +352,7 @@ export const BoxCheckedIcon = ({ className }: { className?: string }) => { viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { strokeLinejoin="round" /> - ) -} + ); +}; export const PictureIcon = ({ className }: { className?: string }) => { return ( @@ -396,8 +380,7 @@ export const PictureIcon = ({ className }: { className?: string }) => { viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { fill="currentColor" /> - ) -} + ); +}; export const XlsxIcon = ({ className }: { className?: string }) => { return ( @@ -417,8 +400,7 @@ export const XlsxIcon = ({ className }: { className?: string }) => { viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { fill="currentColor" /> - ) -} + ); +}; export const FileIcon = ({ className }: { className?: string }) => { return ( @@ -438,15 +420,14 @@ export const FileIcon = ({ className }: { className?: string }) => { viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> - ) -} + ); +}; export const MoreIcon = ({ className }: { className?: string }) => { return ( @@ -457,8 +438,7 @@ export const MoreIcon = ({ className }: { className?: string }) => { viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" - className={className} - > + className={className}> { fill="currentColor" /> - ) -} + ); +}; + +export const FingerPrintIcon = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + ); +}; + +export const EditIcon = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + ); +}; + +export const ThemeIcon = ({ className }: { className?: string }) => { + return ( + + + + ); +}; + +export const ChevronDownIcon = ({ className }: { className?: string }) => { + return ( + + + + ); +}; diff --git a/app/components/management-card.tsx b/app/components/management-card.tsx new file mode 100644 index 0000000..8cb69b5 --- /dev/null +++ b/app/components/management-card.tsx @@ -0,0 +1,123 @@ +import { cn } from "~/utils"; +import { Avatar } from "./ui/avatar"; +import { Button } from "./ui/button"; +import { AddIcon, EditIcon, FingerPrintIcon } from "./icons"; +import { Dialog, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; +import { DialogContent, Portal } from "@radix-ui/react-dialog"; + +interface ManagementCardProps { + title?: string; + value?: string; + subtitle?: string; + isInviteCard?: boolean; + isPasswordCard?: boolean; + isAvatarCard?: boolean; + isDeleteCard?: boolean + buttonText?: string; + buttonOnClick?: () => void + dialogNode?: React.ReactNode +} + +export const ManagementCard = ({ + title, + isAvatarCard, + isInviteCard, + isPasswordCard, + isDeleteCard, + subtitle, + value, + buttonText, + buttonOnClick, + dialogNode +}: ManagementCardProps) => { + const buttonVariant: string = isInviteCard ? "accent" : isDeleteCard ? "destructive" : "default"; + return ( +
+ {isAvatarCard ? ( +
+ + +
+ ) : ( + <> +
+ +

{title}

+
+ {subtitle && ( + {subtitle} + )} + {value && ( + {value} + )} + {isPasswordCard && } + {!dialogNode ? ( + + ): ( + + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account + and remove your data from our servers. + + + + + + )} + + )} +
+ ); +}; + +const PasswordDots = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index 6302846..6c8859e 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -14,7 +14,7 @@ const buttonVariants = cva( // TODO: name it better accent: "bg-ring text-primary-1-foreground hover:bg-ring/75 font-bold", destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + "bg-destructive text-white shadow-sm hover:bg-destructive/90", outline: "border border-input bg-background shadow-sm hover:bg-primary-2/5", secondary: diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..cbe1985 --- /dev/null +++ b/app/components/ui/dropdown-menu.tsx @@ -0,0 +1,203 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "~/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/app/images/QR.png b/app/images/QR.png new file mode 100644 index 0000000..2a2a4f6 Binary files /dev/null and b/app/images/QR.png differ diff --git a/app/routes/account.tsx b/app/routes/account.tsx new file mode 100644 index 0000000..54f0436 --- /dev/null +++ b/app/routes/account.tsx @@ -0,0 +1,287 @@ +import { getFormProps, useForm } from "@conform-to/react"; +import { getZodConstraint, parseWithZod } from "@conform-to/zod"; +import { useState } from "react"; +import { z } from "zod"; +import { Field } from "~/components/forms"; +import { GeneralLayout } from "~/components/general-layout"; +import { AddIcon, CloudIcon } from "~/components/icons"; +import { ManagementCard } from "~/components/management-card"; +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { Input } from "~/components/ui/input"; +import { UsageCard } from "~/components/usage-card"; + +import QRImg from "~/images/QR.png"; + +export default function MyAccount() { + const isLogged = true; + if (!isLogged) { + window.location.href = "/login"; + } + + const [openModal, setModal] = useState({ + changeEmail: false, + changePassword: false, + setupTwoFactor: false, + }); + + return ( + +

My Account

+ } + button={ + + } + /> +

Account Management

+
+ + setModal({ ...openModal, changeEmail: true })} + /> + +
+

Security

+
+ setModal({ ...openModal, changePassword: true })} + /> + setModal({ ...openModal, setupTwoFactor: true })} + /> + +
+

More

+
+ + + +
+ {/* Dialogs must be near to body as possible to open the modal, otherwise will be restricted to parent height-width */} + + setModal({ ...openModal, changeEmail: value }) + } + currentValue="bsimpson@springfield.oh.gov.com" + /> + + setModal({ ...openModal, changePassword: value }) + } + /> + + setModal({ ...openModal, setupTwoFactor: value }) + } + /> +
+ ); +} + +const ChangeEmailSchema = z.object({ + email: z.string().email(), + password: z.string(), + retypePassword: z.string(), +}); + +const ChangeEmailForm = ({ + open, + setOpen, + currentValue, +}: { + open: boolean; + setOpen: (value: boolean) => void; + currentValue: string; +}) => { + const [form, fields] = useForm({ + id: "login", + constraint: getZodConstraint(ChangeEmailSchema), + onValidate({ formData }) { + return parseWithZod(formData, { schema: ChangeEmailSchema }); + }, + shouldValidate: "onSubmit", + }); + + return ( + + + + Change Email +
+ {currentValue} +
+
+ + + + + +
+
+
+ ); +}; + +const ChangePasswordSchema = z.object({ + currentPassword: z.string().email(), + newPassword: z.string(), + retypePassword: z.string(), +}); + +const ChangePasswordForm = ({ + open, + setOpen, +}: { + open: boolean; + setOpen: (value: boolean) => void; +}) => { + const [form, fields] = useForm({ + id: "login", + constraint: getZodConstraint(ChangeEmailSchema), + onValidate({ formData }) { + return parseWithZod(formData, { schema: ChangePasswordSchema }); + }, + shouldValidate: "onSubmit", + }); + + return ( + + + + Change Password +
+ + + + + +
+
+
+ ); +}; + +const SetupTwoFactorDialog = ({ + open, + setOpen, +}: { + open: boolean; + setOpen: (value: boolean) => void; +}) => { + const [continueModal, setContinue] = useState(false); + + return ( + { + setOpen(value) + setContinue(false); + }}> + + + Setup Two-Factor +
+ {continueModal ? ( + <> +

Enter the authentication code generated in your two-factor application to confirm your setup.

+ + + + ) : ( + <> +
+ QR +
+

+ Don’t have access to scan? Use this code instead. +

+
+ HHH7MFGAMPJ44OM44FGAMPJ44O232 +
+ + + )} +
+
+
+
+ ); +};