Compare commits

..

9 Commits

16 changed files with 795 additions and 361 deletions

View File

@ -18,16 +18,21 @@ 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: "file" resource,
dataProviderName: dataProviderName || "default"
} }
}) })

View File

@ -35,6 +35,8 @@ 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,
@ -48,116 +50,122 @@ 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 (
<div className="h-full flex flex-row"> <PinningProvider>
<header className="p-10 pr-0 flex flex-col w-[240px] h-full scroll-m-0 overflow-hidden"> <div className="h-full flex flex-row">
<img src={logoPng} alt="Lume logo" className="h-10 w-32" /> <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" />
<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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="border rounded-full h-auto p-2 gap-x-2 text-ring font-semibold">
<Avatar className="bg-ring h-7 w-7 rounded-full" />
{`${identity?.firstName} ${identity?.lastName}`}
<ChevronDownIcon />
</Button> </Button>
</DropdownMenuTrigger> </DialogTrigger>
<DropdownMenuContent align="end"> <DialogContent className="border rounded-lg p-8">
<DropdownMenuGroup> <UploadFileForm />
<DropdownMenuItem onClick={() => logout()}> </DialogContent>
<ExitIcon className="mr-2" /> </Dialog>
Logout </header>
</DropdownMenuItem> <div className="flex-1 overflow-y-auto p-10">
</DropdownMenuGroup> <div className="flex items-center gap-x-4 justify-end">
</DropdownMenuContent> <Button variant="ghost" className="rounded-full w-fit">
</DropdownMenu> <ThemeIcon className="text-ring" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="border rounded-full h-auto p-2 gap-x-2 text-ring font-semibold">
<Avatar className="bg-ring h-7 w-7 rounded-full" />
{`${identity?.firstName} ${identity?.lastName}`}
<ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => logout()}>
<ExitIcon className="mr-2" />
Logout
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</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>
{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>
</div> <PinningNetworkBanner />
</PinningProvider>
); );
}; };

View File

@ -0,0 +1,107 @@
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>
);
};

View File

@ -0,0 +1,58 @@
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,
};

View File

@ -0,0 +1,53 @@
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 };

View File

@ -1,12 +1,11 @@
import {SdkProvider} from "~/data/sdk-provider.js"; import type {SdkProvider} from "~/data/sdk-provider.js";
import {S5Client} from "@lumeweb/s5-js"; import type {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 {AxiosProgressEvent} from "axios"; import type {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();

View File

@ -1,11 +1,11 @@
interface PinningStatus { export 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>
class PinningProcess { export 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,13 +30,26 @@ class PinningProcess {
return { success: true, message: "Pinning process started" }; return { success: true, message: "Pinning process started" };
} }
static *pollProgress(id: string): Generator<PinningStatus | null, void, unknown> { static async unpin(id: string): Promise<{ success: boolean; message: string }> {
let status = PinningProcess.instances.get(id); if (!PinningProcess.instances.has(id)) {
while (status && status.status !== 'completed') { return { success: false, message: "ID not found or not being processed" };
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
} }
} }

View File

@ -1,4 +1,4 @@
import type {AuthProvider, DataProvider} from "@refinedev/core"; import type {AuthProvider} 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,7 +8,6 @@ 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;
} }

62
app/hooks/usePinning.ts Normal file
View File

@ -0,0 +1,62 @@
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,
};
};

View File

@ -0,0 +1,63 @@
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>
);
};

View File

