From 85615f53f5b40ea20d2a2f084ab6a3f3b4145011 Mon Sep 17 00:00:00 2001 From: Tania Gutierrez Date: Mon, 18 Mar 2024 15:51:02 -0400 Subject: [PATCH 1/7] fix: Added Row Skeletons with loading table data --- app/components/data-table.tsx | 37 +++++++++++++++++------------ app/components/ui/skeleton.tsx | 15 ++++++++++++ app/routes/file-manager/columns.tsx | 9 +++---- 3 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 app/components/ui/skeleton.tsx diff --git a/app/components/data-table.tsx b/app/components/data-table.tsx index 0e2d76d..79f9b5b 100644 --- a/app/components/data-table.tsx +++ b/app/components/data-table.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo} from "react"; import { BaseRecord } from "@refinedev/core"; import { useTable } from "@refinedev/react-table"; import { @@ -14,6 +14,7 @@ import { TableHeader, TableRow, } from "./ui/table" +import { Skeleton } from "./ui/skeleton"; import { DataTablePagination } from "./table-pagination" interface DataTableProps { @@ -21,22 +22,31 @@ interface DataTableProps({ - columns + columns, }: DataTableProps) { - const [hoveredRowId, setHoveredRowId] = useState(""); - const table = useTable({ columns, - meta: { - hoveredRowId, - }, refineCoreProps: { resource: "files" } }) + const loadingRows = useMemo(() => Array(4).fill({}).map(() => ({ + getIsSelected: () => false, + getVisibleCells: () => columns.map(() => ({ + column: { + columnDef: { + cell: , + } + }, + getContext: () => null + })), + })), []) + + const rows = table.refineCore.tableQueryResult.isLoading ? loadingRows : table.getRowModel().rows + return ( -
+ <> {table.getHeaderGroups().map((headerGroup) => ( @@ -57,15 +67,12 @@ export function DataTable({ ))} - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( + {rows.length ? ( + rows.map((row) => ( { - console.log(hoveredRowId, row.id); - setHoveredRowId(row.id) - }} + className="group" > {row.getVisibleCells().map((cell) => ( @@ -84,6 +91,6 @@ export function DataTable({
-
+ ) } diff --git a/app/components/ui/skeleton.tsx b/app/components/ui/skeleton.tsx new file mode 100644 index 0000000..2fcae58 --- /dev/null +++ b/app/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "~/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/app/routes/file-manager/columns.tsx b/app/routes/file-manager/columns.tsx index cc4fbc5..b4a1f2b 100644 --- a/app/routes/file-manager/columns.tsx +++ b/app/routes/file-manager/columns.tsx @@ -3,6 +3,7 @@ import type { ColumnDef, RowData } from "@tanstack/react-table"; import { FileIcon, MoreIcon } from "~/components/icons"; import { Checkbox } from "~/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu"; +import { cn } from "~/utils"; // This type is used to define the shape of our data. // You can use a Zod schema here if you want. @@ -66,12 +67,13 @@ export const columns: ColumnDef[] = [ accessorKey: "createdOn", size: 200, header: "Created On", - cell: ({ row, table }) => ( + cell: ({ row }) => (
{row.getValue("createdOn")} - {(row.getIsSelected() || table.options.meta?.hoveredRowId === row.id) && ( - + @@ -88,7 +90,6 @@ export const columns: ColumnDef[] = [ - )}
) } From add532aa51fcb3e097ec39a75588a1d47b6ea556 Mon Sep 17 00:00:00 2001 From: Tania Gutierrez Date: Mon, 18 Mar 2024 21:41:21 -0400 Subject: [PATCH 2/7] feat: Added Notification provided and toast to verify email and recovery password --- app/components/ui/toast.tsx | 127 ++++++++++++++++++++ app/components/ui/toaster.tsx | 33 +++++ app/components/ui/use-toast.ts | 192 ++++++++++++++++++++++++++++++ app/data/notification-provider.ts | 26 ++++ app/root.tsx | 11 +- app/routes/register.tsx | 13 +- app/routes/reset-password.tsx | 12 ++ package.json | 1 + 8 files changed, 409 insertions(+), 6 deletions(-) create mode 100644 app/components/ui/toast.tsx create mode 100644 app/components/ui/toaster.tsx create mode 100644 app/components/ui/use-toast.ts create mode 100644 app/data/notification-provider.ts diff --git a/app/components/ui/toast.tsx b/app/components/ui/toast.tsx new file mode 100644 index 0000000..100e2f3 --- /dev/null +++ b/app/components/ui/toast.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import { Cross2Icon } from "@radix-ui/react-icons" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "~/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/app/components/ui/toaster.tsx b/app/components/ui/toaster.tsx new file mode 100644 index 0000000..c9ec6fe --- /dev/null +++ b/app/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "~/components/ui/toast" +import { useToast } from "~/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/app/components/ui/use-toast.ts b/app/components/ui/use-toast.ts new file mode 100644 index 0000000..7daac9f --- /dev/null +++ b/app/components/ui/use-toast.ts @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "~/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/app/data/notification-provider.ts b/app/data/notification-provider.ts new file mode 100644 index 0000000..475e53b --- /dev/null +++ b/app/data/notification-provider.ts @@ -0,0 +1,26 @@ +import { NotificationProvider } from "@refinedev/core"; +import { ToastAction } from "~/components/ui/toast"; +import { toast } from "~/components/ui/use-toast"; + +export const notificationProvider = (): NotificationProvider => { + + return { + open: ({ + key, + message, + type, + description, + undoableTimeout, + cancelMutation, + }) => { + toast({ + variant: type, + key, + title: message, + description, + duration: undoableTimeout, + }) + }, + close: () => {} + } +}; diff --git a/app/root.tsx b/app/root.tsx index 96d1531..b5c203c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -12,10 +12,13 @@ import type { LinksFunction } from "@remix-run/node"; // Supports weights 200-800 import '@fontsource-variable/manrope'; import {Refine} from "@refinedev/core"; -import {PortalAuthProvider} from "~/data/auth-provider.js"; import routerProvider from "@refinedev/remix-router"; -import { defaultProvider } from "./data/file-provider"; -import {SdkContextProvider} from "~/components/lib/sdk-context.js"; +import { defaultProvider } from "~/data/file-provider"; +import {PortalAuthProvider} from "~/data/auth-provider"; +import { notificationProvider } from "~/data/notification-provider"; +import {SdkContextProvider} from "~/components/lib/sdk-context"; +import { Toaster } from "~/components/ui/toaster"; + export const links: LinksFunction = () => [ { rel: "stylesheet", href: stylesheet }, @@ -32,6 +35,7 @@ export function Layout({children}: { children: React.ReactNode }) { {children} + @@ -46,6 +50,7 @@ export default function App() { authProvider={auth} routerProvider={routerProvider} dataProvider={defaultProvider} + notificationProvider={notificationProvider} resources={[ { name: 'files' }, { name: 'users' } diff --git a/app/routes/register.tsx b/app/routes/register.tsx index a0d22af..8690270 100644 --- a/app/routes/register.tsx +++ b/app/routes/register.tsx @@ -9,7 +9,7 @@ import { Field, FieldCheckbox } from "~/components/forms" import { getFormProps, useForm } from "@conform-to/react" import { z } from "zod" import { getZodConstraint, parseWithZod } from "@conform-to/zod" -import {useLogin, useRegister} from "@refinedev/core"; +import {useLogin, useNotification, useRegister} from "@refinedev/core"; import {AuthFormRequest, RegisterFormRequest} from "~/data/auth-provider.js"; export const meta: MetaFunction = () => { @@ -43,8 +43,9 @@ const RegisterSchema = z }); export default function Register() { - const register = useRegister() - const login = useLogin(); + const register = useRegister() + const login = useLogin(); + const { open } = useNotification(); const [form, fields] = useForm({ id: "register", constraint: getZodConstraint(RegisterSchema), @@ -62,6 +63,12 @@ export default function Register() { lastName: data.lastName.toString(), }, { onSuccess: () => { + open?.({ + type: "success", + message: "Verify your Email", + description: "An Email was sent to your email address. Please verify your email address to activate your account.", + key: "register-success" + }) login.mutate({ email: data.email.toString(), password: data.password.toString(), diff --git a/app/routes/reset-password.tsx b/app/routes/reset-password.tsx index 15dc865..ab951b5 100644 --- a/app/routes/reset-password.tsx +++ b/app/routes/reset-password.tsx @@ -9,6 +9,7 @@ import { Field } from "~/components/forms"; import { getFormProps, useForm } from "@conform-to/react"; import { z } from "zod"; import { getZodConstraint, parseWithZod } from "@conform-to/zod"; +import { useToast } from "~/components/ui/use-toast"; export const meta: MetaFunction = () => { return [{ title: "Sign Up" }]; @@ -18,12 +19,23 @@ const RecoverPasswordSchema = z.object({ email: z.string().email(), }); export default function RecoverPassword() { + const { toast } = useToast(); const [form, fields] = useForm({ id: "sign-up", constraint: getZodConstraint(RecoverPasswordSchema), onValidate({ formData }) { return parseWithZod(formData, { schema: RecoverPasswordSchema }); }, + onSubmit(e) { + e.preventDefault(); + + toast({ + title: "Password reset email sent", + description: "Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.", + variant: "success", + key: "reset-password-email-sent", + }); + } }); // TODO: another detail is the reset password has no screen to either accept a new pass or diff --git a/package.json b/package.json index a7c61fc..415a038 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@refinedev/cli": "^2.16.1", "@refinedev/core": "https://gitpkg.now.sh/LumeWeb/refine/packages/core?remix", "@refinedev/devtools-internal": "https://gitpkg.now.sh/LumeWeb/refine/packages/devtools-internal?remix", From 37ad1d1dc94d0ddef33addfa89599f9ba1942866 Mon Sep 17 00:00:00 2001 From: Tania Gutierrez Date: Mon, 18 Mar 2024 21:42:32 -0400 Subject: [PATCH 3/7] feat: Added Dialog to Edit Avatar Card on Account View --- app/components/management-card.tsx | 3 +- app/routes/account.tsx | 117 ++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/app/components/management-card.tsx b/app/components/management-card.tsx index af92336..080cbc0 100644 --- a/app/components/management-card.tsx +++ b/app/components/management-card.tsx @@ -3,12 +3,13 @@ import { Avatar } from "./ui/avatar"; import { Button } from "./ui/button"; import { EditIcon, FingerPrintIcon } from "./icons"; -const ManagementCardAvatar = ({ src }: { src?: string }) => { +const ManagementCardAvatar = ({ src, onClick }: { src?: string; onClick?: () => void }) => { return (
+ New Avatar Preview +
+ )} + + {hasStarted ? ( +
+ + {isCompleted + ? "Upload completed" + : `0% completed`} +
+ ) : null} + + {isUploading ? ( + + + + ) : null} + + {isCompleted ? ( + + + + ) : null} + + {!hasStarted && !isCompleted && !isUploading ? ( + + ) : null} + + + ); +}; + const PasswordDots = ({ className }: { className?: string }) => { return ( Date: Tue, 19 Mar 2024 11:38:42 -0400 Subject: [PATCH 4/7] fix: Added cancelMutation to toast --- app/components/ui/toast.tsx | 2 +- app/components/ui/toaster.tsx | 5 +++- app/data/notification-provider.ts | 26 ------------------- app/data/notification-provider.tsx | 40 ++++++++++++++++++++++++++++++ app/routes/reset-password.tsx | 20 +++++++++------ 5 files changed, 57 insertions(+), 36 deletions(-) delete mode 100644 app/data/notification-provider.ts create mode 100644 app/data/notification-provider.tsx diff --git a/app/components/ui/toast.tsx b/app/components/ui/toast.tsx index 100e2f3..f8f37c9 100644 --- a/app/components/ui/toast.tsx +++ b/app/components/ui/toast.tsx @@ -41,7 +41,7 @@ const toastVariants = cva( const Toast = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & - VariantProps + VariantProps & { cancelMutation?: () => void } >(({ className, variant, ...props }, ref) => { return ( - {toasts.map(function ({ id, title, description, action, ...props }) { + {toasts.map(function ({ id, title, description, action, cancelMutation, ...props }) { + const undoButton = cancelMutation ? Undo : undefined return (
@@ -23,6 +25,7 @@ export function Toaster() { )}
{action} + {undoButton} ) diff --git a/app/data/notification-provider.ts b/app/data/notification-provider.ts deleted file mode 100644 index 475e53b..0000000 --- a/app/data/notification-provider.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NotificationProvider } from "@refinedev/core"; -import { ToastAction } from "~/components/ui/toast"; -import { toast } from "~/components/ui/use-toast"; - -export const notificationProvider = (): NotificationProvider => { - - return { - open: ({ - key, - message, - type, - description, - undoableTimeout, - cancelMutation, - }) => { - toast({ - variant: type, - key, - title: message, - description, - duration: undoableTimeout, - }) - }, - close: () => {} - } -}; diff --git a/app/data/notification-provider.tsx b/app/data/notification-provider.tsx new file mode 100644 index 0000000..1ae130d --- /dev/null +++ b/app/data/notification-provider.tsx @@ -0,0 +1,40 @@ +import type { + NotificationProvider, + OpenNotificationParams, +} from "@refinedev/core"; +import type { ToastActionElement } from "~/components/ui/toast"; +import { toast } from "~/components/ui/use-toast"; + +interface Provider extends Omit { + open: ( + params: Omit & { + action?: ToastActionElement; + type: "default" | "destructive"; + }, + ) => void; +} + +export const notificationProvider = () => { + return { + open: ({ + key, + message, + description, + undoableTimeout, + cancelMutation, + action, + type, + }) => { + toast({ + variant: type, + key, + title: message, + description, + duration: undoableTimeout, + action, + cancelMutation, + }); + }, + close: () => {}, + } satisfies Provider; +}; \ No newline at end of file diff --git a/app/routes/reset-password.tsx b/app/routes/reset-password.tsx index ab951b5..84855cb 100644 --- a/app/routes/reset-password.tsx +++ b/app/routes/reset-password.tsx @@ -9,7 +9,8 @@ import { Field } from "~/components/forms"; import { getFormProps, useForm } from "@conform-to/react"; import { z } from "zod"; import { getZodConstraint, parseWithZod } from "@conform-to/zod"; -import { useToast } from "~/components/ui/use-toast"; +import { ToastAction } from "~/components/ui/toast"; +import { useNotification } from "@refinedev/core"; export const meta: MetaFunction = () => { return [{ title: "Sign Up" }]; @@ -19,7 +20,7 @@ const RecoverPasswordSchema = z.object({ email: z.string().email(), }); export default function RecoverPassword() { - const { toast } = useToast(); + const { open } = useNotification(); const [form, fields] = useForm({ id: "sign-up", constraint: getZodConstraint(RecoverPasswordSchema), @@ -28,13 +29,16 @@ export default function RecoverPassword() { }, onSubmit(e) { e.preventDefault(); - - toast({ - title: "Password reset email sent", + open?.({ + type: "success", + message: "Password reset email sent", description: "Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.", - variant: "success", - key: "reset-password-email-sent", - }); + action: Cancel, + cancelMutation: () => { + console.log("cancel mutation"); + }, + }) + } }); From d59130930ff6142e51e3f8bf8cb3e1f12a45f742 Mon Sep 17 00:00:00 2001 From: Tania Gutierrez Date: Tue, 19 Mar 2024 14:13:37 -0400 Subject: [PATCH 5/7] fix: Fixed Dialog not reseting state --- app/components/management-card.tsx | 18 +- app/routes/account.tsx | 672 ++++++++++++++--------------- 2 files changed, 340 insertions(+), 350 deletions(-) diff --git a/app/components/management-card.tsx b/app/components/management-card.tsx index 080cbc0..b063e5a 100644 --- a/app/components/management-card.tsx +++ b/app/components/management-card.tsx @@ -3,17 +3,21 @@ import { Avatar } from "./ui/avatar"; import { Button } from "./ui/button"; import { EditIcon, FingerPrintIcon } from "./icons"; -const ManagementCardAvatar = ({ src, onClick }: { src?: string; onClick?: () => void }) => { +const ManagementCardAvatar = ({ src, button, onClick }: { src?: string; button?: React.ReactNode; onClick?: () => void }) => { return (
- + {!button + ? + : button + } +
); diff --git a/app/routes/account.tsx b/app/routes/account.tsx index c1d592f..5522249 100644 --- a/app/routes/account.tsx +++ b/app/routes/account.tsx @@ -8,11 +8,18 @@ import { useUpdate, useUpdatePassword, } from "@refinedev/core"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { z } from "zod"; import { Field } from "~/components/forms"; import { GeneralLayout } from "~/components/general-layout"; -import { AddIcon, CloudCheckIcon, CloudIcon, CloudUploadIcon, CrownIcon, TrashIcon } from "~/components/icons"; +import { + AddIcon, + CloudCheckIcon, + CloudIcon, + CloudUploadIcon, + CrownIcon, + EditIcon, +} from "~/components/icons"; import { useUppy } from "~/components/lib/uppy"; import { ManagementCard, @@ -27,6 +34,7 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogTrigger, } from "~/components/ui/dialog"; import { Input } from "~/components/ui/input"; import { UsageCard } from "~/components/usage-card"; @@ -46,149 +54,162 @@ export default function MyAccount() { return (

My Account

- } - button={ - - } - /> -

Account Management

-
- - setModal({ ...openModal, changeAvatar: true })} - /> - - - Email Address - - {identity?.email} - - - - - - - Account Type - - Lite Premium Account - - - - - - -
-

Security

-
- - Password - - - - - - - - - Two-Factor Authentication - - Improve security by enabling 2FA. - - - - - -
-

More

-
- - Invite a Friend - - Get 1 GB per friend invited for free (max 5 GB). - - - - - - - Read our Resources - - Navigate helpful articles or get assistance. - - - - - - - Delete Account - - Once initiated, this action cannot be undone. - - - - - -
- {/* Dialogs must be near to body as possible to open the modal, otherwise will be restricted to parent height-width */} - - setModal({ ...openModal, changeAvatar: value }) - } - /> - - setModal({ ...openModal, changeEmail: value }) - } - currentValue={identity?.email || ""} - /> - - setModal({ ...openModal, changePassword: value }) - } - /> - - setModal({ ...openModal, setupTwoFactor: value }) - } - /> + } + /> +

Account Management

+
+ + + + + } + /> + + + Email Address + + {identity?.email} + + + + + + + + + Account Type + + Lite Premium Account + + + + + + +
+

Security

+
+ + Password + + + + + + + + + + + Two-Factor Authentication + + Improve security by enabling 2FA. + + + + + + + +
+

More

+
+ + Invite a Friend + + Get 1 GB per friend invited for free (max 5 GB). + + + + + + + Read our Resources + + Navigate helpful articles or get assistance. + + + + + + + Delete Account + + Once initiated, this action cannot be undone. + + + + + +
+ + {openModal.changeAvatar && } + {openModal.changeEmail && ( + + )} + {openModal.changePassword && } + {openModal.setupTwoFactor && } + +
); } @@ -210,15 +231,7 @@ const ChangeEmailSchema = z return true; }); -const ChangeEmailForm = ({ - open, - setOpen, - currentValue, -}: { - open: boolean; - setOpen: (value: boolean) => void; - currentValue: string; -}) => { +const ChangeEmailForm = ({ currentValue }: { currentValue: string }) => { const { data: identity } = useGetIdentity<{ id: BaseKey }>(); const { mutate: updateEmail } = useUpdate(); const [form, fields] = useForm({ @@ -244,38 +257,36 @@ const ChangeEmailForm = ({ }); return ( - - - - Change Email -
- {currentValue} -
-
- - - - - -
-
-
+ <> + + Change Email + +
+ {currentValue} +
+
+ + + + + + ); }; @@ -296,13 +307,7 @@ const ChangePasswordSchema = z return true; }); -const ChangePasswordForm = ({ - open, - setOpen, -}: { - open: boolean; - setOpen: (value: boolean) => void; -}) => { +const ChangePasswordForm = () => { const { mutate: updatePassword } = useUpdatePassword<{ password: string }>(); const [form, fields] = useForm({ id: "login", @@ -323,101 +328,78 @@ const ChangePasswordForm = ({ }); return ( - - - - Change Password -
- - - - - -
-
-
+ <> + + Change Password + +
+ + + + + + ); }; -const SetupTwoFactorDialog = ({ - open, - setOpen, -}: { - open: boolean; - setOpen: (value: boolean) => void; -}) => { +const SetupTwoFactorDialog = () => { 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 -
- - - )} -
-
-
-
+ <> + + 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 +
+ + + )} +
+ ); }; -const ChangeAvatarForm = ({ - open, - setOpen, -}: { - open: boolean; - setOpen: (value: boolean) => void; -}) => { +const ChangeAvatarForm = () => { const { getRootProps, getInputProps, @@ -437,76 +419,80 @@ const ChangeAvatarForm = ({ const isCompleted = state === "completed"; const hasStarted = state !== "idle" && state !== "initializing"; + const file = getFiles()?.[0]; + + const imagePreview = useMemo(() => { + if (file) { + return URL.createObjectURL(file.data); + } + }, [file]); + return ( - { - setOpen(value); - }}> - - - Edit Avatar - - {!hasStarted && !getFiles().length ? ( -
- - -

Drag & Drop Files or Browse

-
- ) : null} + <> + + Edit Avatar + + {!hasStarted && !getFiles().length ? ( +
+ + +

Drag & Drop Files or Browse

+
+ ) : null} - {(!hasStarted && getFiles().length > 0) && ( -
- - New Avatar Preview -
- )} - - {hasStarted ? ( -
- - {isCompleted - ? "Upload completed" - : `0% completed`} -
- ) : null} - - {isUploading ? ( - - - - ) : null} - - {isCompleted ? ( - - - - ) : null} - - {!hasStarted && !isCompleted && !isUploading ? ( - - ) : null} -
-
+ New Avatar Preview +
+ )} + + {hasStarted ? ( +
+ + {isCompleted ? "Upload completed" : `0% completed`} +
+ ) : null} + + {isUploading ? ( + + + + ) : null} + + {isCompleted ? ( + + + + ) : null} + + {!hasStarted && !isCompleted && !isUploading ? ( + + ) : null} + ); }; From 9a8a7ee97817621f7a4378b25ef9c8b048b81f99 Mon Sep 17 00:00:00 2001 From: Juan Di Toro Date: Tue, 19 Mar 2024 23:41:45 +0100 Subject: [PATCH 6/7] fix: route redirection issue --- app/routes/_index.tsx | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 666f9ca..150bb68 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,24 +1,19 @@ -import {useGo, useIsAuthenticated} from "@refinedev/core"; -import {useEffect} from "react"; +import { useGo, useIsAuthenticated } from "@refinedev/core"; export default function Index() { - const {isLoading, data} = useIsAuthenticated(); + const { isLoading, data } = useIsAuthenticated(); - const go = useGo(); + const go = useGo(); - useEffect(() => { - if (!isLoading) { - if (data?.authenticated) { - go({to: "/dashboard", type: "replace"}); - } else { - go({to: "/login", type: "replace"}); - } - } - }, [isLoading, data]); + if (isLoading) { + return <>Checking Login Status; + } - if (isLoading) { - return <>Checking Login Status || null; - } + if (data?.authenticated) { + go({ to: "/dashboard", type: "replace" }); + } else { + go({ to: "/login", type: "replace" }); + } - return (<>Redirecting) || null; + return <>Redirecting; } From 65bf663b67d81a738e8510314b32ae1b9549ac48 Mon Sep 17 00:00:00 2001 From: Juan Di Toro Date: Tue, 19 Mar 2024 23:47:33 +0100 Subject: [PATCH 7/7] chore: dont break the app by throwing error just yet --- app/data/account-provider.ts | 20 ++++++++++++---- app/data/file-provider.ts | 44 +++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/app/data/account-provider.ts b/app/data/account-provider.ts index 154abd2..45cb5ac 100644 --- a/app/data/account-provider.ts +++ b/app/data/account-provider.ts @@ -10,10 +10,16 @@ type AccountData = AccountParams; export const accountProvider: SdkProvider = { getList: () => { - throw Error("Not Implemented") + console.log("Not implemented"); + return Promise.resolve({ + data: [], + }); }, getOne: () => { - throw Error("Not Implemented") + console.log("Not implemented"); + return Promise.resolve({ + data: {}, + }); }, // @ts-ignore async update( @@ -44,10 +50,16 @@ export const accountProvider: SdkProvider = { }; }, create: () => { - throw Error("Not Implemented") + console.log("Not implemented"); + return Promise.resolve({ + data: {}, + }); }, deleteOne: () => { - throw Error("Not Implemented") + console.log("Not implemented"); + return Promise.resolve({ + data: {}, + }); }, getApiUrl: () => "", } diff --git a/app/data/file-provider.ts b/app/data/file-provider.ts index 4d89673..dc6528a 100644 --- a/app/data/file-provider.ts +++ b/app/data/file-provider.ts @@ -1,11 +1,39 @@ import type { DataProvider } from "@refinedev/core"; -import {SdkProvider} from "~/data/sdk-provider.js"; +import { SdkProvider } from "~/data/sdk-provider.js"; -export const fileProvider: SdkProvider = { - getList: () => { throw Error("Not Implemented") }, - getOne: () => { throw Error("Not Implemented") }, - update: () => { throw Error("Not Implemented") }, - create: () => { throw Error("Not Implemented") }, - deleteOne: () => { throw Error("Not Implemented") }, +export const fileProvider = { + getList: () => { + console.log("Not implemented"); + return Promise.resolve({ + data: [], + total: 0, + }); + }, + getOne: () => { + console.log("Not implemented"); + return Promise.resolve({ + data: { + id: 1 + }, + }); + }, + update: () => { + console.log("Not implemented"); + return Promise.resolve({ + data: {}, + }); + }, + create: () => { + console.log("Not implemented"); + return Promise.resolve({ + data: {}, + }); + }, + deleteOne: () => { + console.log("Not implemented"); + return Promise.resolve({ + data: {}, + }); + }, getApiUrl: () => "", -} +} satisfies SdkProvider;