Compare commits

..

36 Commits

Author SHA1 Message Date
Tania Gutierrez 37ad1d1dc9
feat: Added Dialog to Edit Avatar Card on Account View 2024-03-18 21:42:32 -04:00
Tania Gutierrez add532aa51
feat: Added Notification provided and toast to verify email and recovery password 2024-03-18 21:41:53 -04:00
Tania Gutierrez 43ac8560cb
Merge branch 'develop' into riobuenoDevelops/refine-integration 2024-03-18 15:52:14 -04:00
Tania Gutierrez 85615f53f5
fix: Added Row Skeletons with loading table data 2024-03-18 15:51:02 -04:00
Derrick Hammer dc4f6f7e23
refactor: need to add an uppy pre-processor to configure the tus options per file, including the hashing 2024-03-18 15:32:47 -04:00
Derrick Hammer 043c9b8375
dep: update portal-sdk 2024-03-18 14:32:33 -04:00
Derrick Hammer 7f8310ea9c
debug: disable minify 2024-03-18 13:02:48 -04:00
Derrick Hammer 6794bb2a4a
dep: update portal-sdk 2024-03-18 13:02:22 -04:00
Derrick Hammer 7779f794e4
fix: bind handleUpload to current instance 2024-03-18 12:50:44 -04:00
Derrick Hammer 2402d3e076
fix: set auth token globally on sdk after login 2024-03-18 12:50:26 -04:00
Derrick Hammer 2140a63add
fix: put plugin logic inside setInputProps onChange 2024-03-18 12:37:22 -04:00
Derrick Hammer 756a505590
fix: uppy requires a type 2024-03-18 12:36:58 -04:00
Derrick Hammer 7e78f17937
ci: disable protocolImports 2024-03-18 11:57:38 -04:00
Derrick Hammer ed410051e2
ci: add vite-plugin-node-polyfills plugin 2024-03-18 11:37:44 -04:00
Derrick Hammer 42de97519b
dep: update portal-sdk 2024-03-18 11:37:17 -04:00
Derrick Hammer d11bf0861c
dep: update portal-sdk 2024-03-18 11:18:29 -04:00
Derrick Hammer a3d958065c
dep: update portal-sdk 2024-03-18 11:13:06 -04:00
Juan Di Toro 26b0246429 Merge branch 'develop' of git.lumeweb.com:LumeWeb/portal-dashboard into develop 2024-03-18 16:07:24 +01:00
Juan Di Toro c9d956e1b6 fix: remove the backup key card from my account 2024-03-18 16:07:11 +01:00
Derrick Hammer ec9509ef6c
dep: update portal-sdk 2024-03-18 11:06:45 -04:00
Derrick Hammer 8a3181177b
dep: update portal-sdk 2024-03-18 11:01:13 -04:00
Derrick Hammer 84f7585a66
dep: update portal-sdk 2024-03-18 10:56:20 -04:00
Derrick Hammer 3a4b40ef27
refactor: remove uploader from uppy hook and check by the upload size limit if we use tus plugin or just file post. additionally add a onBeforeUpload filter to set the custom uploader property. 2024-03-18 10:12:27 -04:00
Derrick Hammer 988780b25f
feat: add initial plugin for a post file upload via s5 sdk 2024-03-18 10:11:07 -04:00
Derrick Hammer dca77ba71a
fix: add patch for uppy tus to filter on files with a custom uploader property set to tus 2024-03-18 10:10:33 -04:00
Derrick Hammer 7f26bc1060
feat: add sdk context and useSdk 2024-03-18 10:09:37 -04:00
Derrick Hammer 8643363736
refactor: export RequiredAuthProvider 2024-03-18 10:09:08 -04:00
Derrick Hammer 7c332d0f43
refactor: add sdk getter 2024-03-18 10:08:57 -04:00
Derrick Hammer d3e847baf8
dep: update portal-sdk 2024-03-18 10:08:15 -04:00
Derrick Hammer 06db71bd8a
dep: update portal-sdk 2024-03-17 11:26:23 -04:00
Derrick Hammer 928deb89b6
refactor: update logout text name 2024-03-17 09:48:21 -04:00
Derrick Hammer 245467155d
refactor: don't try to delete the auth cookie 2024-03-17 09:41:52 -04:00
Derrick Hammer fd7fec8580
dep: just use universal-cookie 2024-03-17 09:41:12 -04:00
Derrick Hammer 0e083c7a58
dep: update portal-sdk 2024-03-17 09:37:53 -04:00
Derrick Hammer 9fd55bf994
refactor: update auth token name 2024-03-17 08:18:33 -04:00
Derrick Hammer 20533913bd
refactor: switch to reading cookie only and let server handle it for security, add maybeSetupAuth helper. 2024-03-16 15:41:31 -04:00
21 changed files with 926 additions and 170 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

