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/management-card.tsx b/app/components/management-card.tsx index af92336..b063e5a 100644 --- a/app/components/management-card.tsx +++ b/app/components/management-card.tsx @@ -3,16 +3,21 @@ import { Avatar } from "./ui/avatar"; import { Button } from "./ui/button"; import { EditIcon, FingerPrintIcon } from "./icons"; -const ManagementCardAvatar = ({ src }: { src?: string }) => { +const ManagementCardAvatar = ({ src, button, onClick }: { src?: string; button?: React.ReactNode; onClick?: () => void }) => { return (
- + {!button + ? + : button + } +
); 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/components/ui/toast.tsx b/app/components/ui/toast.tsx new file mode 100644 index 0000000..f8f37c9 --- /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 & { cancelMutation?: () => void } +>(({ 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..3db20c7 --- /dev/null +++ b/app/components/ui/toaster.tsx @@ -0,0 +1,36 @@ +import { + Toast, + ToastAction, + 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, cancelMutation, ...props }) { + const undoButton = cancelMutation ? Undo : undefined + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + {undoButton} + +
+ ) + })} + +
+ ) +} 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/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/auth-provider.ts b/app/data/auth-provider.ts index 0f207b0..25acbc6 100644 --- a/app/data/auth-provider.ts +++ b/app/data/auth-provider.ts @@ -4,12 +4,15 @@ import type { AuthActionResponse, CheckResponse, IdentityResponse, - OnErrorResponse + OnErrorResponse, + SuccessNotificationResponse // @ts-ignore } from "@refinedev/core/dist/interfaces/bindings/auth" import {Sdk} from "@lumeweb/portal-sdk"; import type {AccountInfoResponse} from "@lumeweb/portal-sdk"; +; + export type AuthFormRequest = { email: string; password: string; @@ -31,7 +34,7 @@ export type Identity = { email: string; } -export interface UpdatePasswordFormRequest extends UpdatePasswordFormTypes{ +export interface UpdatePasswordFormRequest extends UpdatePasswordFormTypes { currentPassword: string; } @@ -43,6 +46,39 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => { } }; + type ResponseResult = { + ret: boolean | Error; + successNotification?: SuccessNotificationResponse; + redirectToSuccess?: string; + redirectToError?: string; + successCb?: () => void; + } + + const handleResponse = (result: ResponseResult): AuthActionResponse => { + if (result.ret) { + if (result.ret instanceof Error) { + return { + success: false, + error: result.ret, + redirectTo: result.redirectToError + } + } + + result.successCb?.(); + + return { + success: true, + successNotification: result.successNotification, + redirectTo: result.redirectToSuccess, + } + } + + return { + success: false, + redirectTo: result.redirectToError + } + } + return { async login(params: AuthFormRequest): Promise { const ret = await sdk.account().login({ @@ -50,35 +86,26 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => { password: params.password, }); - let redirectTo: string | undefined; + return handleResponse({ + ret, redirectToSuccess: "/dashboard", redirectToError: "/login", successCb: () => { + sdk.setAuthToken(sdk.account().jwtToken); + }, successNotification: { + message: "Login Successful", + description: "You have successfully logged in." - if (ret) { - redirectTo = params.redirectTo; - if (!redirectTo) { - redirectTo = ret ? "/dashboard" : "/login"; } - sdk.setAuthToken(sdk.account().jwtToken); - } - - return { - success: ret, - redirectTo, - }; + }); }, async logout(params: any): Promise { let ret = await sdk.account().logout(); - return {success: ret, redirectTo: "/login"}; + return handleResponse({ret, redirectToSuccess: "/login"}); }, async check(params?: any): Promise { const ret = await sdk.account().ping(); - if (ret) { - maybeSetupAuth(); - } - - return {authenticated: ret, redirectTo: ret ? undefined : "/login"}; + return handleResponse({ret, redirectToError: "/login", successCb: maybeSetupAuth}); }, async onError(error: any): Promise { @@ -92,7 +119,12 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => { first_name: params.firstName, last_name: params.lastName, }); - return {success: ret, redirectTo: ret ? "/dashboard" : undefined}; + return handleResponse({ + ret, redirectToSuccess: "/login", successNotification: { + message: "Registration Successful", + description: "You have successfully registered. Please check your email to verify your account.", + } + }); }, async forgotPassword(params: any): Promise { @@ -103,22 +135,12 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => { maybeSetupAuth(); const ret = await sdk.account().updatePassword(params.currentPassword, params.password as string); - if (ret) { - if (ret instanceof Error) { - return { - success: false, - error: ret - } + return handleResponse({ + ret, successNotification: { + message: "Password Updated", + description: "Your password has been updated successfully.", } - - return { - success: true - } - } else { - return { - success: false - } - } + }); }, async getPermissions(params?: Record): Promise { 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; 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/root.tsx b/app/root.tsx index f44d8b2..5f17966 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -7,7 +7,9 @@ import type {LinksFunction} from "@remix-run/node"; import '@fontsource-variable/manrope'; import {Refine} from "@refinedev/core"; import routerProvider from "@refinedev/remix-router"; -import {SdkContextProvider} from "~/components/lib/sdk-context.js"; +import { notificationProvider } from "~/data/notification-provider"; +import {SdkContextProvider} from "~/components/lib/sdk-context"; +import { Toaster } from "~/components/ui/toaster"; import {getProviders} from "~/data/providers.js"; import {Sdk} from "@lumeweb/portal-sdk"; import resources from "~/data/resources.js"; @@ -27,6 +29,7 @@ export function Layout({children}: { children: React.ReactNode }) { {children} + @@ -41,6 +44,7 @@ export default function App() { { - if (!isLoading) { - if (data?.authenticated) { - go({to: "/dashboard", type: "replace"}); - } else { - go({to: "/login", type: "replace"}); - } - } - }, [isLoading, data]); - - if (isLoading) { - return <>Checking Login Status || null; - } - - return (<>Redirecting) || null; + return ( + Checking Login Status + }> + + + ) } diff --git a/app/routes/account.tsx b/app/routes/account.tsx index 437bb5a..dedf176 100644 --- a/app/routes/account.tsx +++ b/app/routes/account.tsx @@ -1,16 +1,27 @@ import { getFormProps, useForm } from "@conform-to/react"; import { getZodConstraint, parseWithZod } from "@conform-to/zod"; +import { DialogClose } from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; import { - BaseKey, - useGetIdentity, - useUpdate, - useUpdatePassword, + Authenticated, + BaseKey, + useGetIdentity, + 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, CloudIcon, CrownIcon } from "~/components/icons"; +import { + AddIcon, + CloudCheckIcon, + CloudIcon, + CloudUploadIcon, + CrownIcon, + EditIcon, +} from "~/components/icons"; +import { useUppy } from "~/components/lib/uppy"; import { ManagementCard, ManagementCardAvatar, @@ -24,6 +35,7 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogTrigger, } from "~/components/ui/dialog"; import { Input } from "~/components/ui/input"; import { UsageCard } from "~/components/usage-card"; @@ -38,147 +50,171 @@ export default function MyAccount() { changeEmail: false, changePassword: false, setupTwoFactor: false, + changeAvatar: false, }); return ( - + +

My Account

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

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. - - - - - -
- {/* 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={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 && } + +
+
); } @@ -199,15 +235,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({ @@ -234,38 +262,36 @@ const ChangeEmailForm = ({ }); return ( - - - - Change Email -
- {currentValue} -
-
- - - - - -
-
-
+ <> + + Change Email + +
+ {currentValue} +
+
+ + + + + + ); }; @@ -286,14 +312,8 @@ const ChangePasswordSchema = z return true; }); -const ChangePasswordForm = ({ - open, - setOpen, -}: { - open: boolean; - setOpen: (value: boolean) => void; -}) => { - const { mutate: updatePassword } = useUpdatePassword(); +const ChangePasswordForm = () => { + const { mutate: updatePassword } = useUpdatePassword<{ password: string }>(); const [form, fields] = useForm({ id: "login", constraint: getZodConstraint(ChangePasswordSchema), @@ -307,98 +327,174 @@ const ChangePasswordForm = ({ const data = Object.fromEntries(new FormData(e.currentTarget).entries()); updatePassword({ - currentPassword: data.currentPassword.toString(), password: data.newPassword.toString(), }); }, }); 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 = () => { + const { + getRootProps, + getInputProps, + getFiles, + upload, + state, + removeFile, + cancelAll, + } = useUppy(); + + console.log({ state, files: getFiles() }); + + const isUploading = state === "uploading"; + 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 ( + <> + + Edit Avatar + + {!hasStarted && !getFiles().length ? ( +
+ + +

Drag & Drop Files or Browse

+
+ ) : null} + + {!hasStarted && file && ( +
+ + New Avatar Preview +
+ )} + + {hasStarted ? ( +
+ + {isCompleted ? "Upload completed" : `0% completed`} +
+ ) : null} + + {isUploading ? ( + + + + ) : null} + + {isCompleted ? ( + + + + ) : null} + + {!hasStarted && !isCompleted && !isUploading ? ( + + ) : null} + ); }; 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[] = [ - )}
) } diff --git a/app/routes/register.tsx b/app/routes/register.tsx index a0d22af..fa15ba9 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,14 +43,15 @@ 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), - onValidate({ formData }) { - return parseWithZod(formData, { schema: RegisterSchema }); - }, + id: "register", + constraint: getZodConstraint(RegisterSchema), + onValidate({formData}) { + return parseWithZod(formData, {schema: RegisterSchema}); + }, onSubmit(e) { e.preventDefault(); @@ -60,17 +61,9 @@ export default function Register() { password: data.password.toString(), firstName: data.firstName.toString(), lastName: data.lastName.toString(), - }, { - onSuccess: () => { - login.mutate({ - email: data.email.toString(), - password: data.password.toString(), - rememberMe: false, - }) - } - }) - } - }); + }) + } + }); return (
diff --git a/app/routes/reset-password.tsx b/app/routes/reset-password.tsx index 15dc865..84855cb 100644 --- a/app/routes/reset-password.tsx +++ b/app/routes/reset-password.tsx @@ -9,6 +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 { ToastAction } from "~/components/ui/toast"; +import { useNotification } from "@refinedev/core"; export const meta: MetaFunction = () => { return [{ title: "Sign Up" }]; @@ -18,12 +20,26 @@ const RecoverPasswordSchema = z.object({ email: z.string().email(), }); export default function RecoverPassword() { + const { open } = useNotification(); const [form, fields] = useForm({ id: "sign-up", constraint: getZodConstraint(RecoverPasswordSchema), onValidate({ formData }) { return parseWithZod(formData, { schema: RecoverPasswordSchema }); }, + onSubmit(e) { + e.preventDefault(); + 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.", + action: Cancel, + cancelMutation: () => { + console.log("cancel mutation"); + }, + }) + + } }); // 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 7541186..7a50c58 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@conform-to/react": "^1.0.2", "@conform-to/zod": "^1.0.2", "@fontsource-variable/manrope": "^5.0.19", - "@lumeweb/portal-sdk": "0.0.0-20240319140708", + "@lumeweb/portal-sdk": "0.0.0-20240320165911", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", @@ -26,6 +26,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",