micro/packages/web/src/pages/file/[fileId].tsx

164 lines
5.3 KiB
TypeScript

import { GetFileData } from "@ryanke/micro-api";
import classNames from "classnames";
import copyToClipboard from "copy-to-clipboard";
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
import { useRouter } from "next/router";
import { FC, ReactNode, useState } from "react";
import { Download, Share, Trash } from "react-feather";
import useSWR from "swr";
import { Container } from "../../components/container";
import { Embed } from "../../components/embed/embed";
import { PageLoader } from "../../components/page-loader";
import { Spinner } from "../../components/spinner";
import { Title } from "../../components/title";
import { downloadUrl } from "../../helpers/download.helper";
import { fetcher } from "../../helpers/fetcher.helper";
import { formatBytes } from "../../helpers/format-bytes.helper";
import { getErrorMessage } from "../../helpers/get-error-message.helper";
import { http, HTTPError } from "../../helpers/http.helper";
import { useToasts } from "../../hooks/use-toasts.helper";
import { useUser } from "../../hooks/use-user.helper";
import ErrorPage from "../_error";
export interface FileProps {
fallbackData: GetFileData;
}
const FileOption: FC<{ children: ReactNode; className?: string; onClick: () => void }> = ({
children,
className,
onClick,
}) => {
const classes = classNames(
"flex items-center gap-2 shrink-0 transition-colors duration-100 hover:text-gray-300",
className
);
return (
<span className={classes} onClick={onClick}>
{children}
</span>
);
};
export default function File({ fallbackData }: FileProps) {
const router = useRouter();
const fileId = router.query.fileId;
const file = useSWR<GetFileData>(router.query.fileId ? `file/${fileId}` : null, {
fallbackData: fallbackData,
revalidateOnMount: !fallbackData,
});
const user = useUser();
const [confirm, setConfirm] = useState(false);
const [deleting, setDeleting] = useState(false);
const setToast = useToasts();
if (file.error) {
return <ErrorPage status={file.error.status} message={file.error.text} />;
}
if (!file.data) {
return <PageLoader />;
}
const copyLink = () => {
copyToClipboard(window.location.href);
setToast({
text: `Copied link to clipboard`,
});
};
const downloadFile = async () => {
try {
await downloadUrl(file.data!.paths.direct, file.data!.displayName);
} catch (error: unknown) {
const message = getErrorMessage(error) ?? "An unknown error occurred";
setToast({ error: true, text: message });
}
};
const deleteFile = async () => {
if (!file.data) return;
if (!confirm) {
setConfirm(true);
return;
}
try {
setDeleting(true);
await http(`file/${file.data.id}`, {
method: "DELETE",
});
setDeleting(false);
setToast({ text: `Deleted "${file.data.displayName}"` });
router.replace("/dashboard");
} catch (error: unknown) {
const message = getErrorMessage(error) ?? "An unknown error occurred";
setToast({ error: true, text: message });
} finally {
setDeleting(false);
}
};
return (
<Container className="mt-5 md-2 md:mt-10 md:mb-5">
<Title>{file.data.displayName}</Title>
<div className="grid grid-cols-1 gap-4 md:grid-cols-6">
<div className="flex items-end col-span-5">
<h1 className="mr-2 text-xl font-bold truncate md:text-4xl md:break-all">{file.data.displayName}</h1>
<span className="text-xs text-gray-500">{formatBytes(file.data.size)}</span>
</div>
<Embed
data={{
type: file.data.type,
paths: file.data.paths,
size: file.data.size,
displayName: file.data.displayName,
height: file.data.metadata?.height,
width: file.data.metadata?.width,
}}
/>
<div className="flex md:flex-col">
<div className="flex text-sm gap-3 text-gray-500 cursor-pointer md:flex-col">
<FileOption onClick={copyLink}>
<Share className="h-4 mr-1" /> Copy link
</FileOption>
<FileOption onClick={downloadFile}>
<Download className="h-4 mr-1" /> Download
</FileOption>
{user.data?.id === file.data.owner?.id && (
<FileOption onClick={deleteFile} className="text-red-400 hover:text-red-500">
<Trash className="h-4 mr-1" />
{deleting ? <Spinner size="small" /> : confirm ? "Are you sure?" : "Delete"}
</FileOption>
)}
</div>
</div>
</div>
</Container>
);
}
export async function getServerSideProps(
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<FileProps>> {
// why on earth does nextjs, where the entire point is ssr, not have
// an easy way to handle basic errors like 404 when server-side rendering? why cant it just catch the error
// and pass it to my custom error page where i can have custom error handling?
// why do i have to manually handle the error each time?
// yes, im salty about it.
try {
const fallbackData = await fetcher<GetFileData>(`file/${context.query.fileId}`, context);
return { props: { fallbackData } };
} catch (error) {
if (error instanceof HTTPError) {
return { notFound: true };
}
throw error;
}
}