Merge pull request 'Refine Integration' (#13) from riobuenoDevelops/refine-integration into develop

Reviewed-on: #13
This commit is contained in:
Derrick Hammer 2024-03-19 23:50:54 +00:00
commit 5aa62f7d82
16 changed files with 900 additions and 321 deletions

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useMemo} from "react";
import { BaseRecord } from "@refinedev/core"; import { BaseRecord } from "@refinedev/core";
import { useTable } from "@refinedev/react-table"; import { useTable } from "@refinedev/react-table";
import { import {
@ -14,6 +14,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "./ui/table" } from "./ui/table"
import { Skeleton } from "./ui/skeleton";
import { DataTablePagination } from "./table-pagination" import { DataTablePagination } from "./table-pagination"
interface DataTableProps<TData extends BaseRecord = BaseRecord, TValue = unknown> { interface DataTableProps<TData extends BaseRecord = BaseRecord, TValue = unknown> {
@ -21,22 +22,31 @@ interface DataTableProps<TData extends BaseRecord = BaseRecord, TValue = unknown
} }
export function DataTable<TData extends BaseRecord, TValue>({ export function DataTable<TData extends BaseRecord, TValue>({
columns columns,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [hoveredRowId, setHoveredRowId] = useState<string>("");
const table = useTable({ const table = useTable({
columns, columns,
meta: {
hoveredRowId,
},
refineCoreProps: { refineCoreProps: {
resource: "files" resource: "files"
} }
}) })
const loadingRows = useMemo(() => Array(4).fill({}).map(() => ({
getIsSelected: () => false,
getVisibleCells: () => columns.map(() => ({
column: {
columnDef: {
cell: <Skeleton className="h-4 w-full bg-primary-1/30" />,
}
},
getContext: () => null
})),
})), [])
const rows = table.refineCore.tableQueryResult.isLoading ? loadingRows : table.getRowModel().rows
return ( return (
<div className="rounded-lg"> <>
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@ -57,15 +67,12 @@ export function DataTable<TData extends BaseRecord, TValue>({
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows?.length ? ( {rows.length ? (
table.getRowModel().rows.map((row) => ( rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
onMouseEnter={() => { className="group"
console.log(hoveredRowId, row.id);
setHoveredRowId(row.id)
}}
> >
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>
@ -84,6 +91,6 @@ export function DataTable<TData extends BaseRecord, TValue>({
</TableBody> </TableBody>
</Table> </Table>
<DataTablePagination table={table} /> <DataTablePagination table={table} />
</div> </>
) )
} }

View File

@ -3,16 +3,21 @@ import { Avatar } from "./ui/avatar";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { EditIcon, FingerPrintIcon } from "./icons"; import { EditIcon, FingerPrintIcon } from "./icons";
const ManagementCardAvatar = ({ src }: { src?: string }) => { const ManagementCardAvatar = ({ src, button, onClick }: { src?: string; button?: React.ReactNode; onClick?: () => void }) => {
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="relative w-fit h-fit"> <div className="relative w-fit h-fit">
<Avatar className="border-2 border-ring h-28 w-28" /> <Avatar className="border-2 border-ring h-28 w-28" />
<Button {!button
variant="outline" ? <Button
className="absolute bottom-0 right-0 z-50 flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2"> onClick={onClick}
<EditIcon /> variant="outline"
</Button> className="absolute bottom-0 right-0 z-50 flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
<EditIcon />
</Button>
: button
}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,15 @@
import { cn } from "~/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

127
app/components/ui/toast.tsx Normal file
View File

@ -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<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
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<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants> & { cancelMutation?: () => void }
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -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 (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, cancelMutation, ...props }) {
const undoButton = cancelMutation ? <ToastAction altText="Undo" onClick={cancelMutation}>Undo</ToastAction> : undefined
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
{undoButton}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -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<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
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<ToasterToast, "id">
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<State>(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 }

View File

@ -10,10 +10,16 @@ type AccountData = AccountParams;
export const accountProvider: SdkProvider = { export const accountProvider: SdkProvider = {
getList: () => { getList: () => {
throw Error("Not Implemented") console.log("Not implemented");
return Promise.resolve({
data: [],
});
}, },
getOne: () => { getOne: () => {
throw Error("Not Implemented") console.log("Not implemented");
return Promise.resolve({
data: {},
});
}, },
// @ts-ignore // @ts-ignore
async update<TVariables extends AccountParams = AccountParams>( async update<TVariables extends AccountParams = AccountParams>(
@ -44,10 +50,16 @@ export const accountProvider: SdkProvider = {
}; };
}, },
create: () => { create: () => {
throw Error("Not Implemented") console.log("Not implemented");
return Promise.resolve({
data: {},
});
}, },
deleteOne: () => { deleteOne: () => {
throw Error("Not Implemented") console.log("Not implemented");
return Promise.resolve({
data: {},
});
}, },
getApiUrl: () => "", getApiUrl: () => "",
} }

View File

@ -1,11 +1,39 @@
import type { DataProvider } from "@refinedev/core"; import type { DataProvider } from "@refinedev/core";
import {SdkProvider} from "~/data/sdk-provider.js"; import { SdkProvider } from "~/data/sdk-provider.js";
export const fileProvider: SdkProvider = { export const fileProvider = {
getList: () => { throw Error("Not Implemented") }, getList: () => {
getOne: () => { throw Error("Not Implemented") }, console.log("Not implemented");
update: () => { throw Error("Not Implemented") }, return Promise.resolve({
create: () => { throw Error("Not Implemented") }, data: [],
deleteOne: () => { throw Error("Not Implemented") }, 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: () => "", getApiUrl: () => "",
} } satisfies SdkProvider;

View File

@ -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<NotificationProvider, "open"> {
open: (
params: Omit<OpenNotificationParams, "type"> & {
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;
};

View File

@ -7,7 +7,9 @@ import type {LinksFunction} from "@remix-run/node";
import '@fontsource-variable/manrope'; import '@fontsource-variable/manrope';
import {Refine} from "@refinedev/core"; import {Refine} from "@refinedev/core";
import routerProvider from "@refinedev/remix-router"; 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 {getProviders} from "~/data/providers.js";
import {Sdk} from "@lumeweb/portal-sdk"; import {Sdk} from "@lumeweb/portal-sdk";
import resources from "~/data/resources.js"; import resources from "~/data/resources.js";
@ -27,6 +29,7 @@ export function Layout({children}: { children: React.ReactNode }) {
</head> </head>
<body> <body>
{children} {children}
<Toaster />
<ScrollRestoration/> <ScrollRestoration/>
<Scripts/> <Scripts/>
</body> </body>
@ -41,6 +44,7 @@ export default function App() {
<Refine <Refine
authProvider={providers.auth} authProvider={providers.auth}
routerProvider={routerProvider} routerProvider={routerProvider}
notificationProvider={notificationProvider}
dataProvider={providers.default} dataProvider={providers.default}
resources={resources} resources={resources}
options={{disableTelemetry: true}} options={{disableTelemetry: true}}

View File

@ -1,24 +1,19 @@
import {useGo, useIsAuthenticated} from "@refinedev/core"; import { useGo, useIsAuthenticated } from "@refinedev/core";
import {useEffect} from "react";
export default function Index() { export default function Index() {
const {isLoading, data} = useIsAuthenticated(); const { isLoading, data } = useIsAuthenticated();
const go = useGo(); const go = useGo();
useEffect(() => { if (isLoading) {
if (!isLoading) { return <>Checking Login Status</>;
if (data?.authenticated) { }
go({to: "/dashboard", type: "replace"});
} else {
go({to: "/login", type: "replace"});
}
}
}, [isLoading, data]);
if (isLoading) { if (data?.authenticated) {
return <>Checking Login Status</> || null; go({ to: "/dashboard", type: "replace" });
} } else {
go({ to: "/login", type: "replace" });
}
return (<>Redirecting</>) || null; return <>Redirecting</>;
} }

View File

@ -1,5 +1,7 @@
import { getFormProps, useForm } from "@conform-to/react"; import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod"; import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { DialogClose } from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { import {
Authenticated, Authenticated,
BaseKey, BaseKey,
@ -7,11 +9,19 @@ import {
useUpdate, useUpdate,
useUpdatePassword, useUpdatePassword,
} from "@refinedev/core"; } from "@refinedev/core";
import { useState } from "react"; import { useMemo, useState } from "react";
import { z } from "zod"; import { z } from "zod";
import { Field } from "~/components/forms"; import { Field } from "~/components/forms";
import { GeneralLayout } from "~/components/general-layout"; 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 { import {
ManagementCard, ManagementCard,
ManagementCardAvatar, ManagementCardAvatar,
@ -25,6 +35,7 @@ import {
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { UsageCard } from "~/components/usage-card"; import { UsageCard } from "~/components/usage-card";
@ -39,147 +50,169 @@ export default function MyAccount() {
changeEmail: false, changeEmail: false,
changePassword: false, changePassword: false,
setupTwoFactor: false, setupTwoFactor: false,
changeAvatar: false,
}); });
return ( return (
<Authenticated key="account" v3LegacyAuthProviderCompatible> <Authenticated key="account" v3LegacyAuthProviderCompatible>
<GeneralLayout> <GeneralLayout>
<h1 className="text-lg font-bold mb-4">My Account</h1> <h1 className="text-lg font-bold mb-4">My Account</h1>
<UsageCard <Dialog
label="Usage" onOpenChange={(open) => {
currentUsage={2} if (!open) {
monthlyUsage={10} setModal({
icon={<CloudIcon className="text-ring" />} changeEmail: false,
button={ changePassword: false,
<Button variant="accent" className="gap-x-2 h-12"> setupTwoFactor: false,
<AddIcon /> changeAvatar: false,
Upgrade to Premium });
</Button> }
} }}>
/> <UsageCard
<h2 className="font-bold my-8">Account Management</h2> label="Usage"
<div className="grid grid-cols-3 gap-x-8"> currentUsage={2}
<ManagementCard> monthlyUsage={10}
<ManagementCardAvatar /> icon={<CloudIcon className="text-ring" />}
</ManagementCard> button={
<ManagementCard> <Button variant="accent" className="gap-x-2 h-12">
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changeEmail: true })}>
<AddIcon />
Change Email Address
</Button>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold flex gap-x-2">
Lite Premium Account
<CrownIcon />
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon /> <AddIcon />
Upgrade to Premium Upgrade to Premium
</Button> </Button>
</ManagementCardFooter> }
</ManagementCard> />
</div> <h2 className="font-bold my-8">Account Management</h2>
<h2 className="font-bold my-8">Security</h2> <div className="grid grid-cols-3 gap-x-8">
<div className="grid grid-cols-3 gap-x-8"> <ManagementCard>
<ManagementCard> <ManagementCardAvatar
<ManagementCardTitle>Password</ManagementCardTitle> button={
<ManagementCardContent> <DialogTrigger className="absolute bottom-0 right-0 z-50">
<PasswordDots className="mt-6" /> <Button
</ManagementCardContent> onClick={() => setModal({ ...openModal, changeAvatar: true })}
<ManagementCardFooter> variant="outline"
<Button className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
className="h-12 gap-x-2" <EditIcon />
onClick={() => setModal({ ...openModal, changePassword: true })}> </Button>
<AddIcon /> </DialogTrigger>
Change Password }
</Button> />
</ManagementCardFooter> </ManagementCard>
</ManagementCard> <ManagementCard>
<ManagementCard> <ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle> <ManagementCardContent className="text-ring font-semibold">
<ManagementCardContent> {identity?.email}
Improve security by enabling 2FA. </ManagementCardContent>
</ManagementCardContent> <ManagementCardFooter>
<ManagementCardFooter> <DialogTrigger>
<Button <Button
className="h-12 gap-x-2" className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, setupTwoFactor: true })}> onClick={() => setModal({ ...openModal, changeEmail: true })}>
<AddIcon /> <AddIcon />
Enable Two-Factor Authorization Change Email Address
</Button> </Button>
</ManagementCardFooter> </DialogTrigger>
</ManagementCard> </ManagementCardFooter>
</div> </ManagementCard>
<h2 className="font-bold my-8">More</h2> <ManagementCard>
<div className="grid grid-cols-3 gap-x-8"> <ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCard variant="accent"> <ManagementCardContent className="text-ring font-semibold flex gap-x-2">
<ManagementCardTitle>Invite a Friend</ManagementCardTitle> Lite Premium Account
<ManagementCardContent> <CrownIcon />
Get 1 GB per friend invited for free (max 5 GB). </ManagementCardContent>
</ManagementCardContent> <ManagementCardFooter>
<ManagementCardFooter> <Button className="h-12 gap-x-2">
<Button variant="accent" className="h-12 gap-x-2"> <AddIcon />
<AddIcon /> Upgrade to Premium
Send Invitation </Button>
</Button> </ManagementCardFooter>
</ManagementCardFooter> </ManagementCard>
</ManagementCard> </div>
<ManagementCard> <h2 className="font-bold my-8">Security</h2>
<ManagementCardTitle>Read our Resources</ManagementCardTitle> <div className="grid grid-cols-3 gap-x-8">
<ManagementCardContent> <ManagementCard>
Navigate helpful articles or get assistance. <ManagementCardTitle>Password</ManagementCardTitle>
</ManagementCardContent> <ManagementCardContent>
<ManagementCardFooter> <PasswordDots className="mt-6" />
<Button className="h-12 gap-x-2"> </ManagementCardContent>
<AddIcon /> <ManagementCardFooter>
Open Support Centre <DialogTrigger>
</Button> <Button
</ManagementCardFooter> className="h-12 gap-x-2"
</ManagementCard> onClick={() =>
<ManagementCard> setModal({ ...openModal, changePassword: true })
<ManagementCardTitle>Delete Account</ManagementCardTitle> }>
<ManagementCardContent> <AddIcon />
Once initiated, this action cannot be undone. Change Password
</ManagementCardContent> </Button>
<ManagementCardFooter> </DialogTrigger>
<Button className="h-12 gap-x-2" variant="destructive"> </ManagementCardFooter>
<AddIcon /> </ManagementCard>
Delete my Account <ManagementCard>
</Button> <ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle>
</ManagementCardFooter> <ManagementCardContent>
</ManagementCard> Improve security by enabling 2FA.
</div> </ManagementCardContent>
{/* Dialogs must be near to body as possible to open the modal, otherwise will be restricted to parent height-width */} <ManagementCardFooter>
<ChangeEmailForm <DialogTrigger>
open={openModal.changeEmail} <Button
setOpen={(value: boolean) => className="h-12 gap-x-2"
setModal({ ...openModal, changeEmail: value }) onClick={() =>
} setModal({ ...openModal, setupTwoFactor: true })
currentValue={identity?.email || ""} }>
/> <AddIcon />
<ChangePasswordForm Enable Two-Factor Authorization
open={openModal.changePassword} </Button>
setOpen={(value: boolean) => </DialogTrigger>
setModal({ ...openModal, changePassword: value }) </ManagementCardFooter>
} </ManagementCard>
/> </div>
<SetupTwoFactorDialog <h2 className="font-bold my-8">More</h2>
open={openModal.setupTwoFactor} <div className="grid grid-cols-3 gap-x-8">
setOpen={(value: boolean) => <ManagementCard variant="accent">
setModal({ ...openModal, setupTwoFactor: value }) <ManagementCardTitle>Invite a Friend</ManagementCardTitle>
} <ManagementCardContent>
/> Get 1 GB per friend invited for free (max 5 GB).
</ManagementCardContent>
<ManagementCardFooter>
<Button variant="accent" className="h-12 gap-x-2">
<AddIcon />
Send Invitation
</Button>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Read our Resources</ManagementCardTitle>
<ManagementCardContent>
Navigate helpful articles or get assistance.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon />
Open Support Centre
</Button>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Delete Account</ManagementCardTitle>
<ManagementCardContent>
Once initiated, this action cannot be undone.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2" variant="destructive">
<AddIcon />
Delete my Account
</Button>
</ManagementCardFooter>
</ManagementCard>
</div>
<DialogContent>
{openModal.changeAvatar && <ChangeAvatarForm />}
{openModal.changeEmail && (
<ChangeEmailForm currentValue={identity?.email || ""} />
)}
{openModal.changePassword && <ChangePasswordForm />}
{openModal.setupTwoFactor && <SetupTwoFactorDialog />}
</DialogContent>
</Dialog>
</GeneralLayout> </GeneralLayout>
</Authenticated> </Authenticated>
); );
@ -202,15 +235,7 @@ const ChangeEmailSchema = z
return true; return true;
}); });
const ChangeEmailForm = ({ const ChangeEmailForm = ({ currentValue }: { currentValue: string }) => {
open,
setOpen,
currentValue,
}: {
open: boolean;
setOpen: (value: boolean) => void;
currentValue: string;
}) => {
const { data: identity } = useGetIdentity<{ id: BaseKey }>(); const { data: identity } = useGetIdentity<{ id: BaseKey }>();
const { mutate: updateEmail } = useUpdate(); const { mutate: updateEmail } = useUpdate();
const [form, fields] = useForm({ const [form, fields] = useForm({
@ -237,38 +262,36 @@ const ChangeEmailForm = ({
}); });
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <>
<DialogContent className="p-8"> <DialogHeader>
<DialogHeader> <DialogTitle className="mb-8">Change Email</DialogTitle>
<DialogTitle className="mb-8">Change Email</DialogTitle> </DialogHeader>
<div className="rounded-full px-4 py-2 w-fit text-sm bg-ring font-bold text-secondary-1"> <div className="rounded-full px-4 py-2 w-fit text-sm bg-ring font-bold text-secondary-1">
{currentValue} {currentValue}
</div> </div>
<form {...getFormProps(form)}> <form {...getFormProps(form)}>
<Field <Field
className="mt-8" className="mt-8"
inputProps={{ name: fields.email.name }} inputProps={{ name: fields.email.name }}
labelProps={{ children: "New Email Address" }} labelProps={{ children: "New Email Address" }}
errors={fields.email.errors} errors={fields.email.errors}
/> />
<Field <Field
inputProps={{ name: fields.password.name, type: "password" }} inputProps={{ name: fields.password.name, type: "password" }}
labelProps={{ children: "Password" }} labelProps={{ children: "Password" }}
errors={fields.password.errors} errors={fields.password.errors}
/> />
<Field <Field
inputProps={{ inputProps={{
name: fields.retypePassword.name, name: fields.retypePassword.name,
type: "password", type: "password",
}} }}
labelProps={{ children: "Retype Password" }} labelProps={{ children: "Retype Password" }}
errors={fields.retypePassword.errors} errors={fields.retypePassword.errors}
/> />
<Button className="w-full h-14">Change Email Address</Button> <Button className="w-full h-14">Change Email Address</Button>
</form> </form>
</DialogHeader> </>
</DialogContent>
</Dialog>
); );
}; };
@ -289,14 +312,8 @@ const ChangePasswordSchema = z
return true; return true;
}); });
const ChangePasswordForm = ({ const ChangePasswordForm = () => {
open, const { mutate: updatePassword } = useUpdatePassword<{ password: string }>();
setOpen,
}: {
open: boolean;
setOpen: (value: boolean) => void;
}) => {
const { mutate: updatePassword } = useUpdatePassword<UpdatePasswordFormRequest>();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "login", id: "login",
constraint: getZodConstraint(ChangePasswordSchema), constraint: getZodConstraint(ChangePasswordSchema),
@ -310,98 +327,174 @@ const ChangePasswordForm = ({
const data = Object.fromEntries(new FormData(e.currentTarget).entries()); const data = Object.fromEntries(new FormData(e.currentTarget).entries());
updatePassword({ updatePassword({
currentPassword: data.currentPassword.toString(),
password: data.newPassword.toString(), password: data.newPassword.toString(),
}); });
}, },
}); });
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <>
<DialogContent className="p-8"> <DialogHeader>
<DialogHeader> <DialogTitle className="mb-8">Change Password</DialogTitle>
<DialogTitle className="mb-8">Change Password</DialogTitle> </DialogHeader>
<form {...getFormProps(form)}> <form {...getFormProps(form)}>
<Field <Field
inputProps={{ inputProps={{
name: fields.currentPassword.name, name: fields.currentPassword.name,
type: "password", type: "password",
}} }}
labelProps={{ children: "Current Password" }} labelProps={{ children: "Current Password" }}
errors={fields.currentPassword.errors} errors={fields.currentPassword.errors}
/> />
<Field <Field
inputProps={{ name: fields.newPassword.name, type: "password" }} inputProps={{ name: fields.newPassword.name, type: "password" }}
labelProps={{ children: "New Password" }} labelProps={{ children: "New Password" }}
errors={fields.newPassword.errors} errors={fields.newPassword.errors}
/> />
<Field <Field
inputProps={{ inputProps={{
name: fields.retypePassword.name, name: fields.retypePassword.name,
type: "password", type: "password",
}} }}
labelProps={{ children: "Retype Password" }} labelProps={{ children: "Retype Password" }}
errors={fields.retypePassword.errors} errors={fields.retypePassword.errors}
/> />
<Button className="w-full h-14">Change Password</Button> <Button className="w-full h-14">Change Password</Button>
</form> </form>
</DialogHeader> </>
</DialogContent>
</Dialog>
); );
}; };
const SetupTwoFactorDialog = ({ const SetupTwoFactorDialog = () => {
open,
setOpen,
}: {
open: boolean;
setOpen: (value: boolean) => void;
}) => {
const [continueModal, setContinue] = useState<boolean>(false); const [continueModal, setContinue] = useState<boolean>(false);
return ( return (
<Dialog <>
open={open} <DialogHeader>
onOpenChange={(value) => { <DialogTitle className="mb-8">Setup Two-Factor</DialogTitle>
setOpen(value); </DialogHeader>
setContinue(false); <div className="flex flex-col gap-y-6">
}}> {continueModal ? (
<DialogContent className="p-8"> <>
<DialogHeader> <p className="text-sm text-primary-2">
<DialogTitle className="mb-8">Setup Two-Factor</DialogTitle> Enter the authentication code generated in your two-factor
<div className="flex flex-col gap-y-6"> application to confirm your setup.
{continueModal ? ( </p>
<> <Input fullWidth className="text-center" />
<p className="text-sm text-primary-2"> <Button className="w-full h-14">Confirm</Button>
Enter the authentication code generated in your two-factor </>
application to confirm your setup. ) : (
</p> <>
<Input fullWidth className="text-center" /> <div className="p-6 flex justify-center border bg-secondary-2">
<Button className="w-full h-14">Confirm</Button> <img src={QRImg} alt="QR" className="h-36 w-36" />
</> </div>
) : ( <p className="font-semibold">
<> Dont have access to scan? Use this code instead.
<div className="p-6 flex justify-center border bg-secondary-2"> </p>
<img src={QRImg} alt="QR" className="h-36 w-36" /> <div className="p-4 border text-primary-2 text-center font-bold">
</div> HHH7MFGAMPJ44OM44FGAMPJ44O232
<p className="font-semibold"> </div>
Dont have access to scan? Use this code instead. <Button className="w-full h-14" onClick={() => setContinue(true)}>
</p> Continue
<div className="p-4 border text-primary-2 text-center font-bold"> </Button>
HHH7MFGAMPJ44OM44FGAMPJ44O232 </>
</div> )}
<Button </div>
className="w-full h-14" </>
onClick={() => setContinue(true)}> );
Continue };
</Button>
</> const ChangeAvatarForm = () => {
)} const {
</div> getRootProps,
</DialogHeader> getInputProps,
</DialogContent> getFiles,
</Dialog> 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 (
<>
<DialogHeader className="mb-6">
<DialogTitle>Edit Avatar</DialogTitle>
</DialogHeader>
{!hasStarted && !getFiles().length ? (
<div
{...getRootProps()}
className="border border-border rounded text-primary-2 bg-primary-dark h-48 flex flex-col items-center justify-center">
<input
hidden
aria-hidden
name="uppyFiles[]"
key={new Date().toISOString()}
multiple
{...getInputProps()}
/>
<CloudUploadIcon className="w-24 h-24 stroke stroke-primary-dark" />
<p>Drag & Drop Files or Browse</p>
</div>
) : null}
{!hasStarted && file && (
<div className="border border-border rounded p-4 bg-primary-dark relative">
<Button
className="absolute top-1/2 right-1/2 rounded-full bg-gray-800/50 hover:bg-primary p-2 text-sm"
onClick={() => removeFile(file?.id)}>
<Cross2Icon className="size-4" />
</Button>
<img
className="w-full h-48 object-contain"
src={imagePreview}
alt="New Avatar Preview"
/>
</div>
)}
{hasStarted ? (
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" />
{isCompleted ? "Upload completed" : `0% completed`}
</div>
) : null}
{isUploading ? (
<DialogClose asChild onClick={cancelAll}>
<Button size={"lg"} className="mt-6">
Cancel
</Button>
</DialogClose>
) : null}
{isCompleted ? (
<DialogClose asChild>
<Button size={"lg"} className="mt-6">
Close
</Button>
</DialogClose>
) : null}
{!hasStarted && !isCompleted && !isUploading ? (
<Button size={"lg"} className="mt-6" onClick={upload}>
Upload
</Button>
) : null}
</>
); );
}; };

View File

@ -3,6 +3,7 @@ import type { ColumnDef, RowData } from "@tanstack/react-table";
import { FileIcon, MoreIcon } from "~/components/icons"; import { FileIcon, MoreIcon } from "~/components/icons";
import { Checkbox } from "~/components/ui/checkbox"; import { Checkbox } from "~/components/ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu"; 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. // This type is used to define the shape of our data.
// You can use a Zod schema here if you want. // You can use a Zod schema here if you want.
@ -66,12 +67,13 @@ export const columns: ColumnDef<File>[] = [
accessorKey: "createdOn", accessorKey: "createdOn",
size: 200, size: 200,
header: "Created On", header: "Created On",
cell: ({ row, table }) => ( cell: ({ row }) => (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{row.getValue("createdOn")} {row.getValue("createdOn")}
{(row.getIsSelected() || table.options.meta?.hoveredRowId === row.id) && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger className={
cn("hidden group-hover:block data-[state=open]:block", row.getIsSelected() && "block")
}>
<MoreIcon /> <MoreIcon />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
@ -88,7 +90,6 @@ export const columns: ColumnDef<File>[] = [
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)}
</div> </div>
) )
} }

