Compare commits

..

No commits in common. "c9de506c565db301017a22e85856a9a462b42cba" and "7b31d561fed8bf8d60f6a2991b6bb6a49eadf70b" have entirely different histories.

23 changed files with 722 additions and 975 deletions

View File

@ -1,8 +1,8 @@
import { useMemo} from "react"; import { useMemo} from "react";
import type { BaseRecord } from "@refinedev/core"; import { BaseRecord } from "@refinedev/core";
import { useTable } from "@refinedev/react-table"; import { useTable } from "@refinedev/react-table";
import { import {
type ColumnDef, ColumnDef,
flexRender, flexRender,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
@ -56,9 +56,9 @@ export function DataTable<TData extends BaseRecord, TValue>({
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header, index) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={`FileDataTableHeader_${index}`} style={{ width: header.getSize() }}> <TableHead key={header.id} style={{ width: header.getSize() }}>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
@ -73,14 +73,14 @@ export function DataTable<TData extends BaseRecord, TValue>({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rows.length ? ( {rows.length ? (
rows.map((row, index) => ( rows.map((row) => (
<TableRow <TableRow
key={`FileDataTableRow_${index}`} key={row.id}
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
className="group" className="group"
> >
{row.getVisibleCells().map((cell, index) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={`FileDataTableCell_${index}`}> <TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}

View File

@ -1,6 +1,6 @@
import { Label } from "@radix-ui/react-label" import { Label } from "@radix-ui/react-label"
import { Input } from "./ui/input" import { Input } from "./ui/input"
import { type FieldName, useInputControl } from "@conform-to/react" import { FieldName, useInputControl } from "@conform-to/react"
import { useId } from "react" import { useId } from "react"
import { cn } from "~/utils" import { cn } from "~/utils"
import { Checkbox } from "~/components/ui/checkbox" import { Checkbox } from "~/components/ui/checkbox"
@ -58,7 +58,7 @@ export const FieldCheckbox = ({
const input = useInputControl({ const input = useInputControl({
key, key,
name: inputProps.name, name: inputProps.name,
formId: inputProps.form, formId: inputProps.form,
initialValue: defaultChecked ? checkedValue : undefined initialValue: defaultChecked ? checkedValue : undefined
}) })
const fallbackId = useId() const fallbackId = useId()

View File

@ -25,30 +25,20 @@ import {
PageIcon, PageIcon,
ThemeIcon, ThemeIcon,
} from "./icons"; } from "./icons";
import { import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem } from "./ui/dropdown-menu";
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
} from "./ui/dropdown-menu";
import { Avatar } from "@radix-ui/react-avatar"; import { Avatar } from "@radix-ui/react-avatar";
import { cn } from "~/utils"; import { cn } from "~/utils";
import { useGetIdentity, useLogout } from "@refinedev/core"; import { useGetIdentity, useLogout } from "@refinedev/core";
import { Identity } from "~/data/auth-provider";
import { PinningNetworkBanner } from "./pinning-network-banner"; import { PinningNetworkBanner } from "./pinning-network-banner";
import { PinningProvider } from "~/providers/PinningProvider"; import { PinningProvider } from "~/providers/PinningProvider";
import type { Identity } from "~/data/auth-provider";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "./ui/tooltip";
export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
const location = useLocation(); const location = useLocation();
const { data: identity } = useGetIdentity<Identity>(); const { data: identity } = useGetIdentity<Identity>();
const { mutate: logout } = useLogout(); const{ mutate: logout } = useLogout()
return ( return (
<PinningProvider> <PinningProvider>
<div className="h-full flex flex-row"> <div className="h-full flex flex-row">
@ -103,6 +93,7 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</header> </header>
<div className="flex-1 overflow-y-auto p-10"> <div className="flex-1 overflow-y-auto p-10">
<div className="flex items-center gap-x-4 justify-end"> <div className="flex items-center gap-x-4 justify-end">
<Button variant="ghost" className="rounded-full w-fit"> <Button variant="ghost" className="rounded-full w-fit">
@ -151,11 +142,7 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
<Button <Button
variant={"link"} variant={"link"}
className="flex flex-row gap-x-2 text-input-placeholder"> className="flex flex-row gap-x-2 text-input-placeholder">
<img <img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
className="h-5"
src={lumeColorLogoPng}
alt="Lume Logo"
/>
Connect with us Connect with us
</Button> </Button>
</Link> </Link>
@ -265,7 +252,6 @@ const UploadFileItem = ({
file: UppyFile; file: UppyFile;
onRemove: (id: string) => void; onRemove: (id: string) => void;
}) => { }) => {
const sizeInMb = bytestoMegabytes(file.size).toFixed(2);
return ( return (
<div className="flex flex-col w-full py-4 px-2 bg-primary-dark"> <div className="flex flex-col w-full py-4 px-2 bg-primary-dark">
<div className="flex text-primary-1 items-center justify-between"> <div className="flex text-primary-1 items-center justify-between">
@ -277,23 +263,12 @@ const UploadFileItem = ({
<PageIcon className="w-4 h-4" /> <PageIcon className="w-4 h-4" />
)} )}
</div> </div>
<TooltipProvider> <p className="w-full flex justify-between items-center">
<Tooltip delayDuration={500}> <span className="truncate text-ellipsis max-w-[30ch]">
<TooltipTrigger> {file.name}
<p className="w-full flex justify-between items-center"> </span>{" "}
<span className="truncate text-ellipsis max-w-[20ch]"> <span>({bytestoMegabytes(file.size).toFixed(2)} MB)</span>
{file.name} </p>
</span>{" "}
<span>({sizeInMb}MB)</span>
</p>
</TooltipTrigger>
<TooltipContent>
<p>
{file.name} ({sizeInMb}MB)
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
<Button <Button
size={"icon"} size={"icon"}

View File

@ -1,21 +1,14 @@
import Uppy, { debugLogger, type State, type UppyFile } 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, HashProgressEvent} from "@lumeweb/s5-js";
useState,
} from "react";
import DropTarget, { type DropTargetOptions } from "./uppy-dropzone";
import { useSdk } from "~/components/lib/sdk-context";
import UppyFileUpload from "~/components/lib/uppy-file-upload";
import { PROTOCOL_S5, type Sdk } from "@lumeweb/portal-sdk";
import type { S5Client, HashProgressEvent } from "@lumeweb/s5-js";
const LISTENING_EVENTS = [ const LISTENING_EVENTS = [
"upload", "upload",
@ -23,46 +16,46 @@ const LISTENING_EVENTS = [
"upload-error", "upload-error",
"file-added", "file-added",
"file-removed", "file-removed",
"files-added", "files-added"
] as const; ] as const
export function useUppy() { export function useUppy() {
const sdk = useSdk(); const sdk = useSdk()
const [uploadLimit, setUploadLimit] = useState<number>(0); const [uploadLimit, setUploadLimit] = useState<number>(0)
useEffect(() => { useEffect(() => {
async function getUploadLimit() { async function getUploadLimit() {
try { try {
const limit = await sdk.account!().uploadLimit(); const limit = await sdk.account!().uploadLimit();
setUploadLimit(limit); setUploadLimit(limit);
} catch (err) { } catch (err) {
console.log("Error occured while fetching upload limit", err); console.log('Error occured while fetching upload limit', err);
} }
} }
getUploadLimit(); getUploadLimit();
}, [sdk.account]); }, []);
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>()
const setRef = useCallback( const setRef = useCallback(
(element: HTMLElement | null) => _setTargetRef(element), (element: HTMLElement | null) => _setTargetRef(element),
[], []
); )
const [, setUppyState] = useState<State>(); const [, setUppyState] = useState<State>()
const [state, setState] = useState< const [state, setState] = useState<
"completed" | "idle" | "initializing" | "error" | "uploading" "completed" | "idle" | "initializing" | "error" | "uploading"
>("initializing"); >("initializing")
const [inputProps, setInputProps] = useState< const [inputProps, setInputProps] = useState<
| { | {
ref: typeof inputRef; ref: typeof inputRef
type: "file"; type: "file"
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void
} }
| object | object
>({}); >({})
const getRootProps = useMemo( const getRootProps = useMemo(
() => () => { () => () => {
return { return {
@ -70,110 +63,103 @@ export function useUppy() {
onClick: () => { onClick: () => {
if (inputRef.current) { if (inputRef.current) {
//@ts-expect-error -- dumb html //@ts-expect-error -- dumb html
inputRef.current.value = null; inputRef.current.value = null
inputRef.current.click(); inputRef.current.click()
console.log("clicked", { input: inputRef.current }); console.log("clicked", { input: inputRef.current })
} }
}, },
role: "presentation", role: "presentation"
}; }
}, },
[setRef], [setRef]
); )
const removeFile = useCallback( const removeFile = useCallback(
(id: string) => { (id: string) => {
uppyInstance.current?.removeFile(id); uppyInstance.current?.removeFile(id)
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[targetRef, uppyInstance], [targetRef, uppyInstance]
); )
const cancelAll = useCallback( const cancelAll = useCallback(
() => uppyInstance.current?.cancelAll({ reason: "user" }), () => uppyInstance.current?.cancelAll({ reason: "user" }),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[targetRef, uppyInstance], [targetRef, uppyInstance]
); )
useEffect(() => { useEffect(() => {
if (!targetRef) return; if (!targetRef) return
const tusPreprocessor = async (fileIDs: string[]) => { const tusPreprocessor = async (fileIDs: string[]) => {
for (const fileID of fileIDs) { for(const fileID of fileIDs) {
const file = uppyInstance.current?.getFile(fileID) as UppyFile; const file = uppyInstance.current?.getFile(fileID) as UppyFile
// @ts-ignore // @ts-ignore
if (file.uploader === "tus") { if (file.uploader === "tus") {
const hashProgressCb = (event: HashProgressEvent) => { const hashProgressCb = (event: HashProgressEvent) => {
uppyInstance.current?.emit("preprocess-progress", file, { uppyInstance.current?.emit("preprocess-progress", file, {
uploadStarted: false, uploadStarted: false,
bytesUploaded: 0, bytesUploaded: 0,
preprocess: { preprocess: {
mode: "determinate", mode: "determinate",
message: "Hashing file...", message: "Hashing file...",
value: Math.round((event.total / event.total) * 100), value: Math.round((event.total / event.total) * 100)
}, }
}); })
}; }
const options = await sdk.protocols!() const options = await sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File, {}, {onHashProgress: hashProgressCb})
.get<S5Client>(PROTOCOL_S5) uppyInstance.current?.setFileState(fileID, {
.getSdk() tus: options,
.getTusOptions( meta: {
file.data as File, ...options.metadata,
{}, ...file.meta,
{ onHashProgress: hashProgressCb }, }
); })
uppyInstance.current?.setFileState(fileID, { }
tus: options,
meta: {
...options.metadata,
...file.meta,
},
});
} }
} }
};
const uppy = new Uppy({ const uppy = new Uppy({
logger: debugLogger, logger: debugLogger,
onBeforeUpload: (files) => { onBeforeUpload: (files) => {
for (const file of Object.entries(files)) { for (const file of Object.entries(files)) {
// @ts-ignore // @ts-ignore
file[1].uploader = file[1].size > uploadLimit ? "tus" : "file"; file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
} }
return true; return true;
}, },
}).use(DropTarget, { }).use(DropTarget, {
target: targetRef, target: targetRef
} as DropTargetOptions); } as DropTargetOptions)
uppyInstance.current = uppy; uppyInstance.current = uppy
setInputProps({ setInputProps({
ref: inputRef, ref: inputRef,
type: "file", type: "file",
onChange: (event) => { onChange: (event) => {
const files = toArray(event.target.files); const files = toArray(event.target.files)
if (files.length > 0) { if (files.length > 0) {
uppyInstance.current?.log("[DragDrop] Files selected through input"); uppyInstance.current?.log("[DragDrop] Files selected through input")
uppyInstance.current?.addFiles(files); uppyInstance.current?.addFiles(files)
} }
uppy.iteratePlugins((plugin) => { uppy.iteratePlugins((plugin) => {
uppy.removePlugin(plugin); uppy.removePlugin(plugin);
}); });
uppy.use(UppyFileUpload, { sdk: sdk as Sdk }); uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
let useTus = false; let useTus = false;
uppyInstance.current?.getFiles().forEach((file) => { uppyInstance.current?.getFiles().forEach((file) => {
if (file.size > uploadLimit) { if (file.size > uploadLimit) {
useTus = true; useTus = true;
}
})
if (useTus) {
uppy.use(Tus, { limit: 6, parallelUploads: 10 })
uppy.addPreProcessor(tusPreprocessor)
} }
});
if (useTus) {
uppy.use(Tus, { limit: 6, parallelUploads: 10 });
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
@ -183,52 +169,54 @@ export function useUppy() {
// Chrome will not trigger change if we drop the same file twice (Issue #768). // Chrome will not trigger change if we drop the same file twice (Issue #768).
// @ts-expect-error TS freaks out, but this is fine // @ts-expect-error TS freaks out, but this is fine
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
event.target.value = null; event.target.value = null
}, }
}); })
uppy.on("complete", (result) => { uppy.on("complete", (result) => {
if (result.failed.length === 0) { if (result.failed.length === 0) {
console.log("Upload successful üòÄ"); console.log("Upload successful üòÄ")
setState("completed"); setState("completed")
} else { } else {
console.warn("Upload failed üòû"); console.warn("Upload failed üòû")
setState("error"); setState("error")
} }
console.log("successful files:", result.successful); console.log("successful files:", result.successful)
console.log("failed files:", result.failed); console.log("failed files:", result.failed)
}); })
const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => { const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => {
switch (event) { switch (event) {
case "upload": case "upload":
setState("uploading"); setState("uploading")
break; break
case "upload-error": case "upload-error":
setState("error"); setState("error")
break; break
default: default:
break; break
} }
setUppyState(uppy.getState()); setUppyState(uppy.getState())
}; }
for (const event of LISTENING_EVENTS) { for (const event of LISTENING_EVENTS) {
uppy.on(event, function cb() { uppy.on(event, function cb() {
setStateCb(event); setStateCb(event)
}); })
} }
setState("idle"); setState("idle")
}, [targetRef, uploadLimit]); }, [targetRef, uploadLimit])
useEffect(() => { useEffect(() => {
return () => { return () => {
uppyInstance.current?.cancelAll({ reason: "unmount" }); uppyInstance.current?.cancelAll({ reason: "unmount" })
uppyInstance.current?.logout(); uppyInstance.current?.logout()
uppyInstance.current?.close(); uppyInstance.current?.close()
uppyInstance.current = undefined; uppyInstance.current = undefined
}; }
}, []); }, [])
return { return {
getFiles: () => uppyInstance.current?.getFiles() ?? [], getFiles: () => uppyInstance.current?.getFiles() ?? [],
error: uppyInstance.current?.getState, error: uppyInstance.current?.getState,
@ -239,6 +227,6 @@ export function useUppy() {
getInputProps: () => inputProps, getInputProps: () => inputProps,
getRootProps, getRootProps,
removeFile, removeFile,
cancelAll, cancelAll
}; }
} }

View File

@ -60,7 +60,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action <ToastPrimitives.Action
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-background 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", "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 className
)} )}
{...props} {...props}

View File

@ -14,7 +14,7 @@ export function Toaster() {
return ( return (
<ToastProvider> <ToastProvider>
{toasts.map(({ id, title, description, action, cancelMutation, ...props }) => { {toasts.map(function ({ id, title, description, action, cancelMutation, ...props }) {
const undoButton = cancelMutation ? <ToastAction altText="Undo" onClick={cancelMutation}>Undo</ToastAction> : undefined const undoButton = cancelMutation ? <ToastAction altText="Undo" onClick={cancelMutation}>Undo</ToastAction> : undefined
return ( return (
<Toast key={id} {...props}> <Toast key={id} {...props}>

View File

@ -1,28 +0,0 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "~/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -50,7 +50,7 @@ export const UsageChart = ({ label, dataset }: UsageChartProps) => {
<AnimatedAxis <AnimatedAxis
orientation="bottom" orientation="bottom"
hideTicks hideTicks
tickTransform="translate(50 0)" tickTransform="translateX(50%)"
tickLabelProps={{ className: "text-sm" }} tickLabelProps={{ className: "text-sm" }}
/> />
<AnimatedAxis <AnimatedAxis

View File

@ -30,7 +30,7 @@ export const accountProvider: SdkProvider = {
if (ret) { if (ret) {
if (ret instanceof Error) { if (ret instanceof Error) {
return Promise.reject(ret satisfies HttpError) return Promise.reject(ret)
} }
} else { } else {
return Promise.reject(); return Promise.reject();

View File

@ -1,17 +1,15 @@
import type {AuthProvider, HttpError, UpdatePasswordFormTypes} from "@refinedev/core" import type {AuthProvider, UpdatePasswordFormTypes} from "@refinedev/core"
import type { import type {
AuthActionResponse, AuthActionResponse,
CheckResponse, CheckResponse,
IdentityResponse, IdentityResponse,
OnErrorResponse, OnErrorResponse
SuccessNotificationResponse
// @ts-ignore // @ts-ignore
} from "@refinedev/core/dist/interfaces/bindings/auth" } from "@refinedev/core/dist/interfaces/bindings/auth"
import {Sdk, AccountError} from "@lumeweb/portal-sdk"; import {Sdk} from "@lumeweb/portal-sdk";
import type {AccountInfoResponse} from "@lumeweb/portal-sdk"; import type {AccountInfoResponse} from "@lumeweb/portal-sdk";
export type AuthFormRequest = { export type AuthFormRequest = {
email: string; email: string;
password: string; password: string;
@ -33,7 +31,7 @@ export type Identity = {
email: string; email: string;
} }
export interface UpdatePasswordFormRequest extends UpdatePasswordFormTypes { export interface UpdatePasswordFormRequest extends UpdatePasswordFormTypes{
currentPassword: string; currentPassword: string;
} }
@ -45,54 +43,6 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
} }
}; };
type ResponseResult = {
ret: boolean | Error;
successNotification?: SuccessNotificationResponse;
redirectToSuccess?: string;
redirectToError?: string;
successCb?: () => void;
}
interface CheckResponseResult extends ResponseResult {
authenticated?: boolean;
}
const handleResponse = (result: ResponseResult): AuthActionResponse => {
if (result.ret) {
if (result.ret instanceof AccountError) {
return {
success: false,
error: result.ret satisfies HttpError,
redirectTo: result.redirectToError
}
}
result.successCb?.();
return {
success: true,
successNotification: result.successNotification,
redirectTo: result.redirectToSuccess,
}
}
return {
success: false,
redirectTo: result.redirectToError
}
}
const handleCheckResponse = (result: CheckResponseResult): CheckResponse => {
const response = handleResponse(result);
const success = response.success;
delete response.success;
return {
...response,
authenticated: success
}
}
return { return {
async login(params: AuthFormRequest): Promise<AuthActionResponse> { async login(params: AuthFormRequest): Promise<AuthActionResponse> {
const ret = await sdk.account().login({ const ret = await sdk.account().login({
@ -100,30 +50,39 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
password: params.password, password: params.password,
}); });
return handleResponse({ let redirectTo: string | undefined;
ret, redirectToSuccess: "/dashboard", redirectToError: "/login", successCb: () => {
sdk.setAuthToken(sdk.account().jwtToken);
}, successNotification: {
message: "Login Successful",
description: "You have successfully logged in."
if (ret) {
redirectTo = params.redirectTo;
if (!redirectTo) {
redirectTo = ret ? "/dashboard" : "/login";
} }
}); sdk.setAuthToken(sdk.account().jwtToken);
}
return {
success: ret,
redirectTo,
};
}, },
async logout(params: any): Promise<AuthActionResponse> { async logout(params: any): Promise<AuthActionResponse> {
let ret = await sdk.account().logout(); let ret = await sdk.account().logout();
return handleResponse({ret, redirectToSuccess: "/login"}); return {success: ret, redirectTo: "/login"};
}, },
async check(params?: any): Promise<CheckResponse> { async check(params?: any): Promise<CheckResponse> {
const ret = await sdk.account().ping(); const ret = await sdk.account().ping();
return handleCheckResponse({ret, redirectToError: "/login", successCb: maybeSetupAuth}); if (ret) {
maybeSetupAuth();
}
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
}, },
async onError(error: any): Promise<OnErrorResponse> { async onError(error: any): Promise<OnErrorResponse> {
return {}; return {logout: true};
}, },
async register(params: RegisterFormRequest): Promise<AuthActionResponse> { async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
@ -133,12 +92,7 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
first_name: params.firstName, first_name: params.firstName,
last_name: params.lastName, last_name: params.lastName,
}); });
return handleResponse({ return {success: ret, redirectTo: ret ? "/dashboard" : undefined};
ret, redirectToSuccess: "/login", successNotification: {
message: "Registration Successful",
description: "You have successfully registered. Please check your email to verify your account.",
}
});
}, },
async forgotPassword(params: any): Promise<AuthActionResponse> { async forgotPassword(params: any): Promise<AuthActionResponse> {
@ -149,12 +103,22 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
maybeSetupAuth(); maybeSetupAuth();
const ret = await sdk.account().updatePassword(params.currentPassword, params.password as string); const ret = await sdk.account().updatePassword(params.currentPassword, params.password as string);
return handleResponse({ if (ret) {
ret, successNotification: { if (ret instanceof Error) {
message: "Password Updated", return {
description: "Your password has been updated successfully.", success: false,
error: ret
}
} }
});
return {
success: true
}
} else {
return {
success: false
}
}
}, },
async getPermissions(params?: Record<string, any>): Promise<AuthActionResponse> { async getPermissions(params?: Record<string, any>): Promise<AuthActionResponse> {

View File

@ -1,134 +1,52 @@
import type {SdkProvider} from "~/data/sdk-provider.js"; import type { DataProvider } from "@refinedev/core";
import type {S5Client} from "@lumeweb/s5-js"; import { SdkProvider } from "~/data/sdk-provider.js";
import {PROTOCOL_S5} from "@lumeweb/portal-sdk";
import {Multihash} from "@lumeweb/libs5/lib/multihash.js";
import type {AxiosProgressEvent} from "axios";
import {CID, CID_TYPES, METADATA_TYPES, metadataMagicByte, Unpacker} from "@lumeweb/libs5";
async function getIsManifest(s5: S5Client, hash: string): Promise<boolean | number> { export const fileProvider = {
let type: number | null; getList: () => {
try { console.log("Not implemented");
const abort = new AbortController(); return {
const resp = s5.downloadData(hash, { data: [
onDownloadProgress: (progressEvent: AxiosProgressEvent) => { {
if (progressEvent.loaded >= 10) { name: "whirly-final-draft.psd",
abort.abort(); cid: "0xB45165ED3CD437B",
} size: "1.89 MB",
}, createdOn: " 03/02/2024 at 13:29 PM",
httpConfig: { },
signal: abort.signal, {
}, name: "whirly-final-draft.psd",
}); cid: "0xB45165ED3CD437B",
size: "1.89 MB",
const data = await resp; createdOn: " 03/02/2024 at 13:29 PM",
const unpacker = Unpacker.fromPacked(Buffer.from(data)); },
try { ],
const magic = unpacker.unpackInt(); total: 2
if (magic !== metadataMagicByte) {
return false;
}
type = unpacker.unpackInt();
if (!type || !Object.values(METADATA_TYPES).includes(type)) {
return false;
}
} catch (e) {
return false;
}
} catch (e) {
return false;
} }
},
switch (type) { getOne: () => {
case METADATA_TYPES.DIRECTORY: console.log("Not implemented");
return CID_TYPES.DIRECTORY; return Promise.resolve({
case METADATA_TYPES.WEBAPP: data: {
return CID_TYPES.METADATA_WEBAPP; id: 1
case METADATA_TYPES.MEDIA: },
return CID_TYPES.METADATA_MEDIA; });
case METADATA_TYPES.USER_IDENTITY: },
return CID_TYPES.USER_IDENTITY; update: () => {
} console.log("Not implemented");
return Promise.resolve({
return 0; data: {},
} });
},
export interface FileItem { create: () => {
cid: string; console.log("Not implemented");
type: string; return Promise.resolve({
size: number; data: {},
mimeType: string; });
pinned: string; },
} deleteOne: () => {
console.log("Not implemented");
export const fileProvider: SdkProvider = { return Promise.resolve({
sdk: undefined, data: {},
async getList() { });
const items: FileItem[] = []; },
try { getApiUrl: () => "",
const s5 = fileProvider.sdk?.protocols().get<S5Client>(PROTOCOL_S5)!.getSdk()!;
const pinList = await s5.accountPins();
for (const pin of pinList!.pins) {
const manifest = await getIsManifest(s5, pin.hash) as number;
if (manifest) {
const mHash = Multihash.fromBase64Url(pin.hash);
items.push({
cid: new CID(manifest, mHash, pin.size).toString(),
type: "manifest",
mimeType: "application/octet-stream",
size: pin.size,
pinned: pin.pinned_at,
});
} else {
items.push({
cid: new CID(CID_TYPES.RAW, Multihash.fromBase64Url(pin.hash), pin.size).toString(),
type: "raw",
mimeType: pin.mime_type,
size: pin.size,
pinned: pin.pinned_at,
});
}
}
} catch (e) {
return Promise.reject(e);
}
return {
data: items,
total: items.length,
};
},
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() {
return "";
},
} satisfies SdkProvider; } satisfies SdkProvider;

View File

@ -1,62 +1,51 @@
import { useNotification } from "@refinedev/core"; import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useContext } from "react";
import { useCallback, useContext } from "react";
import { PinningProcess } from "~/data/pinning"; import { PinningProcess } from "~/data/pinning";
import { PinningContext } from "~/providers/PinningProvider"; import { PinningContext } from "~/providers/PinningProvider";
// TODO: Adapt to real API
export const usePinning = () => { export const usePinning = () => {
const queryClient = useQueryClient();
const context = useContext(PinningContext); const context = useContext(PinningContext);
const { open } = useNotification();
const { mutate: pinMutation } = useMutation({ const { mutate } = useMutation({
mutationKey: ["pin-mutation"], mutationKey: ["pin-progress"],
mutationFn: async (variables: { cid: string }) => { mutationFn: async (variables: { cid: string, type: "pin" | "unpin" }) => {
const { cid } = variables; const { cid, type } = variables;
const response = await PinningProcess.pin(cid); switch (type) {
case "pin": {
const response = await PinningProcess.pin(cid);
if (!response.success) { if (!response.success) {
open?.({ open?.({
type: "error", type: "destructive",
message: `Error pinning ${cid}`, message: "Erorr pinning " + cid,
description: response.message, description: response.message,
}); });
}
break;
}
case "unpin": {
const response = await PinningProcess.unpin(cid);
if (!response.success) {
open?.({
type: "destructive",
message: "Erorr removing " + cid,
description: response.message,
});
}
break;
}
} }
queryClient.invalidateQueries({ queryKey: ["pin-progress"] }); context.queryClient.invalidateQueries({ queryKey: ["pin-progress"] })
}, }
}); });
const { mutate: unpinMutation } = useMutation({
mutationKey: ["unpin-mutation"],
mutationFn: async (variables: { cid: string }) => {
const { cid } = variables;
const response = await PinningProcess.unpin(cid);
if (!response.success) {
open?.({
type: "error",
message: `Error removing ${cid}`,
description: response.message,
});
}
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
},
});
const bulkPin = useCallback(
(cids: string[]) => {
for (const cid of cids) {
pinMutation({ cid });
}
},
[pinMutation],
);
return { return {
...context.query, ...context.query,
pin: pinMutation, mutate
unpin: unpinMutation,
bulkPin,
}; };
}; };

View File

@ -1,11 +1,11 @@
import { import {
type QueryClient, QueryClient,
type UseQueryResult, UseQueryResult,
useQuery, useQuery,
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { PinningProcess, type PinningStatus } from "~/data/pinning"; import { PinningProcess, PinningStatus } from "~/data/pinning";
export interface IPinningData { export interface IPinningData {
cid: string; cid: string;

View File

@ -1,26 +1,19 @@
import { import {Links, Meta, Outlet, Scripts, ScrollRestoration,} from "@remix-run/react";
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import stylesheet from "./tailwind.css?url"; import stylesheet from "./tailwind.css?url";
import type { LinksFunction } from "@remix-run/node"; 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 routerProvider from "@refinedev/remix-router"; import routerProvider from "@refinedev/remix-router";
import { notificationProvider } from "~/data/notification-provider"; import { notificationProvider } from "~/data/notification-provider";
import { SdkContextProvider, useSdk } from "~/components/lib/sdk-context"; import {SdkContextProvider} from "~/components/lib/sdk-context";
import { Toaster } from "~/components/ui/toaster"; 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";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useMemo } from "react";
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet }, { rel: "stylesheet", href: stylesheet },
@ -28,55 +21,50 @@ export const links: LinksFunction = () => [
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({children}: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<Meta /> <Meta/>
<Links /> <Links/>
</head> </head>
<body className="overflow-hidden"> <body>
{children} {children}
<Toaster /> <Toaster />
<ScrollRestoration /> <ScrollRestoration/>
<Scripts /> <Scripts/>
</body> </body>
</html> </html>
); );
} }
function App() { export default function App() {
const sdk = useSdk(); console.log(import.meta.env.VITE_PORTAL_URL);
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]); const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL)
return ( const providers = getProviders(sdk);
<QueryClientProvider client={queryClient}> return (
<Refine <QueryClientProvider client={queryClient}>
authProvider={providers.auth} <Refine
routerProvider={routerProvider} authProvider={providers.auth}
notificationProvider={notificationProvider} routerProvider={routerProvider}
dataProvider={{ notificationProvider={notificationProvider}
default: providers.default, dataProvider={{
files: providers.files, default: providers.default,
}} files: providers.files
resources={resources} }}
options={{ disableTelemetry: true }}> resources={resources}
<Outlet /> options={{disableTelemetry: true}}
</Refine> >
</QueryClientProvider> <SdkContextProvider sdk={sdk}>
); <Outlet/>
} </SdkContextProvider>
</Refine>
export default function Root() { </QueryClientProvider>
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL); );
return (
<SdkContextProvider sdk={sdk}>
<App />
</SdkContextProvider>
);
} }
export function HydrateFallback() { export function HydrateFallback() {
return <p>Loading...</p>; return <p>Loading...</p>;
} }

View File

@ -1,12 +1,19 @@
import {Authenticated} from "@refinedev/core"; import { useGo, useIsAuthenticated } from "@refinedev/core";
import {Navigate} from "@remix-run/react";
export default function Index() { export default function Index() {
return ( const { isLoading, data } = useIsAuthenticated();
<Authenticated v3LegacyAuthProviderCompatible key={"index"} loading={
<>Checking Login Status</> const go = useGo();
}>
<Navigate to="/dashboard" replace/> if (isLoading) {
</Authenticated> return <>Checking Login Status</>;
) }
if (data?.authenticated) {
go({ to: "/dashboard", type: "replace" });
} else {
go({ to: "/login", type: "replace" });
}
return <>Redirecting</>;
} }

View File

@ -3,11 +3,11 @@ import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { DialogClose } from "@radix-ui/react-dialog"; import { DialogClose } from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons"; import { Cross2Icon } from "@radix-ui/react-icons";
import { import {
Authenticated, Authenticated,
type BaseKey, BaseKey,
useGetIdentity, useGetIdentity,
useUpdate, useUpdate,
useUpdatePassword, useUpdatePassword,
} from "@refinedev/core"; } from "@refinedev/core";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { z } from "zod"; import { z } from "zod";
@ -41,7 +41,7 @@ import { Input } from "~/components/ui/input";
import { UsageCard } from "~/components/usage-card"; import { UsageCard } from "~/components/usage-card";
import QRImg from "~/images/QR.png"; import QRImg from "~/images/QR.png";
import type { UpdatePasswordFormRequest } from "~/data/auth-provider"; import {UpdatePasswordFormRequest} from "~/data/auth-provider.js";
export default function MyAccount() { export default function MyAccount() {
const { data: identity } = useGetIdentity<{ email: string }>(); const { data: identity } = useGetIdentity<{ email: string }>();
@ -54,175 +54,167 @@ export default function MyAccount() {
}); });
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>
<Dialog <Dialog
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setModal({ setModal({
changeEmail: false, changeEmail: false,
changePassword: false, changePassword: false,
setupTwoFactor: false, setupTwoFactor: false,
changeAvatar: false, changeAvatar: false,
}); });
} }
}}> }}>
<UsageCard <UsageCard
label="Usage" label="Usage"
currentUsage={2} currentUsage={2}
monthlyUsage={10} monthlyUsage={10}
icon={<CloudIcon className="text-ring" />} icon={<CloudIcon className="text-ring" />}
button={ button={
<Button variant="accent" className="gap-x-2 h-12"> <Button variant="accent" className="gap-x-2 h-12">
<AddIcon />
Upgrade to Premium
</Button>
}
/>
<h2 className="font-bold my-8">Account Management</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard>
<ManagementCardAvatar
button={
<DialogTrigger className="absolute bottom-0 right-0 z-50">
<Button
onClick={() => setModal({ ...openModal, changeAvatar: true })}
variant="outline"
className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
<EditIcon />
</Button>
</DialogTrigger>
}
/>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger>
<Button
className="h-12 gap-x-2"
onClick={() => setModal({ ...openModal, changeEmail: true })}>
<AddIcon />
Change Email Address
</Button>
</DialogTrigger>
</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>
<h2 className="font-bold my-8">Account Management</h2> </div>
<div className="grid grid-cols-3 gap-x-8"> <h2 className="font-bold my-8">Security</h2>
<ManagementCard> <div className="grid grid-cols-3 gap-x-8">
<ManagementCardAvatar <ManagementCard>
button={ <ManagementCardTitle>Password</ManagementCardTitle>
<DialogTrigger <ManagementCardContent>
asChild <PasswordDots className="mt-6" />
className="absolute bottom-0 right-0 z-50"> </ManagementCardContent>
<Button <ManagementCardFooter>
onClick={() => <DialogTrigger>
setModal({ ...openModal, changeAvatar: true }) <Button
} className="h-12 gap-x-2"
variant="outline" onClick={() =>
className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2"> setModal({ ...openModal, changePassword: true })
<EditIcon /> }>
</Button>
</DialogTrigger>
}
/>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>Email Address</ManagementCardTitle>
<ManagementCardContent className="text-ring font-semibold">
{identity?.email}
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, changeEmail: true })
}>
<AddIcon />
Change Email Address
</Button>
</DialogTrigger>
</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 Change Password
</Button> </Button>
</ManagementCardFooter> </DialogTrigger>
</ManagementCard> </ManagementCardFooter>
</div> </ManagementCard>
<h2 className="font-bold my-8">Security</h2> <ManagementCard>
<div className="grid grid-cols-3 gap-x-8"> <ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle>
<ManagementCard> <ManagementCardContent>
<ManagementCardTitle>Password</ManagementCardTitle> Improve security by enabling 2FA.
<ManagementCardContent> </ManagementCardContent>
<PasswordDots className="mt-6" /> <ManagementCardFooter>
</ManagementCardContent> <DialogTrigger>
<ManagementCardFooter> <Button
<DialogTrigger asChild> className="h-12 gap-x-2"
<Button onClick={() =>
className="h-12 gap-x-2" setModal({ ...openModal, setupTwoFactor: true })
onClick={() => }>
setModal({ ...openModal, changePassword: true })
}>
<AddIcon />
Change Password
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
<ManagementCard>
<ManagementCardTitle>
Two-Factor Authentication
</ManagementCardTitle>
<ManagementCardContent>
Improve security by enabling 2FA.
</ManagementCardContent>
<ManagementCardFooter>
<DialogTrigger asChild>
<Button
className="h-12 gap-x-2"
onClick={() =>
setModal({ ...openModal, setupTwoFactor: true })
}>
<AddIcon />
Enable Two-Factor Authorization
</Button>
</DialogTrigger>
</ManagementCardFooter>
</ManagementCard>
</div>
<h2 className="font-bold my-8">More</h2>
<div className="grid grid-cols-3 gap-x-8">
<ManagementCard variant="accent">
<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 /> <AddIcon />
Send Invitation Enable Two-Factor Authorization
</Button> </Button>
</ManagementCardFooter> </DialogTrigger>
</ManagementCard> </ManagementCardFooter>
<ManagementCard> </ManagementCard>
<ManagementCardTitle>Read our Resources</ManagementCardTitle> </div>
<ManagementCardContent> <h2 className="font-bold my-8">More</h2>
Navigate helpful articles or get assistance. <div className="grid grid-cols-3 gap-x-8">
</ManagementCardContent> <ManagementCard variant="accent">
<ManagementCardFooter> <ManagementCardTitle>Invite a Friend</ManagementCardTitle>
<Button className="h-12 gap-x-2"> <ManagementCardContent>
<AddIcon /> Get 1 GB per friend invited for free (max 5 GB).
Open Support Centre </ManagementCardContent>
</Button> <ManagementCardFooter>
</ManagementCardFooter> <Button variant="accent" className="h-12 gap-x-2">
</ManagementCard> <AddIcon />
<ManagementCard> Send Invitation
<ManagementCardTitle>Delete Account</ManagementCardTitle> </Button>
<ManagementCardContent> </ManagementCardFooter>
Once initiated, this action cannot be undone. </ManagementCard>
</ManagementCardContent> <ManagementCard>
<ManagementCardFooter> <ManagementCardTitle>Read our Resources</ManagementCardTitle>
<Button className="h-12 gap-x-2" variant="destructive"> <ManagementCardContent>
<AddIcon /> Navigate helpful articles or get assistance.
Delete my Account </ManagementCardContent>
</Button> <ManagementCardFooter>
</ManagementCardFooter> <Button className="h-12 gap-x-2">
</ManagementCard> <AddIcon />
</div> Open Support Centre
<DialogContent> </Button>
{openModal.changeAvatar && <ChangeAvatarForm />} </ManagementCardFooter>
{openModal.changeEmail && ( </ManagementCard>
<ChangeEmailForm currentValue={identity?.email || ""} /> <ManagementCard>
)} <ManagementCardTitle>Delete Account</ManagementCardTitle>
{openModal.changePassword && <ChangePasswordForm />} <ManagementCardContent>
{openModal.setupTwoFactor && <SetupTwoFactorDialog />} Once initiated, this action cannot be undone.
</DialogContent> </ManagementCardContent>
</Dialog> <ManagementCardFooter>
</GeneralLayout> <Button className="h-12 gap-x-2" variant="destructive">
</Authenticated> <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>
</Authenticated>
); );
} }
@ -260,7 +252,7 @@ const ChangeEmailForm = ({ currentValue }: { currentValue: string }) => {
console.log(identity); console.log(identity);
updateEmail({ updateEmail({
resource: "account", resource: "account",
id: "", id: "",
values: { values: {
email: data.email.toString(), email: data.email.toString(),
password: data.password.toString(), password: data.password.toString(),
@ -321,8 +313,7 @@ const ChangePasswordSchema = z
}); });
const ChangePasswordForm = () => { const ChangePasswordForm = () => {
const { mutate: updatePassword } = const { mutate: updatePassword } = useUpdatePassword<{ password: string }>();
useUpdatePassword<UpdatePasswordFormRequest>();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "login", id: "login",
constraint: getZodConstraint(ChangePasswordSchema), constraint: getZodConstraint(ChangePasswordSchema),
@ -336,8 +327,7 @@ 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(),
}); });
}, },
}); });
@ -479,7 +469,7 @@ const ChangeAvatarForm = () => {
{hasStarted ? ( {hasStarted ? (
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1"> <div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" /> <CloudCheckIcon className="w-32 h-32" />
{isCompleted ? "Upload completed" : "0% completed"} {isCompleted ? "Upload completed" : `0% completed`}
</div> </div>
) : null} ) : null}

View File

@ -1,5 +1,5 @@
import { TrashIcon } from "@radix-ui/react-icons"; import { DrawingPinIcon, TrashIcon } from "@radix-ui/react-icons";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef, Row } 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 { import {
@ -7,11 +7,12 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import { usePinning } from "~/hooks/usePinning";
import { cn } from "~/utils"; import { cn } from "~/utils";
import type { FileItem } from "~/data/file-provider";
// 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.
@ -22,7 +23,47 @@ export type File = {
createdOn: string; createdOn: string;
}; };
export const columns: ColumnDef<FileItem>[] = [ const CreatedOnCell = ({ row }: { row: Row<File> }) => {
// const { open } = useNotification();
const { mutate } = usePinning();
return (
<div className="flex items-center justify-between">
{row.getValue("createdOn")}
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
"hidden group-hover:block data-[state=open]:block",
row.getIsSelected() && "block",
)}>
<MoreIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => {
console.log(`Adding ${row.getValue("cid")} for pinning...`);
mutate({
cid: row.getValue("cid"),
type: "pin"
});
}}>
<DrawingPinIcon className="mr-2" />
Pin CID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<TrashIcon className="mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export const columns: ColumnDef<File>[] = [
{ {
id: "select", id: "select",
size: 20, size: 20,
@ -65,34 +106,9 @@ export const columns: ColumnDef<FileItem>[] = [
header: "Size", header: "Size",
}, },
{ {
accessorKey: "pinned", accessorKey: "createdOn",
header: "Pinned On", size: 200,
cell: ({ row }) => new Date(row.getValue("pinned")).toLocaleString(), header: "Created On",
}, cell: ({ row }) => <CreatedOnCell row={row} />,
{
accessorKey: "actions",
header: () => null,
size: 20,
cell: ({ row }) => (
<div className="flex w-5 items-center justify-between">
<DropdownMenu>
<DropdownMenuTrigger
className={cn(
"hidden group-hover:block data-[state=open]:block",
row.getIsSelected() && "block",
)}>
<MoreIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<TrashIcon className="mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
}, },
]; ];

View File

@ -5,126 +5,59 @@ import { columns } from "./columns";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { AddIcon } from "~/components/icons"; import { AddIcon } from "~/components/icons";
import { Authenticated } from "@refinedev/core";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { Field } from "~/components/forms";
import { z } from "zod";
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { usePinning } from "~/hooks/usePinning";
export default function FileManager() { export default function FileManager() {
return ( return (
<Authenticated key="dashboard" v3LegacyAuthProviderCompatible> <GeneralLayout>
<GeneralLayout> <h1 className="font-bold mb-4 text-lg">File Manager</h1>
<Dialog> <FileCardList>
<h1 className="font-bold mb-4 text-lg">File Manager</h1> <FileCard
<FileCardList> fileName="Backups"
<FileCard size="33 files"
fileName="Backups" type={FileTypes.Folder}
size="33 files" createdAt="2 days ago"
type={FileTypes.Folder} />
createdAt="2 days ago" <FileCard
/> fileName="Backups"
<FileCard size="33 files"
fileName="Backups" type={FileTypes.Folder}
size="33 files" createdAt="2 days ago"
type={FileTypes.Folder} />
createdAt="2 days ago" <FileCard
/> fileName="Backups"
<FileCard size="33 files"
fileName="Backups" type={FileTypes.Folder}
size="33 files" createdAt="2 days ago"
type={FileTypes.Folder} />
createdAt="2 days ago" <FileCard
/> fileName="Backups"
<FileCard size="33 files"
fileName="Backups" type={FileTypes.Folder}
size="33 files" createdAt="2 days ago"
type={FileTypes.Folder} />
createdAt="2 days ago" </FileCardList>
/> <h2 className="font-bold text-l mt-8">Files</h2>
</FileCardList> <div className="flex items-center space-x-4 my-6 w-full">
<h2 className="font-bold text-l mt-8">Files</h2> <Input
<div className="flex items-center space-x-4 my-6 w-full"> fullWidth
<Input leftIcon={<AddIcon />}
fullWidth placeholder="Search files by name or CID"
leftIcon={<AddIcon />} className="border-ring font-medium w-full grow h-12 flex-1 bg-primary-2/10"
placeholder="Search files by name or CID" />
className="border-ring font-medium w-full grow h-12 flex-1 bg-primary-2/10" <Button className="h-12 gap-x-2">
/> <AddIcon />
<Button className="h-12 gap-x-2"> Select All
<AddIcon /> </Button>
Select All <Button className="h-12 gap-x-2">
</Button> <AddIcon />
<DialogTrigger asChild> New Folder
<Button className="h-12 gap-x-2"> </Button>
<AddIcon /> </div>
Pin Content <DataTable
</Button> columns={columns}
</DialogTrigger> resource="file"
</div> dataProviderName="files"
<DataTable />
columns={columns} </GeneralLayout>
resource="file"
dataProviderName="files"
/>
<DialogContent>
<PinFilesForm />
</DialogContent>
</Dialog>
</GeneralLayout>
</Authenticated>
); );
} }
const PinFilesSchema = z.object({
cids: z.string().transform((value) => value.split(",")),
});
const PinFilesForm = () => {
const { bulkPin } = usePinning();
const [form, fields] = useForm({
id: "pin-files",
constraint: getZodConstraint(PinFilesSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: PinFilesSchema });
},
shouldValidate: "onSubmit",
onSubmit(e, { submission }) {
if (submission?.status === "success") {
const value = submission.value;
bulkPin(value.cids);
}
},
});
return (
<>
<DialogHeader>
<DialogTitle>Pin Content</DialogTitle>
</DialogHeader>
<form {...getFormProps(form)} className="w-full flex flex-col gap-y-4">
<Field
inputProps={{
name: fields.cids.name,
placeholder: "Comma separated CIDs",
}}
labelProps={{ htmlFor: "cids", children: "Content to Pin" }}
errors={fields.cids.errors}
/>
<Button type="submit" className="w-full">
Pin Content
</Button>
</form>
</>
);
};

View File

@ -1,69 +0,0 @@
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { useGo, useIsAuthenticated, useParsed } from "@refinedev/core";
import { Link } from "@remix-run/react";
import { useEffect } from "react";
import { z } from "zod";
import { Field } from "~/components/forms";
import { Button } from "~/components/ui/button";
import type { LoginParams } from "./login";
const OtpSchema = z.object({
otp: z.string().length(6, { message: "OTP must be 6 characters" }),
});
export default function OtpForm() {
const { isLoading: isAuthLoading, data: authData } = useIsAuthenticated();
const go = useGo();
const parsed = useParsed<LoginParams>();
// TODO: Add support for resending the OTP
const [form, fields] = useForm({
id: "otp",
constraint: getZodConstraint(OtpSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: OtpSchema });
},
shouldValidate: "onSubmit",
});
const valid = true;
const to = parsed.params?.to ?? "/dashboard";
useEffect(() => {
if (!isAuthLoading) {
if (authData?.authenticated && valid) {
go({ to, type: "push" });
}
}
}, [isAuthLoading, authData, to, go]);
return (
<form
className="w-full p-2 max-w-md mt-12 bg-background"
{...getFormProps(form)}>
<span className="block !mb-8 space-y-2">
<h2 className="text-3xl font-bold">Check your inbox</h2>
<p className="text-input-placeholder">
We will need the six digit confirmation code you received in your
email in order to verify your account and get started. Didnt receive
a code?{" "}
<Button type="button" variant={"link"} className="text-md h-0">
Resend now
</Button>
</p>
</span>
<Field
inputProps={{ name: fields.otp.name }}
labelProps={{ children: "Confirmation Code" }}
errors={fields.otp.errors}
/>
<Button className="w-full h-14">Verify</Button>
<p className="text-input-placeholder w-full text-left">
<Link
to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4">
Back to Login
</Link>
</p>
</form>
);
}

View File

@ -1,5 +1,5 @@
import type { MetaFunction } from "@remix-run/node"; import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react"; import { Link, useLocation } from "@remix-run/react";
import { z } from "zod"; import { z } from "zod";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import logoPng from "~/images/lume-logo.png?url"; import logoPng from "~/images/lume-logo.png?url";
@ -11,6 +11,7 @@ import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod"; import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { import {
useGo, useGo,
useIsAuthenticated,
useLogin, useLogin,
useParsed, useParsed,
} from "@refinedev/core"; } from "@refinedev/core";
@ -24,11 +25,30 @@ export const meta: MetaFunction = () => {
]; ];
}; };
export type LoginParams = { type LoginParams = {
to: string; to: string;
}; };
export default function Login() { export default function Login() {
const location = useLocation();
const { isLoading: isAuthLoading, data: authData } = useIsAuthenticated();
const hash = location.hash;
const go = useGo();
const parsed = useParsed<LoginParams>();
useEffect(() => {
if (!isAuthLoading) {
if (authData?.authenticated) {
let to = "/dashboard";
if (parsed.params?.to) {
to = parsed.params.to;
}
go({ to, type: "push" });
}
}
}, [isAuthLoading, authData]);
return ( return (
<div className="p-10 h-screen relative"> <div className="p-10 h-screen relative">
<header> <header>
@ -42,7 +62,8 @@ export default function Login() {
/> />
</div> </div>
<LoginForm /> {hash === "" && <LoginForm />}
{hash === "#otp" && <OtpForm />}
<footer className="my-5"> <footer className="my-5">
<ul className="flex flex-row"> <ul className="flex flex-row">
@ -75,12 +96,11 @@ export default function Login() {
const LoginSchema = z.object({ const LoginSchema = z.object({
email: z.string().email(), email: z.string().email(),
password: z.string(), password: z.string(),
rememberMe: z.boolean().optional(), rememberMe: z.boolean(),
}); });
const LoginForm = () => { const LoginForm = () => {
const login = useLogin<AuthFormRequest>(); const login = useLogin<AuthFormRequest>();
const go = useGo();
const parsed = useParsed<LoginParams>(); const parsed = useParsed<LoginParams>();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "login", id: "login",
@ -89,27 +109,19 @@ const LoginForm = () => {
return parseWithZod(formData, { schema: LoginSchema }); return parseWithZod(formData, { schema: LoginSchema });
}, },
shouldValidate: "onSubmit", shouldValidate: "onSubmit",
onSubmit(e, { submission }) { onSubmit(e) {
e.preventDefault(); e.preventDefault();
if (submission?.status === "success") { const data = Object.fromEntries(new FormData(e.currentTarget).entries());
const data = submission.value; login.mutate({
login.mutate({ email: data.email.toString(),
email: data.email, password: data.password.toString(),
password: data.password, rememberMe: data.rememberMe.toString() === "on",
rememberMe: data.rememberMe ?? false, redirectTo: parsed.params?.to,
redirectTo: parsed.params?.to, });
});
}
}, },
}); });
useEffect(() => {
if (form.status === "success") {
go({ to: "/login/otp", type: "push" });
}
}, [form.status, go]);
return ( return (
<form <form
className="w-full p-2 max-w-md space-y-3 mt-12 bg-background" className="w-full p-2 max-w-md space-y-3 mt-12 bg-background"
@ -147,3 +159,56 @@ const LoginForm = () => {
</form> </form>
); );
}; };
const OtpSchema = z.object({
otp: z.string().length(6, { message: "OTP must be 6 characters" }),
});
const OtpForm = () => {
// TODO: Add support for resending the OTP
const [form, fields] = useForm({
id: "otp",
constraint: getZodConstraint(OtpSchema),
onValidate({ formData }) {
return parseWithZod(formData, { schema: OtpSchema });
},
shouldValidate: "onSubmit",
});
const valid = false; // TODO: some sort of logic to verify user is on OTP state validly
if (!valid) {
location.hash = "";
return null;
}
return (
<form
className="w-full p-2 max-w-md mt-12 bg-background"
{...getFormProps(form)}>
<span className="block !mb-8 space-y-2">
<h2 className="text-3xl font-bold">Check your inbox</h2>
<p className="text-input-placeholder">
We will need the six digit confirmation code you received in your
email in order to verify your account and get started. Didnt receive
a code?{" "}
<Button type="button" variant={"link"} className="text-md h-0">
Resend now
</Button>
</p>
</span>
<Field
inputProps={{ name: fields.otp.name }}
labelProps={{ children: "Confirmation Code" }}
errors={fields.otp.errors}
/>
<Button className="w-full h-14">Verify</Button>
<p className="text-input-placeholder w-full text-left">
<Link
to="/login"
className="text-primary-1 text-md hover:underline hover:underline-offset-4">
Back to Login
</Link>
</p>
</form>
);
};

View File

@ -47,11 +47,11 @@ export default function Register() {
const login = useLogin<AuthFormRequest>(); const login = useLogin<AuthFormRequest>();
const { open } = useNotification(); const { open } = useNotification();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "register", id: "register",
constraint: getZodConstraint(RegisterSchema), constraint: getZodConstraint(RegisterSchema),
onValidate({formData}) { onValidate({ formData }) {
return parseWithZod(formData, {schema: RegisterSchema}); return parseWithZod(formData, { schema: RegisterSchema });
}, },
onSubmit(e) { onSubmit(e) {
e.preventDefault(); e.preventDefault();
@ -61,9 +61,23 @@ export default function Register() {
password: data.password.toString(), password: data.password.toString(),
firstName: data.firstName.toString(), firstName: data.firstName.toString(),
lastName: data.lastName.toString(), lastName: data.lastName.toString(),
}) }, {
} onSuccess: () => {
}); open?.({
type: "success",
message: "Verify your Email",
description: "An Email was sent to your email address. Please verify your email address to activate your account.",
key: "register-success"
})
login.mutate({
email: data.email.toString(),
password: data.password.toString(),
rememberMe: false,
})
}
})
}
});
return ( return (
<div className="p-10 h-screen relative"> <div className="p-10 h-screen relative">

View File

@ -4,8 +4,8 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 3.9%; --background: 240, 33%, 3%;
--foreground: 0 0% 98%; --foreground: 0, 0%, 88%;
--card: 0 0% 0%; --card: 0 0% 0%;
--card-foreground: 0 0% 3.9%; --card-foreground: 0 0% 3.9%;

View File

@ -16,21 +16,19 @@
"@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-20240319140708",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@lumeweb/portal-sdk": "0.0.0-20240321203634",
"@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",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.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-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@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",
@ -47,7 +45,6 @@
"@visx/visx": "^3.10.2", "@visx/visx": "^3.10.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"date-fns": "^3.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",