@ -1,69 +1,82 @@
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, 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 {useMemo} from "react"; 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 },
]; ];
export function Layout({children}: { children: React.ReactNode }) { const queryClient = new QueryClient();
return (
<html lang="en"> export function Layout({ children }: { children: React.ReactNode }) {
<head> return (
<meta charSet="utf-8"/> <html lang="en">
<meta name="viewport" content="width=device-width, initial-scale=1"/> <head>
<Meta/> <meta charSet="utf-8" />
<Links/> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> <Meta />
<body className="overflow-hidden"> <Links />
</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 (
<Refine <QueryClientProvider client={queryClient}>
authProvider={providers.auth} <Refine
routerProvider={routerProvider} authProvider={providers.auth}
notificationProvider={notificationProvider} routerProvider={routerProvider}
dataProvider={providers} notificationProvider={notificationProvider}
resources={resources} dataProvider={{
options={{disableTelemetry: true}} default: providers.default,
> files: providers.files,
<Outlet/> }}
</Refine> resources={resources}
); 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>;
} }

View File

@ -3,7 +3,7 @@ import {Navigate} from "@remix-run/react";
export default function Index() { export default function Index() {
return ( return (
<Authenticated v3LegacyAuthProviderCompatible key={"index"} loading={ <Authenticated key={"index"} loading={
<>Checking Login Status</> <>Checking Login Status</>
}> }>
<Navigate to="/dashboard" replace/> <Navigate to="/dashboard" replace/>

View File

@ -3,11 +3,11 @@ import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { DialogClose } from "@radix-ui/react-dialog"; import { DialogClose } from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons"; import { Cross2Icon } from "@radix-ui/react-icons";
import { import {
Authenticated, Authenticated,
type BaseKey, 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,167 +54,175 @@ 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> />
</div> <h2 className="font-bold my-8">Account Management</h2>
<h2 className="font-bold my-8">Security</h2> <div className="grid grid-cols-3 gap-x-8">
<div className="grid grid-cols-3 gap-x-8"> <ManagementCard>
<ManagementCard> <ManagementCardAvatar
<ManagementCardTitle>Password</ManagementCardTitle> button={
<ManagementCardContent> <DialogTrigger
<PasswordDots className="mt-6" /> asChild
</ManagementCardContent> className="absolute bottom-0 right-0 z-50">
<ManagementCardFooter> <Button
<DialogTrigger asChild> onClick={() =>
<Button setModal({ ...openModal, changeAvatar: true })
className="h-12 gap-x-2" }
onClick={() => variant="outline"
setModal({ ...openModal, changePassword: true }) 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 />
Change Password Upgrade to Premium
</Button> </Button>
</DialogTrigger> </ManagementCardFooter>
</ManagementCardFooter> </ManagementCard>
</ManagementCard> </div>
<ManagementCard> <h2 className="font-bold my-8">Security</h2>
<ManagementCardTitle>Two-Factor Authentication</ManagementCardTitle> <div className="grid grid-cols-3 gap-x-8">
<ManagementCardContent> <ManagementCard>
Improve security by enabling 2FA. <ManagementCardTitle>Password</ManagementCardTitle>
</ManagementCardContent> <ManagementCardContent>
<ManagementCardFooter> <PasswordDots className="mt-6" />
<DialogTrigger asChild> </ManagementCardContent>
<Button <ManagementCardFooter>
className="h-12 gap-x-2" <DialogTrigger asChild>
onClick={() => <Button
setModal({ ...openModal, setupTwoFactor: true }) className="h-12 gap-x-2"
}> 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 />
Enable Two-Factor Authorization Send Invitation
</Button> </Button>
</DialogTrigger> </ManagementCardFooter>
</ManagementCardFooter> </ManagementCard>
</ManagementCard> <ManagementCard>
</div> <ManagementCardTitle>Read our Resources</ManagementCardTitle>
<h2 className="font-bold my-8">More</h2> <ManagementCardContent>
<div className="grid grid-cols-3 gap-x-8"> Navigate helpful articles or get assistance.
<ManagementCard variant="accent"> </ManagementCardContent>
<ManagementCardTitle>Invite a Friend</ManagementCardTitle> <ManagementCardFooter>
<ManagementCardContent> <Button className="h-12 gap-x-2">
Get 1 GB per friend invited for free (max 5 GB). <AddIcon />
</ManagementCardContent> Open Support Centre
<ManagementCardFooter> </Button>
<Button variant="accent" className="h-12 gap-x-2"> </ManagementCardFooter>
<AddIcon /> </ManagementCard>
Send Invitation <ManagementCard>
</Button> <ManagementCardTitle>Delete Account</ManagementCardTitle>
</ManagementCardFooter> <ManagementCardContent>
</ManagementCard> Once initiated, this action cannot be undone.
<ManagementCard> </ManagementCardContent>
<ManagementCardTitle>Read our Resources</ManagementCardTitle> <ManagementCardFooter>
<ManagementCardContent> <Button className="h-12 gap-x-2" variant="destructive">
Navigate helpful articles or get assistance. <AddIcon />
</ManagementCardContent> Delete my Account
<ManagementCardFooter> </Button>
<Button className="h-12 gap-x-2"> </ManagementCardFooter>
<AddIcon /> </ManagementCard>
Open Support Centre </div>
</Button> <DialogContent>
</ManagementCardFooter> {openModal.changeAvatar && <ChangeAvatarForm />}
</ManagementCard> {openModal.changeEmail && (
<ManagementCard> <ChangeEmailForm currentValue={identity?.email || ""} />
<ManagementCardTitle>Delete Account</ManagementCardTitle> )}
<ManagementCardContent> {openModal.changePassword && <ChangePasswordForm />}
Once initiated, this action cannot be undone. {openModal.setupTwoFactor && <SetupTwoFactorDialog />}
</ManagementCardContent> </DialogContent>
<ManagementCardFooter> </Dialog>
<Button className="h-12 gap-x-2" variant="destructive"> </GeneralLayout>
<AddIcon /> </Authenticated>
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>
); );
} }
@ -252,7 +260,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(),
@ -313,7 +321,8 @@ const ChangePasswordSchema = z
}); });
const ChangePasswordForm = () => { const ChangePasswordForm = () => {
const { mutate: updatePassword } = useUpdatePassword<UpdatePasswordFormRequest>(); const { mutate: updatePassword } =
useUpdatePassword<UpdatePasswordFormRequest>();
const [form, fields] = useForm({ const [form, fields] = useForm({
id: "login", id: "login",
constraint: getZodConstraint(ChangePasswordSchema), constraint: getZodConstraint(ChangePasswordSchema),
@ -327,8 +336,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(),
}); });
}, },
}); });