View File

@ -9,7 +9,7 @@ import { Field, FieldCheckbox } from "~/components/forms"
import { getFormProps, useForm } from "@conform-to/react" import { getFormProps, useForm } from "@conform-to/react"
import { z } from "zod" import { z } from "zod"
import { getZodConstraint, parseWithZod } from "@conform-to/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"; import {AuthFormRequest, RegisterFormRequest} from "~/data/auth-provider.js";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
@ -43,8 +43,9 @@ const RegisterSchema = z
}); });
export default function Register() { export default function Register() {
const register = useRegister<RegisterFormRequest>() const register = useRegister<RegisterFormRequest>()
const login = useLogin<AuthFormRequest>(); const login = useLogin<AuthFormRequest>();
const { open } = useNotification();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "register", id: "register",
constraint: getZodConstraint(RegisterSchema), constraint: getZodConstraint(RegisterSchema),
@ -62,6 +63,12 @@ export default function Register() {
lastName: data.lastName.toString(), lastName: data.lastName.toString(),
}, { }, {
onSuccess: () => { 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({ login.mutate({
email: data.email.toString(), email: data.email.toString(),
password: data.password.toString(), password: data.password.toString(),

View File

@ -9,6 +9,8 @@ import { Field } from "~/components/forms";
import { getFormProps, useForm } from "@conform-to/react"; import { getFormProps, useForm } from "@conform-to/react";
import { z } from "zod"; import { z } from "zod";
import { getZodConstraint, parseWithZod } from "@conform-to/zod"; import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { ToastAction } from "~/components/ui/toast";
import { useNotification } from "@refinedev/core";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [{ title: "Sign Up" }]; return [{ title: "Sign Up" }];
@ -18,12 +20,26 @@ const RecoverPasswordSchema = z.object({
email: z.string().email(), email: z.string().email(),
}); });
export default function RecoverPassword() { export default function RecoverPassword() {
const { open } = useNotification();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "sign-up", id: "sign-up",
constraint: getZodConstraint(RecoverPasswordSchema), constraint: getZodConstraint(RecoverPasswordSchema),
onValidate({ formData }) { onValidate({ formData }) {
return parseWithZod(formData, { schema: RecoverPasswordSchema }); 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 doesnt appear within a few minutes, check your spam folder.",
action: <ToastAction altText="Cancel">Cancel</ToastAction>,
cancelMutation: () => {
console.log("cancel mutation");
},
})
}
}); });
// TODO: another detail is the reset password has no screen to either accept a new pass or // TODO: another detail is the reset password has no screen to either accept a new pass or

View File

@ -26,6 +26,7 @@
"@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@refinedev/cli": "^2.16.1", "@refinedev/cli": "^2.16.1",
"@refinedev/core": "https://gitpkg.now.sh/LumeWeb/refine/packages/core?remix", "@refinedev/core": "https://gitpkg.now.sh/LumeWeb/refine/packages/core?remix",
"@refinedev/devtools-internal": "https://gitpkg.now.sh/LumeWeb/refine/packages/devtools-internal?remix", "@refinedev/devtools-internal": "https://gitpkg.now.sh/LumeWeb/refine/packages/devtools-internal?remix",