Compare commits
No commits in common. "8d4fcfba5c972d1c353e75a4c9e92a23a3b852c5" and "c9851d8dee866e3c5834b0da4c72896503b33b2f" have entirely different histories.
8d4fcfba5c
...
c9851d8dee
|
@ -18,21 +18,16 @@ import { Skeleton } from "./ui/skeleton";
|
||||||
import { DataTablePagination } from "./table-pagination"
|
import { DataTablePagination } from "./table-pagination"
|
||||||
|
|
||||||
interface DataTableProps<TData extends BaseRecord = BaseRecord, TValue = unknown> {
|
interface DataTableProps<TData extends BaseRecord = BaseRecord, TValue = unknown> {
|
||||||
columns: ColumnDef<TData, TValue>[],
|
columns: ColumnDef<TData, TValue>[]
|
||||||
resource: string;
|
|
||||||
dataProviderName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData extends BaseRecord, TValue>({
|
export function DataTable<TData extends BaseRecord, TValue>({
|
||||||
columns,
|
columns,
|
||||||
resource,
|
|
||||||
dataProviderName
|
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const table = useTable({
|
const table = useTable({
|
||||||
columns,
|
columns,
|
||||||
refineCoreProps: {
|
refineCoreProps: {
|
||||||
resource,
|
resource: "file"
|
||||||
dataProviderName: dataProviderName || "default"
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -35,8 +35,6 @@ import {
|
||||||
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 { PinningNetworkBanner } from "./pinning-network-banner";
|
|
||||||
import { PinningProvider } from "~/providers/PinningProvider";
|
|
||||||
import type { Identity } from "~/data/auth-provider";
|
import type { Identity } from "~/data/auth-provider";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
@ -50,122 +48,116 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
|
||||||
const { data: identity } = useGetIdentity<Identity>();
|
const { data: identity } = useGetIdentity<Identity>();
|
||||||
const { mutate: logout } = useLogout();
|
const { mutate: logout } = useLogout();
|
||||||
return (
|
return (
|
||||||
<PinningProvider>
|
<div className="h-full flex flex-row">
|
||||||
<div className="h-full flex flex-row">
|
<header className="p-10 pr-0 flex flex-col w-[240px] h-full scroll-m-0 overflow-hidden">
|
||||||
<header className="p-10 pr-0 flex flex-col w-[240px] h-full scroll-m-0 overflow-hidden">
|
<img src={logoPng} alt="Lume logo" className="h-10 w-32" />
|
||||||
<img src={logoPng} alt="Lume logo" className="h-10 w-32" />
|
|
||||||
|
|
||||||
<nav className="my-10 flex-1">
|
<nav className="my-10 flex-1">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/dashboard">
|
<Link to="/dashboard">
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
active={location.pathname.includes("dashboard")}>
|
active={location.pathname.includes("dashboard")}>
|
||||||
<ClockIcon className="w-5 h-5 mr-2" />
|
<ClockIcon className="w-5 h-5 mr-2" />
|
||||||
Dashboard
|
Dashboard
|
||||||
</NavigationButton>
|
</NavigationButton>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/file-manager">
|
<Link to="/file-manager">
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
active={location.pathname.includes("file-manager")}>
|
active={location.pathname.includes("file-manager")}>
|
||||||
<DriveIcon className="w-5 h-5 mr-2" />
|
<DriveIcon className="w-5 h-5 mr-2" />
|
||||||
File Manager
|
File Manager
|
||||||
</NavigationButton>
|
</NavigationButton>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/account">
|
<Link to="/account">
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
active={location.pathname.includes("account")}>
|
active={location.pathname.includes("account")}>
|
||||||
<CircleLockIcon className="w-5 h-5 mr-2" />
|
<CircleLockIcon className="w-5 h-5 mr-2" />
|
||||||
Account
|
Account
|
||||||
</NavigationButton>
|
</NavigationButton>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<span className="text-primary-2 mb-3 -space-y-1 opacity-40">
|
<span className="text-primary-2 mb-3 -space-y-1 opacity-40">
|
||||||
<p>Freedom</p>
|
<p>Freedom</p>
|
||||||
<p>Privacy</p>
|
<p>Privacy</p>
|
||||||
<p>Ownership</p>
|
<p>Ownership</p>
|
||||||
</span>
|
</span>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button size={"lg"} className="w-[calc(100%-3rem)] font-semibold">
|
<Button size={"lg"} className="w-[calc(100%-3rem)] font-semibold">
|
||||||
<CloudUploadIcon className="w-5 h-5 mr-2" />
|
<CloudUploadIcon className="w-5 h-5 mr-2" />
|
||||||
Upload Files
|
Upload Files
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="border rounded-lg p-8">
|
|
||||||
<UploadFileForm />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</header>
|
|
||||||
<div className="flex-1 overflow-y-auto p-10">
|
|
||||||
<div className="flex items-center gap-x-4 justify-end">
|
|
||||||
<Button variant="ghost" className="rounded-full w-fit">
|
|
||||||
<ThemeIcon className="text-ring" />
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="border rounded-lg p-8">
|
||||||
|
<UploadFileForm />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</header>
|
||||||
|
|
||||||
<DropdownMenu>
|
<div className="flex-1 overflow-y-auto p-10">
|
||||||
<DropdownMenuTrigger asChild>
|
<div className="flex items-center gap-x-4 justify-end">
|
||||||
<Button className="border rounded-full h-auto p-2 gap-x-2 text-ring font-semibold">
|
<Button variant="ghost" className="rounded-full w-fit">
|
||||||
<Avatar className="bg-ring h-7 w-7 rounded-full" />
|
<ThemeIcon className="text-ring" />
|
||||||
{`${identity?.firstName} ${identity?.lastName}`}
|
</Button>
|
||||||
<ChevronDownIcon />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem onClick={() => logout()}>
|
|
||||||
<ExitIcon className="mr-2" />
|
|
||||||
Logout
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{children}
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
<footer className="mt-5">
|
<Button className="border rounded-full h-auto p-2 gap-x-2 text-ring font-semibold">
|
||||||
<ul className="flex flex-row">
|
<Avatar className="bg-ring h-7 w-7 rounded-full" />
|
||||||
<li>
|
{`${identity?.firstName} ${identity?.lastName}`}
|
||||||
<Link to="https://discord.lumeweb.com">
|
<ChevronDownIcon />
|
||||||
<Button
|
</Button>
|
||||||
variant={"link"}
|
</DropdownMenuTrigger>
|
||||||
className="flex flex-row gap-x-2 text-input-placeholder">
|
<DropdownMenuContent align="end">
|
||||||
<img
|
<DropdownMenuGroup>
|
||||||
className="h-5"
|
<DropdownMenuItem onClick={() => logout()}>
|
||||||
src={discordLogoPng}
|
<ExitIcon className="mr-2" />
|
||||||
alt="Discord Logo"
|
Logout
|
||||||
/>
|
</DropdownMenuItem>
|
||||||
Connect with us
|
</DropdownMenuGroup>
|
||||||
</Button>
|
</DropdownMenuContent>
|
||||||
</Link>
|
</DropdownMenu>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="https://lumeweb.com">
|
|
||||||
<Button
|
|
||||||
variant={"link"}
|
|
||||||
className="flex flex-row gap-x-2 text-input-placeholder">
|
|
||||||
<img
|
|
||||||
className="h-5"
|
|
||||||
src={lumeColorLogoPng}
|
|
||||||
alt="Lume Logo"
|
|
||||||
/>
|
|
||||||
Connect with us
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<footer className="mt-5">
|
||||||
|
<ul className="flex flex-row">
|
||||||
|
<li>
|
||||||
|
<Link to="https://discord.lumeweb.com">
|
||||||
|
<Button
|
||||||
|
variant={"link"}
|
||||||
|
className="flex flex-row gap-x-2 text-input-placeholder">
|
||||||
|
<img
|
||||||
|
className="h-5"
|
||||||
|
src={discordLogoPng}
|
||||||
|
alt="Discord Logo"
|
||||||
|
/>
|
||||||
|
Connect with us
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="https://lumeweb.com">
|
||||||
|
<Button
|
||||||
|
variant={"link"}
|
||||||
|
className="flex flex-row gap-x-2 text-input-placeholder">
|
||||||
|
<img className="h-5" src={lumeColorLogoPng} alt="Lume Logo" />
|
||||||
|
Connect with us
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<PinningNetworkBanner />
|
</div>
|
||||||
</PinningProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
import { useContext, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "./ui/accordion";
|
|
||||||
import { Progress } from "./ui/progress";
|
|
||||||
import { usePinning } from "~/hooks/usePinning";
|
|
||||||
import { Tabs, TabsTrigger, TabsList, TabsContent } from "./ui/tabs";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
|
||||||
import type { PinningStatus } from "~/data/pinning";
|
|
||||||
import { PinningContext } from "~/providers/PinningProvider";
|
|
||||||
|
|
||||||
export const PinningNetworkBanner = () => {
|
|
||||||
// const context = useContext(PinningContext);
|
|
||||||
const { data } = usePinning();
|
|
||||||
|
|
||||||
// TODO: Adapt to real API
|
|
||||||
const itemsLeft = useMemo(
|
|
||||||
() =>
|
|
||||||
data?.items.filter((item: PinningStatus) =>
|
|
||||||
item.status.includes("inprogress"),
|
|
||||||
) || [],
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
const completedItems = useMemo(
|
|
||||||
() =>
|
|
||||||
data?.items.filter((item: PinningStatus) =>
|
|
||||||
item.status.includes("completed"),
|
|
||||||
) || [],
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`bg-background border border-border rounded-lg absolute w-1/3 bottom-4 right-4 ${
|
|
||||||
!data?.items.length ? "hidden" : "block"
|
|
||||||
}`}>
|
|
||||||
<Accordion type="single" defaultValue="item-1" collapsible>
|
|
||||||
<AccordionItem value="item-1">
|
|
||||||
<AccordionTrigger className="font-bold bg-primary px-4 rounded-tr-lg rounded-tl-lg">
|
|
||||||
{`${completedItems.length}/${data?.items.length} items completed`}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<Tabs className="w-full" defaultValue="inProgress">
|
|
||||||
<TabsList className="rounded-none">
|
|
||||||
<TabsTrigger value="inProgress">In Progress</TabsTrigger>
|
|
||||||
<TabsTrigger value="completed">Completed</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="inProgress">
|
|
||||||
{itemsLeft.length ? (
|
|
||||||
itemsLeft.map((item: PinningStatus) => (
|
|
||||||
<PinCidItem key={item.id} item={item} />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-primary-2 text-sm flex justify-center items-center h-10">
|
|
||||||
Nothing yet.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="completed">
|
|
||||||
{completedItems.length ? (
|
|
||||||
completedItems.map((item: PinningStatus) => (
|
|
||||||
<PinCidItem key={item.id} item={item} />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-muted text-sm flex justify-center items-center h-10">
|
|
||||||
Nothing yet.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PinCidItem = ({ item }: { item: PinningStatus }) => {
|
|
||||||
const { unpin } = usePinning();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-4 mb-4">
|
|
||||||
<div className="relative flex flex-col items-center rounded-lg py-2 px-4 hover:bg-primary/50 group">
|
|
||||||
<div className="flex justify-between items-center w-full">
|
|
||||||
<span className="font-semibold">{item.id}</span>
|
|
||||||
<span className="group-hover:hidden">{item.progress}%</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="absolute top-2 right-2 hidden group-hover:flex rounded-full h-3"
|
|
||||||
onClick={() =>
|
|
||||||
unpin({
|
|
||||||
cid: item.id,
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<Cross2Icon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Progress value={item.progress} className="h-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,58 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
||||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
|
||||||
|
|
||||||
import { cn } from "~/utils";
|
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root;
|
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-b", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
AccordionItem.displayName = "AccordionItem";
|
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Header className="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}>
|
|
||||||
{children}
|
|
||||||
<ChevronDownIcon className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
));
|
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
||||||
{...props}>
|
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
));
|
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export {
|
|
||||||
Accordion,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
AccordionContent,
|
|
||||||
};
|
|
|
@ -1,53 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
||||||
|
|
||||||
import { cn } from "~/utils";
|
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root;
|
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-primary-dark p-1 text-muted-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-primary-1 data-[state=active]:shadow",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type {SdkProvider} from "~/data/sdk-provider.js";
|
import {SdkProvider} from "~/data/sdk-provider.js";
|
||||||
import type {S5Client} from "@lumeweb/s5-js";
|
import {S5Client} from "@lumeweb/s5-js";
|
||||||
import {PROTOCOL_S5} from "@lumeweb/portal-sdk";
|
import {PROTOCOL_S5} from "@lumeweb/portal-sdk";
|
||||||
import {Multihash} from "@lumeweb/libs5/lib/multihash.js";
|
import {Multihash} from "@lumeweb/libs5/lib/multihash.js";
|
||||||
import type {AxiosProgressEvent} from "axios";
|
import {AxiosProgressEvent} from "axios";
|
||||||
import {CID, CID_TYPES, METADATA_TYPES, metadataMagicByte, Unpacker} from "@lumeweb/libs5";
|
import {CID, CID_TYPES, METADATA_TYPES, metadataMagicByte, Unpacker} from "@lumeweb/libs5";
|
||||||
|
|
||||||
async function getIsManifest(s5: S5Client, hash: string): Promise<boolean | number> {
|
async function getIsManifest(s5: S5Client, hash: string): Promise<boolean | number> {
|
||||||
|
|
||||||
let type: number | null;
|
let type: number | null;
|
||||||
try {
|
try {
|
||||||
const abort = new AbortController();
|
const abort = new AbortController();
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
export interface PinningStatus {
|
interface PinningStatus {
|
||||||
id: string;
|
id: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
status: 'inprogress' | 'completed' | 'stale';
|
status: 'inprogress' | 'completed' | 'stale';
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
|
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
|
||||||
export class PinningProcess {
|
class PinningProcess {
|
||||||
private static instances: Map<string, PinningStatus> = new Map();
|
private static instances: Map<string, PinningStatus> = new Map();
|
||||||
|
|
||||||
static async pin(id: string): Promise<{ success: boolean; message: string }> {
|
static async pin(id: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
@ -30,26 +30,13 @@ export class PinningProcess {
|
||||||
return { success: true, message: "Pinning process started" };
|
return { success: true, message: "Pinning process started" };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async unpin(id: string): Promise<{ success: boolean; message: string }> {
|
static *pollProgress(id: string): Generator<PinningStatus | null, void, unknown> {
|
||||||
if (!PinningProcess.instances.has(id)) {
|
let status = PinningProcess.instances.get(id);
|
||||||
return { success: false, message: "ID not found or not being processed" };
|
while (status && status.status !== 'completed') {
|
||||||
|
yield status;
|
||||||
|
status = PinningProcess.instances.get(id);
|
||||||
}
|
}
|
||||||
|
yield status ?? null; // Yield the final status, could be null if ID doesn't exist
|
||||||
PinningProcess.instances.delete(id);
|
|
||||||
return { success: true, message: "Pinning process removed" }
|
|
||||||
}
|
|
||||||
|
|
||||||
static *pollAllProgress(): Generator<PinningStatus[], void, unknown> {
|
|
||||||
let allStatuses = Array.from(PinningProcess.instances.values());
|
|
||||||
let inProgress = allStatuses.some(status => status.status !== 'completed');
|
|
||||||
|
|
||||||
while (inProgress) {
|
|
||||||
yield allStatuses;
|
|
||||||
allStatuses = Array.from(PinningProcess.instances.values());
|
|
||||||
inProgress = allStatuses.some(status => status.status !== 'completed');
|
|
||||||
}
|
|
||||||
|
|
||||||
yield allStatuses ?? []; // Yield the final statuses
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type {AuthProvider} from "@refinedev/core";
|
import type {AuthProvider, DataProvider} from "@refinedev/core";
|
||||||
import {fileProvider} from "~/data/file-provider.js";
|
import {fileProvider} from "~/data/file-provider.js";
|
||||||
import {Sdk} from "@lumeweb/portal-sdk";
|
import {Sdk} from "@lumeweb/portal-sdk";
|
||||||
import {accountProvider} from "~/data/account-provider.js";
|
import {accountProvider} from "~/data/account-provider.js";
|
||||||
|
@ -8,6 +8,7 @@ import {createPortalAuthProvider} from "~/data/auth-provider.js";
|
||||||
interface DataProviders {
|
interface DataProviders {
|
||||||
default: SdkProvider;
|
default: SdkProvider;
|
||||||
auth: AuthProvider;
|
auth: AuthProvider;
|
||||||
|
|
||||||
[key: string]: SdkProvider | AuthProvider;
|
[key: string]: SdkProvider | AuthProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { useNotification } from "@refinedev/core";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useCallback, useContext } from "react";
|
|
||||||
import { PinningProcess } from "~/data/pinning";
|
|
||||||
import { PinningContext } from "~/providers/PinningProvider";
|
|
||||||
|
|
||||||
export const usePinning = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const context = useContext(PinningContext);
|
|
||||||
const { open } = useNotification();
|
|
||||||
|
|
||||||
const { mutate: pinMutation } = useMutation({
|
|
||||||
mutationKey: ["pin-mutation"],
|
|
||||||
mutationFn: async (variables: { cid: string }) => {
|
|
||||||
const { cid } = variables;
|
|
||||||
const response = await PinningProcess.pin(cid);
|
|
||||||
|
|
||||||
if (!response.success) {
|
|
||||||
open?.({
|
|
||||||
type: "error",
|
|
||||||
message: `Error pinning ${cid}`,
|
|
||||||
description: response.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
...context.query,
|
|
||||||
pin: pinMutation,
|
|
||||||
unpin: unpinMutation,
|
|
||||||
bulkPin,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,63 +0,0 @@
|
||||||
import {
|
|
||||||
type QueryClient,
|
|
||||||
type UseQueryResult,
|
|
||||||
useQuery,
|
|
||||||
useQueryClient,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import { createContext, useContext } from "react";
|
|
||||||
import { PinningProcess, type PinningStatus } from "~/data/pinning";
|
|
||||||
|
|
||||||
export interface IPinningData {
|
|
||||||
cid: string;
|
|
||||||
progress: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPinningContextType {
|
|
||||||
query: UseQueryResult<
|
|
||||||
{
|
|
||||||
items: PinningStatus[];
|
|
||||||
lastUpdated: number;
|
|
||||||
},
|
|
||||||
Error
|
|
||||||
>;
|
|
||||||
queryClient: QueryClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PinningContext = createContext<IPinningContextType>(
|
|
||||||
{} as IPinningContextType,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const usePinningContext = () => useContext(PinningContext);
|
|
||||||
|
|
||||||
const usePinProgressQuery = () =>
|
|
||||||
useQuery({
|
|
||||||
queryKey: ["pin-progress"],
|
|
||||||
refetchInterval: (query) => {
|
|
||||||
if (!query.state.data || !query.state.data.items.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1000;
|
|
||||||
},
|
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
queryFn: () => {
|
|
||||||
const response = PinningProcess.pollAllProgress();
|
|
||||||
const result = response.next();
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: result.value || [],
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PinningProvider = ({ children }: React.PropsWithChildren) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const queryResult = usePinProgressQuery();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PinningContext.Provider value={{ query: queryResult, queryClient }}>
|
|
||||||
{children}
|
|
||||||
</PinningContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
109
app/root.tsx
109
app/root.tsx
|
@ -1,82 +1,69 @@
|
||||||
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, 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 {useMemo} from "react";
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
{ rel: "stylesheet", href: stylesheet },
|
{rel: "stylesheet", href: stylesheet},
|
||||||
];
|
];
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
export function Layout({children}: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
<html lang="en">
|
||||||
return (
|
<head>
|
||||||
<html lang="en">
|
<meta charSet="utf-8"/>
|
||||||
<head>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<meta charSet="utf-8" />
|
<Meta/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<Links/>
|
||||||
<Meta />
|
</head>
|
||||||
<Links />
|
<body className="overflow-hidden">
|
||||||
</head>
|
|
||||||
<body className="overflow-hidden">
|
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster/>
|
||||||
<ScrollRestoration />
|
<ScrollRestoration/>
|
||||||
<Scripts />
|
<Scripts/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const sdk = useSdk();
|
const sdk = useSdk();
|
||||||
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]);
|
const providers = useMemo(() => getProviders(sdk as Sdk), [sdk]);
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<Refine
|
||||||
<Refine
|
authProvider={providers.auth}
|
||||||
authProvider={providers.auth}
|
routerProvider={routerProvider}
|
||||||
routerProvider={routerProvider}
|
notificationProvider={notificationProvider}
|
||||||
notificationProvider={notificationProvider}
|
dataProvider={providers}
|
||||||
dataProvider={{
|
resources={resources}
|
||||||
default: providers.default,
|
options={{disableTelemetry: true}}
|
||||||
files: providers.files,
|
>
|
||||||
}}
|
<Outlet/>
|
||||||
resources={resources}
|
</Refine>
|
||||||
options={{ disableTelemetry: true }}>
|
);
|
||||||
<Outlet />
|
|
||||||
</Refine>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL);
|
const sdk = Sdk.create(import.meta.env.VITE_PORTAL_URL)
|
||||||
return (
|
return (
|
||||||
<SdkContextProvider sdk={sdk}>
|
<SdkContextProvider sdk={sdk}>
|
||||||
<App />
|
<App/>
|
||||||
</SdkContextProvider>
|
</SdkContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HydrateFallback() {
|
export function HydrateFallback() {
|
||||||
return <p>Loading...</p>;
|
return <p>Loading...</p>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Navigate} from "@remix-run/react";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
return (
|
return (
|
||||||
<Authenticated key={"index"} loading={
|
<Authenticated v3LegacyAuthProviderCompatible key={"index"} loading={
|
||||||
<>Checking Login Status</>
|
<>Checking Login Status</>
|
||||||
}>
|
}>
|
||||||
<Navigate to="/dashboard" replace/>
|
<Navigate to="/dashboard" replace/>
|
||||||
|
|
|
@ -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,
|
type 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 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 }>();
|
||||||
|
@ -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 asChild 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 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
|
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 asChild>
|
||||||
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 asChild>
|
||||||
<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<UpdatePasswordFormRequest>();
|
||||||
useUpdatePassword<UpdatePasswordFormRequest>();
|
|
||||||
const [form, fields] = useForm({
|
const [form, fields] = useForm({
|
||||||
id: "login",
|
id: "login",
|
||||||
constraint: getZodConstraint(ChangePasswordSchema),
|
constraint: getZodConstraint(ChangePasswordSchema),
|
||||||
|
@ -336,8 +327,8 @@ 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(),
|
currentPassword: data.currentPassword.toString(),
|
||||||
password: data.newPassword.toString(),
|
password: data.newPassword.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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, RowData } from "@tanstack/react-table";
|
||||||
import { FileIcon, MoreIcon } from "~/components/icons";
|
import { FileIcon, MoreIcon } from "~/components/icons";
|
||||||
import { Checkbox } from "~/components/ui/checkbox";
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
|
@ -7,20 +7,17 @@ import {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "~/components/ui/dropdown-menu";
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
|
||||||
import { cn } from "~/utils";
|
import { cn } from "~/utils";
|
||||||
import type { FileItem } from "~/data/file-provider";
|
import type { FileItem } from "~/data/file-provider";
|
||||||
|
|
||||||
// This type is used to define the shape of our data.
|
declare module "@tanstack/table-core" {
|
||||||
// You can use a Zod schema here if you want.
|
interface TableMeta<TData extends RowData> {
|
||||||
export type File = {
|
hoveredRowId: string;
|
||||||
name: string;
|
}
|
||||||
cid: string;
|
}
|
||||||
size: string;
|
|
||||||
createdOn: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const columns: ColumnDef<FileItem>[] = [
|
export const columns: ColumnDef<FileItem>[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,14 +14,10 @@ import {
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import { Field } from "~/components/forms";
|
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="file-manager">
|
<Authenticated key="dashboard" v3LegacyAuthProviderCompatible>
|
||||||
<GeneralLayout>
|
<GeneralLayout>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
||||||
|
@ -70,62 +66,27 @@ export default function FileManager() {
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable columns={columns} />
|
||||||
columns={columns}
|
|
||||||
resource="file"
|
|
||||||
dataProviderName="files"
|
|
||||||
/>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<PinFilesForm />
|
<DialogHeader>
|
||||||
|
<DialogTitle>Pin Content</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form action="" className="w-full flex flex-col gap-y-4">
|
||||||
|
<Field
|
||||||
|
inputProps={{
|
||||||
|
name: "cids",
|
||||||
|
placeholder: "Comma separated CIDs",
|
||||||
|
}}
|
||||||
|
labelProps={{ htmlFor: "cids", children: "Content to Pin" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Pin Content
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</GeneralLayout>
|
</GeneralLayout>
|
||||||
</Authenticated>
|
</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 }) {
|
|
||||||
e.preventDefault();
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
"@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",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
|
||||||
"@lumeweb/portal-sdk": "0.0.0-20240321203634",
|
"@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",
|
||||||
|
@ -28,7 +27,6 @@
|
||||||
"@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-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@refinedev/cli": "^2.16.1",
|
"@refinedev/cli": "^2.16.1",
|
||||||
|
@ -39,7 +37,6 @@
|
||||||
"@refinedev/remix-router": "https://gitpkg.now.sh/LumeWeb/refine/packages/remix?remix",
|
"@refinedev/remix-router": "https://gitpkg.now.sh/LumeWeb/refine/packages/remix?remix",
|
||||||
"@remix-run/node": "^2.8.0",
|
"@remix-run/node": "^2.8.0",
|
||||||
"@remix-run/react": "^2.8.0",
|
"@remix-run/react": "^2.8.0",
|
||||||
"@tanstack/react-query": "^5.28.6",
|
|
||||||
"@tanstack/react-table": "^8.13.2",
|
"@tanstack/react-table": "^8.13.2",
|
||||||
"@uppy/core": "^3.9.3",
|
"@uppy/core": "^3.9.3",
|
||||||
"@uppy/tus": "^3.5.3",
|
"@uppy/tus": "^3.5.3",
|
||||||
|
|
Loading…
Reference in New Issue