Compare commits
54 Commits
7b31d561fe
...
c9de506c56
Author | SHA1 | Date |
---|---|---|
Juan Di Toro | c9de506c56 | |
Juan Di Toro | e1ca45f1f1 | |
Juan Di Toro | c9851d8dee | |
Juan Di Toro | 2ccf55f75f | |
Juan Di Toro | a61fe195ee | |
Juan Di Toro | e401889b04 | |
Juan Di Toro | 9d9aa4e9c9 | |
Juan Di Toro | db91cb9590 | |
Derrick Hammer | 364f2b048b | |
Derrick Hammer | 4619f9709c | |
Derrick Hammer | 6252979d28 | |
Derrick Hammer | 23cc02b26e | |
Derrick Hammer | ea395df494 | |
Derrick Hammer | 7ea4af1346 | |
Derrick Hammer | 20d8905a67 | |
Derrick Hammer | 0c304b20db | |
Derrick Hammer | f7a760051f | |
Derrick Hammer | 6cdf8e40b9 | |
Derrick Hammer | 3a5cd0ae32 | |
Derrick Hammer | b42a04ebc3 | |
Derrick Hammer | 61b70ffa2e | |
Derrick Hammer | a7ef0b4773 | |
Derrick Hammer | 3162d4dcd6 | |
Derrick Hammer | d724359622 | |
Derrick Hammer | 39ac3467fc | |
Derrick Hammer | b9652ab261 | |
Derrick Hammer | 184b3d9a0e | |
Derrick Hammer | 335e982ab0 | |
Derrick Hammer | be31f0db04 | |
Derrick Hammer | 7bd2ecdc82 | |
Derrick Hammer | d326881f9b | |
Derrick Hammer | d4ad64bf88 | |
Derrick Hammer | 51e8cfef76 | |
Derrick Hammer | 583a95e68a | |
Derrick Hammer | adf306b074 | |
Derrick Hammer | 02b712793e | |
Derrick Hammer | 8fecd31e43 | |
Derrick Hammer | 214eb14583 | |
Derrick Hammer | 82b4f9f4fc | |
Derrick Hammer | 2d0609c95c | |
Derrick Hammer | bba7ce59d3 | |
Derrick Hammer | 9332598627 | |
Derrick Hammer | 6ce3b02dc6 | |
Derrick Hammer | 81d52d2524 | |
Derrick Hammer | 64e2216a36 | |
Derrick Hammer | bec75e63ee | |
Juan Di Toro | 43a0ab0b29 | |
Derrick Hammer | b3f0044723 | |
Derrick Hammer | 729414c45a | |
Derrick Hammer | 6fbbe4975c | |
Derrick Hammer | 33decc2e2d | |
Derrick Hammer | 081df029e0 | |
Derrick Hammer | 6bffca0524 | |
Juan Di Toro | c291434881 |
|
@ -1,8 +1,8 @@
|
||||||
import { useMemo} from "react";
|
import { useMemo} from "react";
|
||||||
import { BaseRecord } from "@refinedev/core";
|
import type { BaseRecord } from "@refinedev/core";
|
||||||
import { useTable } from "@refinedev/react-table";
|
import { useTable } from "@refinedev/react-table";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
type 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) => {
|
{headerGroup.headers.map((header, index) => {
|
||||||
return (
|
return (
|
||||||
<TableHead key={header.id} style={{ width: header.getSize() }}>
|
<TableHead key={`FileDataTableHeader_${index}`} 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) => (
|
rows.map((row, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row.id}
|
key={`FileDataTableRow_${index}`}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
className="group"
|
className="group"
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell, index) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell key={`FileDataTableCell_${index}`}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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 { FieldName, useInputControl } from "@conform-to/react"
|
import { type 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"
|
||||||
|
|
|
@ -25,20 +25,30 @@ import {
|
||||||
PageIcon,
|
PageIcon,
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem } from "./ui/dropdown-menu";
|
import {
|
||||||
|
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">
|
||||||
|
@ -93,7 +103,6 @@ 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">
|
||||||
|
@ -142,7 +151,11 @@ 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 className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
|
<img
|
||||||
|
className="h-5"
|
||||||
|
src={lumeColorLogoPng}
|
||||||
|
alt="Lume Logo"
|
||||||
|
/>
|
||||||
Connect with us
|
Connect with us
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -252,6 +265,7 @@ 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">
|
||||||
|
@ -263,12 +277,23 @@ const UploadFileItem = ({
|
||||||
<PageIcon className="w-4 h-4" />
|
<PageIcon className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={500}>
|
||||||
|
<TooltipTrigger>
|
||||||
<p className="w-full flex justify-between items-center">
|
<p className="w-full flex justify-between items-center">
|
||||||
<span className="truncate text-ellipsis max-w-[30ch]">
|
<span className="truncate text-ellipsis max-w-[20ch]">
|
||||||
{file.name}
|
{file.name}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span>({bytestoMegabytes(file.size).toFixed(2)} MB)</span>
|
<span>({sizeInMb}MB)</span>
|
||||||
</p>
|
</p>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{file.name} ({sizeInMb}MB)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size={"icon"}
|
size={"icon"}
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
|
import Uppy, { debugLogger, type State, type 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 {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
|
import {
|
||||||
import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
|
type ChangeEvent,
|
||||||
import {useSdk} from "~/components/lib/sdk-context.js";
|
useCallback,
|
||||||
import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
|
useEffect,
|
||||||
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
|
useMemo,
|
||||||
import {S5Client, HashProgressEvent} from "@lumeweb/s5-js";
|
useRef,
|
||||||
|
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",
|
||||||
|
@ -16,13 +23,13 @@ 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() {
|
||||||
|
@ -30,32 +37,32 @@ export function useUppy() {
|
||||||
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 {
|
||||||
|
@ -63,35 +70,35 @@ 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) => {
|
||||||
|
@ -101,21 +108,28 @@ export function useUppy() {
|
||||||
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!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File, {}, {onHashProgress: hashProgressCb})
|
const options = await sdk.protocols!()
|
||||||
|
.get<S5Client>(PROTOCOL_S5)
|
||||||
|
.getSdk()
|
||||||
|
.getTusOptions(
|
||||||
|
file.data as File,
|
||||||
|
{},
|
||||||
|
{ onHashProgress: hashProgressCb },
|
||||||
|
);
|
||||||
uppyInstance.current?.setFileState(fileID, {
|
uppyInstance.current?.setFileState(fileID, {
|
||||||
tus: options,
|
tus: options,
|
||||||
meta: {
|
meta: {
|
||||||
...options.metadata,
|
...options.metadata,
|
||||||
...file.meta,
|
...file.meta,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const uppy = new Uppy({
|
const uppy = new Uppy({
|
||||||
logger: debugLogger,
|
logger: debugLogger,
|
||||||
|
@ -128,25 +142,25 @@ export function useUppy() {
|
||||||
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;
|
||||||
|
|
||||||
|
@ -154,11 +168,11 @@ export function useUppy() {
|
||||||
if (file.size > uploadLimit) {
|
if (file.size > uploadLimit) {
|
||||||
useTus = true;
|
useTus = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (useTus) {
|
if (useTus) {
|
||||||
uppy.use(Tus, { limit: 6, parallelUploads: 10 })
|
uppy.use(Tus, { limit: 6, parallelUploads: 10 });
|
||||||
uppy.addPreProcessor(tusPreprocessor)
|
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
|
||||||
|
@ -169,54 +183,52 @@ 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,
|
||||||
|
@ -227,6 +239,6 @@ export function useUppy() {
|
||||||
getInputProps: () => inputProps,
|
getInputProps: () => inputProps,
|
||||||
getRootProps,
|
getRootProps,
|
||||||
removeFile,
|
removeFile,
|
||||||
cancelAll
|
cancelAll,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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-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",
|
"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",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function Toaster() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(function ({ id, title, description, action, cancelMutation, ...props }) {
|
{toasts.map(({ 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}>
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
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 }
|
|
@ -50,7 +50,7 @@ export const UsageChart = ({ label, dataset }: UsageChartProps) => {
|
||||||
<AnimatedAxis
|
<AnimatedAxis
|
||||||
orientation="bottom"
|
orientation="bottom"
|
||||||
hideTicks
|
hideTicks
|
||||||
tickTransform="translateX(50%)"
|
tickTransform="translate(50 0)"
|
||||||
tickLabelProps={{ className: "text-sm" }}
|
tickLabelProps={{ className: "text-sm" }}
|
||||||
/>
|
/>
|
||||||
<AnimatedAxis
|
<AnimatedAxis
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const accountProvider: SdkProvider = {
|
||||||
|
|
||||||
if (ret) {
|
if (ret) {
|
||||||
if (ret instanceof Error) {
|
if (ret instanceof Error) {
|
||||||
return Promise.reject(ret)
|
return Promise.reject(ret satisfies HttpError)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import type {AuthProvider, UpdatePasswordFormTypes} from "@refinedev/core"
|
import type {AuthProvider, HttpError, 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} from "@lumeweb/portal-sdk";
|
import {Sdk, AccountError} 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;
|
||||||
|
@ -43,6 +45,54 @@ 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({
|
||||||
|
@ -50,39 +100,30 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
|
||||||
password: params.password,
|
password: params.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
let redirectTo: string | undefined;
|
return handleResponse({
|
||||||
|
ret, redirectToSuccess: "/dashboard", redirectToError: "/login", successCb: () => {
|
||||||
if (ret) {
|
|
||||||
redirectTo = params.redirectTo;
|
|
||||||
if (!redirectTo) {
|
|
||||||
redirectTo = ret ? "/dashboard" : "/login";
|
|
||||||
}
|
|
||||||
sdk.setAuthToken(sdk.account().jwtToken);
|
sdk.setAuthToken(sdk.account().jwtToken);
|
||||||
}
|
}, successNotification: {
|
||||||
|
message: "Login Successful",
|
||||||
|
description: "You have successfully logged in."
|
||||||
|
|
||||||
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 {success: ret, redirectTo: "/login"};
|
return handleResponse({ret, redirectToSuccess: "/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();
|
||||||
|
|
||||||
if (ret) {
|
return handleCheckResponse({ret, redirectToError: "/login", successCb: maybeSetupAuth});
|
||||||
maybeSetupAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async onError(error: any): Promise<OnErrorResponse> {
|
async onError(error: any): Promise<OnErrorResponse> {
|
||||||
return {logout: true};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
|
async register(params: RegisterFormRequest): Promise<AuthActionResponse> {
|
||||||
|
@ -92,7 +133,12 @@ export const createPortalAuthProvider = (sdk: Sdk): AuthProvider => {
|
||||||
first_name: params.firstName,
|
first_name: params.firstName,
|
||||||
last_name: params.lastName,
|
last_name: params.lastName,
|
||||||
});
|
});
|
||||||
return {success: ret, redirectTo: ret ? "/dashboard" : undefined};
|
return handleResponse({
|
||||||
|
ret, redirectToSuccess: "/login", successNotification: {
|
||||||
|
message: "Registration Successful",
|
||||||
|
description: "You have successfully registered. Please check your email to verify your account.",
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async forgotPassword(params: any): Promise<AuthActionResponse> {
|
async forgotPassword(params: any): Promise<AuthActionResponse> {
|
||||||
|
@ -103,22 +149,12 @@ 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);
|
||||||
|
|
||||||
if (ret) {
|
return handleResponse({
|
||||||
if (ret instanceof Error) {
|
ret, successNotification: {
|
||||||
return {
|
message: "Password Updated",
|
||||||
success: false,
|
description: "Your password has been updated successfully.",
|
||||||
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> {
|
||||||
|
|
|
@ -1,28 +1,108 @@
|
||||||
import type { DataProvider } from "@refinedev/core";
|
import type {SdkProvider} from "~/data/sdk-provider.js";
|
||||||
import { SdkProvider } from "~/data/sdk-provider.js";
|
import type {S5Client} from "@lumeweb/s5-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";
|
||||||
|
|
||||||
export const fileProvider = {
|
async function getIsManifest(s5: S5Client, hash: string): Promise<boolean | number> {
|
||||||
getList: () => {
|
let type: number | null;
|
||||||
console.log("Not implemented");
|
try {
|
||||||
return {
|
const abort = new AbortController();
|
||||||
data: [
|
const resp = s5.downloadData(hash, {
|
||||||
{
|
onDownloadProgress: (progressEvent: AxiosProgressEvent) => {
|
||||||
name: "whirly-final-draft.psd",
|
if (progressEvent.loaded >= 10) {
|
||||||
cid: "0xB45165ED3CD437B",
|
abort.abort();
|
||||||
size: "1.89 MB",
|
|
||||||
createdOn: " 03/02/2024 at 13:29 PM",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "whirly-final-draft.psd",
|
|
||||||
cid: "0xB45165ED3CD437B",
|
|
||||||
size: "1.89 MB",
|
|
||||||
createdOn: " 03/02/2024 at 13:29 PM",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getOne: () => {
|
httpConfig: {
|
||||||
|
signal: abort.signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp;
|
||||||
|
const unpacker = Unpacker.fromPacked(Buffer.from(data));
|
||||||
|
try {
|
||||||
|
const magic = unpacker.unpackInt();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
case METADATA_TYPES.DIRECTORY:
|
||||||
|
return CID_TYPES.DIRECTORY;
|
||||||
|
case METADATA_TYPES.WEBAPP:
|
||||||
|
return CID_TYPES.METADATA_WEBAPP;
|
||||||
|
case METADATA_TYPES.MEDIA:
|
||||||
|
return CID_TYPES.METADATA_MEDIA;
|
||||||
|
case METADATA_TYPES.USER_IDENTITY:
|
||||||
|
return CID_TYPES.USER_IDENTITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileItem {
|
||||||
|
cid: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
mimeType: string;
|
||||||
|
pinned: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileProvider: SdkProvider = {
|
||||||
|
sdk: undefined,
|
||||||
|
async getList() {
|
||||||
|
const items: FileItem[] = [];
|
||||||
|
try {
|
||||||
|
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");
|
console.log("Not implemented");
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: {
|
data: {
|
||||||
|
@ -30,23 +110,25 @@ export const fileProvider = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
update: () => {
|
update() {
|
||||||
console.log("Not implemented");
|
console.log("Not implemented");
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
create: () => {
|
create() {
|
||||||
console.log("Not implemented");
|
console.log("Not implemented");
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteOne: () => {
|
deleteOne() {
|
||||||
console.log("Not implemented");
|
console.log("Not implemented");
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getApiUrl: () => "",
|
getApiUrl() {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
} satisfies SdkProvider;
|
} satisfies SdkProvider;
|
||||||
|
|
|
@ -1,51 +1,62 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { useContext } from "react";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
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 } = useMutation({
|
const { mutate: pinMutation } = useMutation({
|
||||||
mutationKey: ["pin-progress"],
|
mutationKey: ["pin-mutation"],
|
||||||
mutationFn: async (variables: { cid: string, type: "pin" | "unpin" }) => {
|
mutationFn: async (variables: { cid: string }) => {
|
||||||
const { cid, type } = variables;
|
const { cid } = variables;
|
||||||
switch (type) {
|
|
||||||
case "pin": {
|
|
||||||
const response = await PinningProcess.pin(cid);
|
const response = await PinningProcess.pin(cid);
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
open?.({
|
open?.({
|
||||||
type: "destructive",
|
type: "error",
|
||||||
message: "Erorr pinning " + cid,
|
message: `Error pinning ${cid}`,
|
||||||
description: response.message,
|
description: response.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
|
||||||
case "unpin": {
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: unpinMutation } = useMutation({
|
||||||
|
mutationKey: ["unpin-mutation"],
|
||||||
|
mutationFn: async (variables: { cid: string }) => {
|
||||||
|
const { cid } = variables;
|
||||||
const response = await PinningProcess.unpin(cid);
|
const response = await PinningProcess.unpin(cid);
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
open?.({
|
open?.({
|
||||||
type: "destructive",
|
type: "error",
|
||||||
message: "Erorr removing " + cid,
|
message: `Error removing ${cid}`,
|
||||||
description: response.message,
|
description: response.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
|
||||||
break;
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.queryClient.invalidateQueries({ queryKey: ["pin-progress"] })
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bulkPin = useCallback(
|
||||||
|
(cids: string[]) => {
|
||||||
|
for (const cid of cids) {
|
||||||
|
pinMutation({ cid });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pinMutation],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...context.query,
|
...context.query,
|
||||||
mutate
|
pin: pinMutation,
|
||||||
|
unpin: unpinMutation,
|
||||||
|
bulkPin,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
type QueryClient,
|
||||||
UseQueryResult,
|
type 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, PinningStatus } from "~/data/pinning";
|
import { PinningProcess, type PinningStatus } from "~/data/pinning";
|
||||||
|
|
||||||
export interface IPinningData {
|
export interface IPinningData {
|
||||||
cid: string;
|
cid: string;
|
||||||
|
|
38
app/root.tsx
38
app/root.tsx
|
@ -1,19 +1,26 @@
|
||||||
import {Links, Meta, Outlet, Scripts, ScrollRestoration,} from "@remix-run/react";
|
import {
|
||||||
|
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} from "~/components/lib/sdk-context";
|
import { SdkContextProvider, useSdk } 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 },
|
||||||
|
@ -30,7 +37,7 @@ export function Layout({children}: { children: React.ReactNode }) {
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body className="overflow-hidden">
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
@ -40,10 +47,9 @@ export function Layout({children}: { children: React.ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
function App() {
|
||||||
console.log(import.meta.env.VITE_PORTAL_URL);
|
const sdk = useSdk();
|
||||||
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL)
|
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]);
|
||||||
const providers = getProviders(sdk);
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Refine
|
<Refine
|
||||||
|
@ -52,19 +58,25 @@ export default function App() {
|
||||||
notificationProvider={notificationProvider}
|
notificationProvider={notificationProvider}
|
||||||
dataProvider={{
|
dataProvider={{
|
||||||
default: providers.default,
|
default: providers.default,
|
||||||
files: providers.files
|
files: providers.files,
|
||||||
}}
|
}}
|
||||||
resources={resources}
|
resources={resources}
|
||||||
options={{disableTelemetry: true}}
|
options={{ disableTelemetry: true }}>
|
||||||
>
|
|
||||||
<SdkContextProvider sdk={sdk}>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</SdkContextProvider>
|
|
||||||
</Refine>
|
</Refine>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Root() {
|
||||||
|
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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
import { useGo, useIsAuthenticated } from "@refinedev/core";
|
import {Authenticated} from "@refinedev/core";
|
||||||
|
import {Navigate} from "@remix-run/react";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { isLoading, data } = useIsAuthenticated();
|
return (
|
||||||
|
<Authenticated v3LegacyAuthProviderCompatible key={"index"} loading={
|
||||||
const go = useGo();
|
<>Checking Login Status</>
|
||||||
|
}>
|
||||||
if (isLoading) {
|
<Navigate to="/dashboard" replace/>
|
||||||
return <>Checking Login Status</>;
|
</Authenticated>
|
||||||
}
|
)
|
||||||
|
|
||||||
if (data?.authenticated) {
|
|
||||||
go({ to: "/dashboard", type: "replace" });
|
|
||||||
} else {
|
|
||||||
go({ to: "/login", type: "replace" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>Redirecting</>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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,
|
||||||
BaseKey,
|
type BaseKey,
|
||||||
useGetIdentity,
|
useGetIdentity,
|
||||||
useUpdate,
|
useUpdate,
|
||||||
useUpdatePassword,
|
useUpdatePassword,
|
||||||
|
@ -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 {UpdatePasswordFormRequest} from "~/data/auth-provider.js";
|
import type { UpdatePasswordFormRequest } from "~/data/auth-provider";
|
||||||
|
|
||||||
export default function MyAccount() {
|
export default function MyAccount() {
|
||||||
const { data: identity } = useGetIdentity<{ email: string }>();
|
const { data: identity } = useGetIdentity<{ email: string }>();
|
||||||
|
@ -85,9 +85,13 @@ export default function MyAccount() {
|
||||||
<ManagementCard>
|
<ManagementCard>
|
||||||
<ManagementCardAvatar
|
<ManagementCardAvatar
|
||||||
button={
|
button={
|
||||||
<DialogTrigger className="absolute bottom-0 right-0 z-50">
|
<DialogTrigger
|
||||||
|
asChild
|
||||||
|
className="absolute bottom-0 right-0 z-50">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setModal({ ...openModal, changeAvatar: true })}
|
onClick={() =>
|
||||||
|
setModal({ ...openModal, changeAvatar: true })
|
||||||
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
|
className=" flex items-center w-10 h-10 p-0 border-white rounded-full justiyf-center hover:bg-secondary-2">
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
|
@ -102,10 +106,12 @@ export default function MyAccount() {
|
||||||
{identity?.email}
|
{identity?.email}
|
||||||
</ManagementCardContent>
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<DialogTrigger>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="h-12 gap-x-2"
|
className="h-12 gap-x-2"
|
||||||
onClick={() => setModal({ ...openModal, changeEmail: true })}>
|
onClick={() =>
|
||||||
|
setModal({ ...openModal, changeEmail: true })
|
||||||
|
}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
Change Email Address
|
Change Email Address
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -134,7 +140,7 @@ export default function MyAccount() {
|
||||||
<PasswordDots className="mt-6" />
|
<PasswordDots className="mt-6" />
|
||||||
</ManagementCardContent>
|
</ManagementCardContent>
|
||||||
<ManagementCardFooter>
|
<ManagementCardFooter>
|
||||||
<DialogTrigger>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="h-12 gap-x-2"
|
className="h-12 gap-x-2"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -147,12 +153,14 @@ export default function MyAccount() {
|
||||||
</ManagementCardFooter>
|
</ManagementCardFooter>
|
||||||
</ManagementCard>
|
</ManagementCard>
|
||||||
<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>
|
||||||
<DialogTrigger>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="h-12 gap-x-2"
|
className="h-12 gap-x-2"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -313,7 +321,8 @@ const ChangePasswordSchema = z
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChangePasswordForm = () => {
|
const ChangePasswordForm = () => {
|
||||||
const { mutate: updatePassword } = useUpdatePassword<{ password: string }>();
|
const { mutate: updatePassword } =
|
||||||
|
useUpdatePassword<UpdatePasswordFormRequest>();
|
||||||
const [form, fields] = useForm({
|
const [form, fields] = useForm({
|
||||||
id: "login",
|
id: "login",
|
||||||
constraint: getZodConstraint(ChangePasswordSchema),
|
constraint: getZodConstraint(ChangePasswordSchema),
|
||||||
|
@ -327,6 +336,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(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -469,7 +479,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}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { DrawingPinIcon, TrashIcon } from "@radix-ui/react-icons";
|
import { TrashIcon } from "@radix-ui/react-icons";
|
||||||
import type { ColumnDef, Row } from "@tanstack/react-table";
|
import type { ColumnDef } 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,12 +7,11 @@ 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.
|
||||||
|
@ -23,47 +22,7 @@ export type File = {
|
||||||
createdOn: string;
|
createdOn: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreatedOnCell = ({ row }: { row: Row<File> }) => {
|
export const columns: ColumnDef<FileItem>[] = [
|
||||||
// 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,
|
||||||
|
@ -106,9 +65,34 @@ export const columns: ColumnDef<File>[] = [
|
||||||
header: "Size",
|
header: "Size",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createdOn",
|
accessorKey: "pinned",
|
||||||
size: 200,
|
header: "Pinned On",
|
||||||
header: "Created On",
|
cell: ({ row }) => new Date(row.getValue("pinned")).toLocaleString(),
|
||||||
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>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,10 +5,25 @@ 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>
|
||||||
|
<Dialog>
|
||||||
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
||||||
<FileCardList>
|
<FileCardList>
|
||||||
<FileCard
|
<FileCard
|
||||||
|
@ -48,16 +63,68 @@ export default function FileManager() {
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
Select All
|
Select All
|
||||||
</Button>
|
</Button>
|
||||||
|
<DialogTrigger asChild>
|
||||||
<Button className="h-12 gap-x-2">
|
<Button className="h-12 gap-x-2">
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
New Folder
|
Pin Content
|
||||||
</Button>
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
resource="file"
|
resource="file"
|
||||||
dataProviderName="files"
|
dataProviderName="files"
|
||||||
/>
|
/>
|
||||||
|
<DialogContent>
|
||||||
|
<PinFilesForm />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</GeneralLayout>
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
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. Didn’t 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import type { MetaFunction } from "@remix-run/node";
|
import type { MetaFunction } from "@remix-run/node";
|
||||||
import { Link, useLocation } from "@remix-run/react";
|
import { Link } 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,7 +11,6 @@ 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";
|
||||||
|
@ -25,30 +24,11 @@ export const meta: MetaFunction = () => {
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoginParams = {
|
export 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>
|
||||||
|
@ -62,8 +42,7 @@ export default function Login() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hash === "" && <LoginForm />}
|
<LoginForm />
|
||||||
{hash === "#otp" && <OtpForm />}
|
|
||||||
|
|
||||||
<footer className="my-5">
|
<footer className="my-5">
|
||||||
<ul className="flex flex-row">
|
<ul className="flex flex-row">
|
||||||
|
@ -96,11 +75,12 @@ 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(),
|
rememberMe: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
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",
|
||||||
|
@ -109,19 +89,27 @@ const LoginForm = () => {
|
||||||
return parseWithZod(formData, { schema: LoginSchema });
|
return parseWithZod(formData, { schema: LoginSchema });
|
||||||
},
|
},
|
||||||
shouldValidate: "onSubmit",
|
shouldValidate: "onSubmit",
|
||||||
onSubmit(e) {
|
onSubmit(e, { submission }) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
|
if (submission?.status === "success") {
|
||||||
|
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"
|
||||||
|
@ -159,56 +147,3 @@ 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. Didn’t 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -61,20 +61,6 @@ 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 240, 33%, 3%;
|
--background: 0 0% 3.9%;
|
||||||
--foreground: 0, 0%, 88%;
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
--card: 0 0% 0%;
|
--card: 0 0% 0%;
|
||||||
--card-foreground: 0 0% 3.9%;
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
|
|
@ -16,19 +16,21 @@
|
||||||
"@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",
|
||||||
|
@ -45,6 +47,7 @@
|
||||||
"@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",
|
||||||
|
|
Loading…
Reference in New Issue