View File

@ -1,5 +1,5 @@
import { DrawingPinIcon, TrashIcon } from "@radix-ui/react-icons"; import { TrashIcon } from "@radix-ui/react-icons";
import type { ColumnDef, RowData } 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,17 +7,20 @@ 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";
declare module "@tanstack/table-core" { // This type is used to define the shape of our data.
interface TableMeta<TData extends RowData> { // You can use a Zod schema here if you want.
hoveredRowId: string; export type File = {
} name: string;
} cid: string;
size: string;
createdOn: string;
};
export const columns: ColumnDef<FileItem>[] = [ export const columns: ColumnDef<FileItem>[] = [
{ {

View File

@ -14,10 +14,14 @@ 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="dashboard" v3LegacyAuthProviderCompatible> <Authenticated key="file-manager">
<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>
@ -66,27 +70,62 @@ export default function FileManager() {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</div> </div>
<DataTable columns={columns} /> <DataTable
columns={columns}
resource="file"
dataProviderName="files"
/>
<DialogContent> <DialogContent>
<DialogHeader> <PinFilesForm />
<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>
</>
);
};

View File

@ -16,6 +16,7 @@
"@conform-to/react": "^1.0.2", "@conform-to/react": "^1.0.2",
"@conform-to/zod": "^1.0.2", "@conform-to/zod": "^1.0.2",
"@fontsource-variable/manrope": "^5.0.19", "@fontsource-variable/manrope": "^5.0.19",
"@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",
@ -27,6 +28,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-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",
@ -37,6 +39,7 @@
"@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",