@ -108,8 +108,8 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem onClick={() => logout()}> <DropdownMenuItem onClick={() => logout()}>
<ExitIcon className="mr-2" /> <ExitIcon className="mr-2" />
Log Out Logout
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@ -0,0 +1,18 @@
import React from "react";
import {Sdk} from "@lumeweb/portal-sdk";
export const SdkContext = React.createContext<
Partial<Sdk>
>({});
export const SdkContextProvider: React.FC< {sdk: Sdk, children: React.ReactNode}> = ({sdk, children}) => {
return (
<SdkContext.Provider value={sdk}>
{children}
</SdkContext.Provider>
);
};
export function useSdk(): Partial<Sdk>{
return React.useContext(SdkContext);
}

View File

@ -0,0 +1,63 @@
import Uppy, {BasePlugin, DefaultPluginOptions} from '@uppy/core';
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
import {S5Client} from "@lumeweb/s5-js";
import {AxiosProgressEvent} from "axios";
interface UppyFileUploadOptions extends DefaultPluginOptions {
sdk: Sdk;
}
export default class UppyFileUpload extends BasePlugin {
private _sdk: Sdk;
constructor(uppy: Uppy, opts?: UppyFileUploadOptions) {
super(uppy, opts);
this.id = opts?.id || 'file-upload';
this.type = 'uploader';
this._sdk = opts?.sdk as Sdk;
}
install() {
this.uppy.addUploader(this.handleUpload.bind(this));
}
private async handleUpload(fileIDs: string[]) {
for (const fileID of fileIDs) {
const file = this.uppy.getFile(fileID);
if (!file) {
continue;
}
// @ts-ignore
if (file.uploader !== 'file') {
continue;
}
const uploadLimit = await this._sdk.account().uploadLimit();
let data = file.data;
if (file.data instanceof Blob) {
data = new File([data], file.name, {type: file.type});
}
try {
await this._sdk.protocols().get<S5Client>(PROTOCOL_S5).getSdk().uploadFile(data as File, {
largeFileSize: uploadLimit,
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
uploader: this,
bytesUploaded: progressEvent.loaded,
bytesTotal: progressEvent.total,
})
}
});
this.uppy.emit('upload-success', file, {uploadURL: null});
} catch (err) {
this.uppy.emit('upload-error', file, err);
}
}
}
}

View File

