portal-dashboard/app/components/general-layout.tsx

367 lines
11 KiB
TypeScript
Raw Normal View History

2024-03-14 01:55:44 +00:00
import { Button } from "~/components/ui/button";
import logoPng from "~/images/lume-logo.png?url";
import lumeColorLogoPng from "~/images/lume-color-logo.png?url";
import discordLogoPng from "~/images/discord-logo.png?url";
import { Link, useLocation } from "@remix-run/react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
2024-03-14 01:55:44 +00:00
DialogTrigger,
} from "~/components/ui/dialog";
import { useUppy } from "./lib/uppy";
2024-03-26 13:42:18 +00:00
import type { FailedUppyFile, UppyFile } from "@uppy/core";
2024-03-14 01:55:44 +00:00
import { Progress } from "~/components/ui/progress";
import { DialogClose } from "@radix-ui/react-dialog";
import { ChevronDownIcon, ExitIcon, TrashIcon } from "@radix-ui/react-icons";
import {
ClockIcon,
DriveIcon,
CircleLockIcon,
CloudUploadIcon,
CloudCheckIcon,
BoxCheckedIcon,
PageIcon,
2024-03-14 01:55:44 +00:00
ThemeIcon,
2024-03-26 13:42:18 +00:00
ExclamationCircleIcon,
2024-03-14 01:55:44 +00:00
} from "./icons";
2024-03-22 13:14:45 +00:00
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
} from "./ui/dropdown-menu";
2024-03-14 01:55:44 +00:00
import { Avatar } from "@radix-ui/react-avatar";
import { cn } from "~/utils";
import { useGetIdentity, useLogout } from "@refinedev/core";
import { PinningNetworkBanner } from "./pinning-network-banner";
import { PinningProvider } from "~/providers/PinningProvider";
import type { Identity } from "~/data/auth-provider";
2024-03-22 13:14:45 +00:00
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "./ui/tooltip";
2024-03-27 06:02:21 +00:00
import filesize from "./lib/filesize";
export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
const location = useLocation();
const { data: identity } = useGetIdentity<Identity>();
2024-03-22 13:14:45 +00:00
const { mutate: logout } = useLogout();
return (
<PinningProvider>
<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">
<img src={logoPng} alt="Lume logo" className="h-10 w-32" />
2024-03-14 01:55:44 +00:00
<nav className="my-10 flex-1">
<ul>
<li>
<Link to="/dashboard">
<NavigationButton
active={location.pathname.includes("dashboard")}>
<ClockIcon className="w-5 h-5 mr-2" />
Dashboard
</NavigationButton>
</Link>
</li>
<li>
<Link to="/file-manager">
<NavigationButton
active={location.pathname.includes("file-manager")}>
<DriveIcon className="w-5 h-5 mr-2" />
File Manager
</NavigationButton>
</Link>
</li>
<li>
<Link to="/account">
<NavigationButton
active={location.pathname.includes("account")}>
<CircleLockIcon className="w-5 h-5 mr-2" />
Account
</NavigationButton>
</Link>
</li>
</ul>
</nav>
<span className="text-primary-2 mb-3 -space-y-1 opacity-40">
<p>Freedom</p>
<p>Privacy</p>
<p>Ownership</p>
</span>
<Dialog>
<DialogTrigger asChild>
<Button size={"lg"} className="w-[calc(100%-3rem)] font-semibold">
<CloudUploadIcon className="w-5 h-5 mr-2" />
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>
2024-03-07 17:26:11 +00:00
<DropdownMenu>
2024-03-22 18:33:30 +00:00
<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>
<PinningNetworkBanner />
</PinningProvider>
2024-03-14 01:55:44 +00:00
);
};
2024-03-07 17:26:11 +00:00
2024-03-12 14:32:00 +00:00
const UploadFileForm = () => {
const {
getRootProps,
getInputProps,
getFiles,
upload,
state,
removeFile,
2024-03-14 01:55:44 +00:00
cancelAll,
2024-03-26 13:42:18 +00:00
failedFiles,
} = useUppy();
2024-03-12 14:32:00 +00:00
2024-03-14 01:55:44 +00:00
const isUploading = state === "uploading";
const isCompleted = state === "completed";
2024-03-26 13:42:18 +00:00
const hasErrored = state === "error";
2024-03-14 01:55:44 +00:00
const hasStarted = state !== "idle" && state !== "initializing";
2024-03-26 13:42:18 +00:00
const getFailedState = (id: string) =>
failedFiles.find((file) => file.id === id);
return (
2024-03-12 14:32:00 +00:00
<>
<DialogHeader className="mb-6">
<DialogTitle>Upload Files</DialogTitle>
</DialogHeader>
{!hasStarted ? (
<div
{...getRootProps()}
2024-03-14 01:55:44 +00:00
className="border border-border rounded text-primary-2 bg-primary-dark h-48 flex flex-col items-center justify-center">
2024-03-12 14:32:00 +00:00
<input
hidden
aria-hidden
name="uppyFiles[]"
key={new Date().toISOString()}
multiple
{...getInputProps()}
/>
<CloudUploadIcon className="w-24 h-24 stroke stroke-primary-dark" />
<p>Drag & Drop Files or Browse</p>
</div>
) : null}
<div className="w-full space-y-3 max-h-48 overflow-y-auto">
{getFiles().map((file) => (
<UploadFileItem
key={file.id}
file={file}
onRemove={(id) => {
2024-03-14 01:55:44 +00:00
removeFile(id);
2024-03-12 14:32:00 +00:00
}}
2024-03-26 13:42:18 +00:00
failedState={getFailedState(file.id)}
2024-03-12 14:32:00 +00:00
/>
))}
</div>
2024-03-27 06:02:21 +00:00
2024-03-26 13:42:18 +00:00
{hasErrored ? (
<div className="text-red-500">
<p>An error occurred</p>
</div>
) : null}
2024-03-26 13:42:18 +00:00
{hasStarted && !hasErrored ? (
2024-03-12 14:32:00 +00:00
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" />
{isCompleted
? "Upload completed"
: `${getFiles().length} files being uploaded`}
</div>
2024-03-12 14:32:00 +00:00
) : null}
2024-03-12 14:32:00 +00:00
{isUploading ? (
<DialogClose asChild onClick={cancelAll}>
<Button size={"lg"} className="mt-6">
Cancel
</Button>
</DialogClose>
) : null}
2024-03-12 14:32:00 +00:00
{isCompleted ? (
<DialogClose asChild>
<Button size={"lg"} className="mt-6">
Close
</Button>
2024-03-12 14:32:00 +00:00
</DialogClose>
) : null}
2024-03-12 14:32:00 +00:00
{!hasStarted && !isCompleted && !isUploading ? (
<Button size={"lg"} className="mt-6" onClick={upload}>
Upload
</Button>
) : null}
</>
2024-03-14 01:55:44 +00:00
);
};
function bytestoMegabytes(bytes: number) {
2024-03-14 01:55:44 +00:00
return bytes / 1024 / 1024;
}
const UploadFileItem = ({
file,
2024-03-26 13:42:18 +00:00
failedState,
2024-03-14 01:55:44 +00:00
onRemove,
}: {
2024-03-14 01:55:44 +00:00
file: UppyFile;
2024-03-26 13:42:18 +00:00
failedState?: FailedUppyFile<Record<string, any>, Record<string, any>>;
2024-03-14 01:55:44 +00:00
onRemove: (id: string) => void;
}) => {
return (
<div className="flex flex-col w-full py-4 px-2 bg-primary-dark">
2024-03-26 13:42:18 +00:00
<div
className={`flex items-center justify-between ${
failedState ? "text-red-500" : "text-primary-1"
}`}>
<div className="flex items-center">
<div className="p-2">
{file.progress?.uploadComplete ? (
<BoxCheckedIcon className="w-4 h-4" />
2024-03-26 13:42:18 +00:00
) : failedState?.error ? (
<ExclamationCircleIcon className="w-4 h-4" />
) : (
<PageIcon className="w-4 h-4" />
)}
</div>
2024-03-22 13:14:45 +00:00
<TooltipProvider>
<Tooltip delayDuration={500}>
<TooltipTrigger>
<p className="w-full flex justify-between items-center">
<span className="truncate text-ellipsis max-w-[20ch]">
{file.name}
</span>{" "}
2024-03-27 06:02:21 +00:00
<span>({filesize(file.size, 2)})</span>
2024-03-22 13:14:45 +00:00
</p>
</TooltipTrigger>
<TooltipContent>
<p>
2024-03-27 06:02:21 +00:00
{file.name} ({filesize(file.size, 2)})
2024-03-22 13:14:45 +00:00
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button
size={"icon"}
variant={"ghost"}
className="!text-inherit"
2024-03-14 01:55:44 +00:00
onClick={() => onRemove(file.id)}>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
2024-03-26 13:42:18 +00:00
{failedState ? (
<div className="mt-2 text-red-500 text-sm">
<p>Error uploading: {failedState.error}</p>
<div className="flex gap-2">
<Button
size={"sm"}
onClick={() => {
/* Retry upload function here */
}}>
Retry
</Button>
<Button
size={"sm"}
variant={"outline"}
onClick={() => onRemove(file.id)}>
Remove
</Button>
</div>
</div>
) : null}
{file.progress?.uploadStarted && !file.progress.uploadComplete ? (
<Progress max={100} value={file.progress.percentage} className="mt-2" />
) : null}
</div>
2024-03-14 01:55:44 +00:00
);
};
2024-03-14 01:55:44 +00:00
const NavigationButton = ({
children,
active,
}: React.PropsWithChildren<{ active?: boolean }>) => {
2024-03-07 17:26:11 +00:00
return (
2024-03-14 01:55:44 +00:00
<Button
variant="ghost"
className={cn(
"justify-start h-14 w-full font-semibold",
active && "bg-secondary-1 text-secondary-1-foreground",
)}>
2024-03-07 17:26:11 +00:00
{children}
</Button>
2024-03-14 01:55:44 +00:00
);
};