@ -1,17 +1,14 @@
import Uppy, { type State, debugLogger } from "@uppy/core" import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
import Tus from "@uppy/tus" import Tus from "@uppy/tus"
import toArray from "@uppy/utils/lib/toArray" import toArray from "@uppy/utils/lib/toArray"
import { import {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
type ChangeEvent, import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
useCallback, import {useSdk} from "~/components/lib/sdk-context.js";
useEffect, import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
useMemo, import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
useRef, import {S5Client} from "@lumeweb/s5-js";
useState
} from "react"
import DropTarget, { type DropTargetOptions } from "./uppy-dropzone"
const LISTENING_EVENTS = [ const LISTENING_EVENTS = [
"upload", "upload",
@ -23,12 +20,26 @@ const LISTENING_EVENTS = [
] as const ] as const
export function useUppy({ export function useUppy({
uploader,
endpoint endpoint
}: { }: {
uploader: "tus"
endpoint: string endpoint: string
}) { }) {
const sdk = useSdk()
const [uploadLimit, setUploadLimit] = useState<number>(0)
useEffect(() => {
async function getUploadLimit() {
try {
const limit = await sdk.account!().uploadLimit();
setUploadLimit(limit);
} catch (err) {
console.log('Error occured while fetching upload limit', err);
}
}
getUploadLimit();
}, []);
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null) const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
const uppyInstance = useRef<Uppy>() const uppyInstance = useRef<Uppy>()
@ -82,7 +93,29 @@ export function useUppy({
useEffect(() => { useEffect(() => {
if (!targetRef) return if (!targetRef) return
const uppy = new Uppy({ logger: debugLogger }).use(DropTarget, { const tusPreprocessor = async (fileIDs: string[]) => {
for(const fileID of fileIDs) {
const file = uppyInstance.current?.getFile(fileID) as UppyFile
// @ts-ignore
if (file.uploader === "tus") {
uppyInstance.current?.setFileState(fileID, {
tus: await sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File)
})
}
}
}
const uppy = new Uppy({
logger: debugLogger,
onBeforeUpload: (files) => {
for (const file of Object.entries(files)) {
// @ts-ignore
file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
}
return true;
},
}).use(DropTarget, {
target: targetRef target: targetRef
} as DropTargetOptions) } as DropTargetOptions)
@ -97,6 +130,25 @@ export function useUppy({
uppyInstance.current?.addFiles(files) uppyInstance.current?.addFiles(files)
} }
uppy.iteratePlugins((plugin) => {
uppy.removePlugin(plugin);
});
uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
let useTus = false;
uppyInstance.current?.getFiles().forEach((file) => {
if (file.size > uploadLimit) {
useTus = true;
}
})
if (useTus) {
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
uppy.addPreProcessor(tusPreprocessor)
}
// We clear the input after a file is selected, because otherwise // We clear the input after a file is selected, because otherwise
// change event is not fired in Chrome and Safari when a file // change event is not fired in Chrome and Safari when a file
// with the same name is selected. // with the same name is selected.
@ -109,12 +161,7 @@ export function useUppy({
} }
}) })
switch (uploader) {
case "tus":
uppy.use(Tus, { endpoint: endpoint, limit: 6 })
break
default:
}
uppy.on("complete", (result) => { uppy.on("complete", (result) => {
if (result.failed.length === 0) { if (result.failed.length === 0) {
@ -148,7 +195,7 @@ export function useUppy({
}) })
} }
setState("idle") setState("idle")
}, [targetRef, endpoint, uploader]) }, [targetRef, endpoint, uploadLimit])
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

@ -3,12 +3,13 @@ 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, onClick }: { src?: string; 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
onClick={onClick}
variant="outline" variant="outline"
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"> 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 /> <EditIcon />

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>
>(({ 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,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 (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<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

@ -33,10 +33,8 @@ export type Identity = {
} }
export class PortalAuthProvider implements RequiredAuthProvider { export class PortalAuthProvider implements RequiredAuthProvider {
private sdk: Sdk;
constructor(apiUrl: string) { constructor(apiUrl: string) {
this.sdk = Sdk.create(apiUrl); this._sdk = Sdk.create(apiUrl);
const methods: Array<keyof AuthProvider> = [ const methods: Array<keyof AuthProvider> = [
'login', 'login',
@ -55,9 +53,18 @@ export class PortalAuthProvider implements RequiredAuthProvider {
}); });
} }
private _sdk: Sdk;
get sdk(): Sdk {
return this._sdk;
}
public static create(apiUrl: string): AuthProvider {
return new PortalAuthProvider(apiUrl);
}
async login(params: AuthFormRequest): Promise<AuthActionResponse> { async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const cookies = new Cookies(); const ret = await this._sdk.account().login({
const ret = await this.sdk.account().login({
email: params.email, email: params.email,
password: params.password, password: params.password,
}) })
@ -65,11 +72,11 @@ export class PortalAuthProvider implements RequiredAuthProvider {
let redirectTo: string | undefined; let redirectTo: string | undefined;
if (ret) { if (ret) {
cookies.set('jwt', this.sdk.account().jwtToken, {path: '/'});
redirectTo = params.redirectTo; redirectTo = params.redirectTo;
if (!redirectTo) { if (!redirectTo) {
redirectTo = ret ? "/dashboard" : "/login"; redirectTo = ret ? "/dashboard" : "/login";
} }
this._sdk.setAuthToken(this._sdk.account().jwtToken);
} }
return { return {
@ -79,38 +86,25 @@ export class PortalAuthProvider implements RequiredAuthProvider {
} }
async logout(params: any): Promise<AuthActionResponse> { async logout(params: any): Promise<AuthActionResponse> {
let ret = await this.sdk.account().logout(); let ret = await this._sdk.account().logout();
if (ret) {
const cookies = new Cookies();
cookies.remove('jwt');
}
return {success: ret, redirectTo: "/login"}; return {success: ret, redirectTo: "/login"};
} }
async check(params?: any): Promise<CheckResponse> { async check(params?: any): Promise<CheckResponse> {
const cookies = new Cookies(); this.maybeSetupAuth();
const jwtCookie = cookies.get('jwt'); const ret = await this._sdk.account().ping();
if (jwtCookie) {
this.sdk.setAuthToken(jwtCookie);
}
const ret = await this.sdk.account().ping();
if (!ret) {
cookies.remove('jwt');
}
return {authenticated: ret, redirectTo: ret ? undefined : "/login"}; return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
} }
async onError(error: any): Promise<OnErrorResponse> { async onError(error: any): Promise<OnErrorResponse> {
const cookies = new Cookies();
return {logout: true}; return {logout: true};
} }
async register(params: RegisterFormRequest): Promise<AuthActionResponse> { async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
const ret = await this.sdk.account().register({ const ret = await this._sdk.account().register({
email: params.email, email: params.email,
password: params.password, password: params.password,
first_name: params.firstName, first_name: params.firstName,
@ -132,7 +126,8 @@ export class PortalAuthProvider implements RequiredAuthProvider {
} }
async getIdentity(params?: Identity): Promise<IdentityResponse> { async getIdentity(params?: Identity): Promise<IdentityResponse> {
const ret = await this.sdk.account().info(); this.maybeSetupAuth();
const ret = await this._sdk.account().info();
if (!ret) { if (!ret) {
return {identity: null}; return {identity: null};
@ -148,12 +143,16 @@ export class PortalAuthProvider implements RequiredAuthProvider {
}; };
} }
public static create(apiUrl: string): AuthProvider { maybeSetupAuth(): void {
return new PortalAuthProvider(apiUrl); const cookies = new Cookies();
const jwtCookie = cookies.get('auth_token');
if (jwtCookie) {
this._sdk.setAuthToken(jwtCookie);
}
} }
} }
interface RequiredAuthProvider extends AuthProvider { export interface RequiredAuthProvider extends AuthProvider {
login: AuthProvider['login']; login: AuthProvider['login'];
logout: AuthProvider['logout']; logout: AuthProvider['logout'];
check: AuthProvider['check']; check: AuthProvider['check'];

View File

@ -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: () => {}
}
};

55
app/data/pinning.ts Normal file
View File

@ -0,0 +1,55 @@
interface PinningStatus {
id: string;
progress: number;
status: 'inprogress' | 'completed' | 'stale';
}
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
class PinningProcess {
private static instances: Map<string, PinningStatus> = new Map();
static async pin(id: string): Promise<{ success: boolean; message: string }> {
if (PinningProcess.instances.has(id)) {
return { success: false, message: "ID is already being processed" };
}
const pinningStatus: PinningStatus = { id, progress: 0, status: 'inprogress' };
PinningProcess.instances.set(id, pinningStatus);
// Simulate async progress
(async () => {
for (let progress = 1; progress <= 100; progress++) {
await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (500 - 100 + 1)) + 100)); // Simulate time passing with random duration between 100 and 500
pinningStatus.progress = progress;
if (progress === 100) {
pinningStatus.status = 'completed';
}
}
})();
return { success: true, message: "Pinning process started" };
}
static *pollProgress(id: string): Generator<PinningStatus | null, void, unknown> {
let status = PinningProcess.instances.get(id);
while (status && status.status !== 'completed') {
yield status;
status = PinningProcess.instances.get(id);
}
yield status ?? null; // Yield the final status, could be null if ID doesn't exist
}
}
// Example usage:
// (async () => {
// const { success, message } = await PinningProcess.pin("123");
// console.log(message);
// if (success) {
// const progressGenerator = PinningProcess.pollProgress("123");
// let result = progressGenerator.next();
// while (!result.done) {
// console.log(result.value); // Log the progress
// result = progressGenerator.next();
// }
// }
// })();

View File

@ -12,9 +12,13 @@ import type { LinksFunction } from "@remix-run/node";
// Supports weights 200-800 // Supports weights 200-800
import '@fontsource-variable/manrope'; import '@fontsource-variable/manrope';
import {Refine} from "@refinedev/core"; import {Refine} from "@refinedev/core";
import {PortalAuthProvider} from "~/data/auth-provider.js";
import routerProvider from "@refinedev/remix-router"; import routerProvider from "@refinedev/remix-router";
import { defaultProvider } from "./data/file-provider"; 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 = () => [ export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet }, { rel: "stylesheet", href: stylesheet },
@ -31,6 +35,7 @@ export function Layout({children}: { children: React.ReactNode }) {
</head> </head>
<body> <body>
{children} {children}
<Toaster />
<ScrollRestoration/> <ScrollRestoration/>
<Scripts/> <Scripts/>
</body> </body>
@ -39,17 +44,21 @@ export function Layout({children}: { children: React.ReactNode }) {
} }
export default function App() { export default function App() {
const auth = PortalAuthProvider.create("https://alpha.pinner.xyz")
return ( return (
<Refine <Refine
authProvider={PortalAuthProvider.create("https://alpha.pinner.xyz")} authProvider={auth}
routerProvider={routerProvider} routerProvider={routerProvider}
dataProvider={defaultProvider} dataProvider={defaultProvider}
notificationProvider={notificationProvider}
resources={[ resources={[
{ name: 'files' }, { name: 'files' },
{ name: 'users' } { name: 'users' }
]} ]}
> >
<Outlet/> <SdkContextProvider sdk={(auth as PortalAuthProvider).sdk}>
<Outlet/>
</SdkContextProvider>
</Refine> </Refine>
); );
} }

View File

@ -1,12 +1,26 @@
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 { BaseKey, useGetIdentity, useUpdate, useUpdatePassword } from "@refinedev/core"; import { DialogClose } from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import {
BaseKey,
useGetIdentity,
useUpdate,
useUpdatePassword,
} from "@refinedev/core";
import { useState } from "react"; import { 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, TrashIcon } from "~/components/icons";
import { ManagementCard, ManagementCardAvatar, ManagementCardContent, ManagementCardFooter, ManagementCardTitle } from "~/components/management-card"; import { useUppy } from "~/components/lib/uppy";
import {
ManagementCard,
ManagementCardAvatar,
ManagementCardContent,
ManagementCardFooter,
ManagementCardTitle,
} from "~/components/management-card";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
Dialog, Dialog,
@ -26,6 +40,7 @@ export default function MyAccount() {
changeEmail: false, changeEmail: false,
changePassword: false, changePassword: false,
setupTwoFactor: false, setupTwoFactor: false,
changeAvatar: false,
}); });
return ( return (
@ -46,15 +61,19 @@ export default function MyAccount() {
<h2 className="font-bold my-8">Account Management</h2> <h2 className="font-bold my-8">Account Management</h2>
<div className="grid grid-cols-3 gap-x-8"> <div className="grid grid-cols-3 gap-x-8">
<ManagementCard> <ManagementCard>
<ManagementCardAvatar /> <ManagementCardAvatar
onClick={() => setModal({ ...openModal, changeAvatar: true })}
/>
</ManagementCard> </ManagementCard>
<ManagementCard> <ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle> <ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold"> <ManagementCardContent className="text-ring font-semibold">
{identity?.email} {identity?.email}
</ManagementCardContent> </ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, changeEmail: true })}> <Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changeEmail: true })}>
<AddIcon /> <AddIcon />
Change Email Address Change Email Address
</Button> </Button>
@ -63,8 +82,8 @@ export default function MyAccount() {
<ManagementCard> <ManagementCard>
<ManagementCardTitle>Account Type</ManagementCardTitle> <ManagementCardTitle>Account Type</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold flex gap-x-2"> <ManagementCardContent className="text-ring font-semibold flex gap-x-2">
Lite Premium Account Lite Premium Account
<CrownIcon /> <CrownIcon />
</ManagementCardContent> </ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button className="h-12 gap-x-2"> <Button className="h-12 gap-x-2">
@ -82,7 +101,9 @@ export default function MyAccount() {
<PasswordDots className="mt-6" /> <PasswordDots className="mt-6" />
</ManagementCardContent> </ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, changePassword: true })}> <Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changePassword: true })}>
<AddIcon /> <AddIcon />
Change Password Change Password
</Button> </Button>
@ -91,35 +112,27 @@ export default function MyAccount() {
<ManagementCard> <ManagementCard>
<ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle> <ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle>
<ManagementCardContent> <ManagementCardContent>
Improve security by enabling 2FA. Improve security by enabling 2FA.
</ManagementCardContent> </ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button className="h-12 gap-x-2" onClick={() => setModal({ ...openModal, setupTwoFactor: true })}> <Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, setupTwoFactor: true })}>
<AddIcon /> <AddIcon />
Enable Two-Factor Authorization Enable Two-Factor Authorization
</Button> </Button>
</ManagementCardFooter> </ManagementCardFooter>
</ManagementCard> </ManagementCard>
<ManagementCard>
<ManagementCardTitle>Backup Key</ManagementCardTitle>
<ManagementCardContent>
Never share this code with anyone.
</ManagementCardContent>
<ManagementCardFooter>
<Button className="h-12 gap-x-2">
<AddIcon />
Export Backup Key
</Button>
</ManagementCardFooter>
</ManagementCard>
</div> </div>
<h2 className="font-bold my-8">More</h2> <h2 className="font-bold my-8">More</h2>
<div className="grid grid-cols-3 gap-x-8"> <div className="grid grid-cols-3 gap-x-8">
<ManagementCard variant="accent"> <ManagementCard variant="accent">
<ManagementCardTitle>Invite a Friend</ManagementCardTitle> <ManagementCardTitle>Invite a Friend</ManagementCardTitle>
<ManagementCardContent>Get 1 GB per friend invited for free (max 5 GB).</ManagementCardContent> <ManagementCardContent>
Get 1 GB per friend invited for free (max 5 GB).
</ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button variant="accent" className="h-12 gap-x-2"> <Button variant="accent" className="h-12 gap-x-2">
<AddIcon /> <AddIcon />
Send Invitation Send Invitation
</Button> </Button>
@ -127,7 +140,9 @@ export default function MyAccount() {
</ManagementCard> </ManagementCard>
<ManagementCard> <ManagementCard>
<ManagementCardTitle>Read our Resources</ManagementCardTitle> <ManagementCardTitle>Read our Resources</ManagementCardTitle>
<ManagementCardContent>Navigate helpful articles or get assistance.</ManagementCardContent> <ManagementCardContent>
Navigate helpful articles or get assistance.
</ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button className="h-12 gap-x-2"> <Button className="h-12 gap-x-2">
<AddIcon /> <AddIcon />
@ -137,7 +152,9 @@ export default function MyAccount() {
</ManagementCard> </ManagementCard>
<ManagementCard> <ManagementCard>
<ManagementCardTitle>Delete Account</ManagementCardTitle> <ManagementCardTitle>Delete Account</ManagementCardTitle>
<ManagementCardContent>Once initiated, this action cannot be undone.</ManagementCardContent> <ManagementCardContent>
Once initiated, this action cannot be undone.
</ManagementCardContent>
<ManagementCardFooter> <ManagementCardFooter>
<Button className="h-12 gap-x-2" variant="destructive"> <Button className="h-12 gap-x-2" variant="destructive">
<AddIcon /> <AddIcon />
@ -147,6 +164,12 @@ export default function MyAccount() {
</ManagementCard> </ManagementCard>
</div> </div>
{/* Dialogs must be near to body as possible to open the modal, otherwise will be restricted to parent height-width */} {/* Dialogs must be near to body as possible to open the modal, otherwise will be restricted to parent height-width */}
<ChangeAvatarForm
open={openModal.changeAvatar}
setOpen={(value: boolean) =>
setModal({ ...openModal, changeAvatar: value })
}
/>
<ChangeEmailForm <ChangeEmailForm
open={openModal.changeEmail} open={openModal.changeEmail}
setOpen={(value: boolean) => setOpen={(value: boolean) =>
@ -170,21 +193,22 @@ export default function MyAccount() {
); );
} }
const ChangeEmailSchema = z.object({ const ChangeEmailSchema = z
email: z.string().email(), .object({
password: z.string(), email: z.string().email(),
retypePassword: z.string(), password: z.string(),
}) retypePassword: z.string(),
.superRefine((data, ctx) => { })
if (data.password !== data.retypePassword) { .superRefine((data, ctx) => {
return ctx.addIssue({ if (data.password !== data.retypePassword) {
code: z.ZodIssueCode.custom, return ctx.addIssue({
path: ["retypePassword"], code: z.ZodIssueCode.custom,
message: "Passwords do not match", path: ["retypePassword"],
}); message: "Passwords do not match",
} });
return true; }
}); return true;
});
const ChangeEmailForm = ({ const ChangeEmailForm = ({
open, open,
@ -195,7 +219,7 @@ const ChangeEmailForm = ({
setOpen: (value: boolean) => void; setOpen: (value: boolean) => void;
currentValue: string; 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({
id: "login", id: "login",
@ -210,18 +234,18 @@ const ChangeEmailForm = ({
const data = Object.fromEntries(new FormData(e.currentTarget).entries()); const data = Object.fromEntries(new FormData(e.currentTarget).entries());
console.log(identity); console.log(identity);
updateEmail({ updateEmail({
resource: 'users', resource: "users",
id: identity?.id || "", id: identity?.id || "",
values: { values: {
email: data.email.toString() email: data.email.toString(),
} },
}) });
} },
}); });
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="p-8"> <DialogContent className="p-8" forceMount>
<DialogHeader> <DialogHeader>
<DialogTitle className="mb-8">Change Email</DialogTitle> <DialogTitle className="mb-8">Change Email</DialogTitle>
<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">
@ -255,21 +279,22 @@ const ChangeEmailForm = ({
); );
}; };
const ChangePasswordSchema = z.object({ const ChangePasswordSchema = z
currentPassword: z.string().email(), .object({
newPassword: z.string(), currentPassword: z.string().email(),
retypePassword: z.string(), newPassword: z.string(),
}) retypePassword: z.string(),
.superRefine((data, ctx) => { })
if (data.newPassword !== data.retypePassword) { .superRefine((data, ctx) => {
return ctx.addIssue({ if (data.newPassword !== data.retypePassword) {
code: z.ZodIssueCode.custom, return ctx.addIssue({
path: ["retypePassword"], code: z.ZodIssueCode.custom,
message: "Passwords do not match", path: ["retypePassword"],
}); message: "Passwords do not match",
} });
return true; }
}); return true;
});
const ChangePasswordForm = ({ const ChangePasswordForm = ({
open, open,
@ -290,11 +315,10 @@ const ChangePasswordForm = ({
e.preventDefault(); e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries()); const data = Object.fromEntries(new FormData(e.currentTarget).entries());
updatePassword({
password: data.newPassword.toString()
});
updatePassword({
password: data.newPassword.toString(),
});
}, },
}); });
@ -387,9 +411,109 @@ const SetupTwoFactorDialog = ({
); );
}; };
const ChangeAvatarForm = ({
open,
setOpen,
}: {
open: boolean;
setOpen: (value: boolean) => void;
}) => {
const {
getRootProps,
getInputProps,
getFiles,
upload,
state,
removeFile,
cancelAll,
} = useUppy({
uploader: "tus",
endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT,
});
console.log({ state, files: getFiles() });
const isUploading = state === "uploading";
const isCompleted = state === "completed";
const hasStarted = state !== "idle" && state !== "initializing";
return (
<Dialog
open={open}
onOpenChange={(value) => {
setOpen(value);
}}>
<DialogContent className="p-8">
<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 && getFiles().length > 0) && (
<div className="border border-border rounded p-4 bg-primary-dark relative">
<Button
className="absolute top-4 right-4 rounded-full aspect-square bg-primary-dark hover:bg-primary p-2 text-sm"
onClick={() => removeFile(getFiles()[0].id)}>
<Cross2Icon />
</Button>
<img className="w-full h-48" src={URL.createObjectURL(getFiles()[0].data)} 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}
</DialogContent>
</Dialog>
);
};
const PasswordDots = ({ className }: { className?: string }) => { const PasswordDots = ({ className }: { className?: string }) => {
return ( return (
<svg <svg
aria-hidden="true"
width="219" width="219"
height="7" height="7"
viewBox="0 0 219 7" viewBox="0 0 219 7"
@ -414,4 +538,4 @@ const PasswordDots = ({ className }: { className?: string }) => {
<circle cx="215.5" cy="3.5" r="3.5" fill="currentColor" /> <circle cx="215.5" cy="3.5" r="3.5" fill="currentColor" />
</svg> </svg>
); );
}; };

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,7 @@ 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 { useToast } from "~/components/ui/use-toast";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [{ title: "Sign Up" }]; return [{ title: "Sign Up" }];
@ -18,12 +19,23 @@ const RecoverPasswordSchema = z.object({
email: z.string().email(), email: z.string().email(),
}); });
export default function RecoverPassword() { export default function RecoverPassword() {
const { toast } = useToast();
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();
toast({
title: "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.",
variant: "success",
key: "reset-password-email-sent",
});
}
}); });
// 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

@ -14,7 +14,7 @@
"@conform-to/react": "^1.0.2", "@conform-to/react": "^1.0.2",
"@conform-to/zod": "^1.0.2", "@conform-to/zod": "^1.0.2",
"@fontsource-variable/manrope": "^5.0.19", "@fontsource-variable/manrope": "^5.0.19",
"@lumeweb/portal-sdk": "0.0.0-20240314110748", "@lumeweb/portal-sdk": "0.0.0-20240318183202",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
@ -24,6 +24,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",
@ -40,10 +41,10 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"universal-cookie": "^7.1.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
@ -62,6 +63,7 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^5.1.0", "vite": "^5.1.0",
"vite-plugin-node-polyfills": "^0.21.0",
"vite-tsconfig-paths": "^4.2.1" "vite-tsconfig-paths": "^4.2.1"
}, },
"engines": { "engines": {

View File

@ -0,0 +1,13 @@
diff --git a/node_modules/@uppy/tus/lib/index.js b/node_modules/@uppy/tus/lib/index.js
index 1e0a1bb..ba95bb5 100644
--- a/node_modules/@uppy/tus/lib/index.js
+++ b/node_modules/@uppy/tus/lib/index.js
@@ -506,7 +506,7 @@ function _getCompanionClientArgs2(file) {
}
async function _uploadFiles2(files) {
const filesFiltered = filterNonFailedFiles(files);
- const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered);
+ const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered).filter(file => file?.uploader == 'tus');
this.uppy.emit('upload-start', filesToEmit);
await Promise.allSettled(filesFiltered.map(file => {
if (file.isRemote) {

View File

@ -1,27 +1,32 @@
import { vitePlugin as remix } from "@remix-run/dev" import {vitePlugin as remix} from "@remix-run/dev"
import { defineConfig } from "vite" import {defineConfig} from "vite"
import tsconfigPaths from "vite-tsconfig-paths" import tsconfigPaths from "vite-tsconfig-paths"
import {nodePolyfills} from 'vite-plugin-node-polyfills'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
remix({ remix({
ssr: false, ssr: false,
ignoredRouteFiles: ["**/*.css"] ignoredRouteFiles: ["**/*.css"]
}), }),
tsconfigPaths() tsconfigPaths(),
], nodePolyfills({protocolImports: false}),
server: { ],
fs: { build: {
// Restrict files that could be served by Vite's dev server. Accessing minify: false
// files outside this directory list that aren't imported from an allowed },
// file will result in a 403. Both directories and files can be provided. server: {
// If you're comfortable with Vite's dev server making any file within the fs: {
// project root available, you can remove this option. See more: // Restrict files that could be served by Vite's dev server. Accessing
// https://vitejs.dev/config/server-options.html#server-fs-allow // files outside this directory list that aren't imported from an allowed
allow: [ // file will result in a 403. Both directories and files can be provided.
"app", // If you're comfortable with Vite's dev server making any file within the
"node_modules/@fontsource-variable/manrope", // project root available, you can remove this option. See more:
] // https://vitejs.dev/config/server-options.html#server-fs-allow
allow: [
"app",
"node_modules/@fontsource-variable/manrope",
]
}
} }
}
}) })