Finalize basic function

This commit is contained in:
mbaharip
2023-05-08 10:07:03 +07:00
parent 714f1d1a92
commit 153047de55
31 changed files with 627 additions and 853 deletions
+3 -1
View File
@@ -58,7 +58,9 @@ Since Google Drive direct download need the file to be public, I implement the `
- [x] Fetch file or folder `700~1400ms`
- [x] Fetch breadcrumb `900~1500ms`
- [x] Fetch readme `1100~1200ms`
- [x] Fetch banner `500-900ms`
- [ ] Fetch readme from specific folder `1200~1600ms`
- [x] Fetch banner `500-800ms`
- [x] Fetch banner from specific folder `500~800ms`
- [ ] Protect folder
### Fetch files `/api/files` | `14/17 Completed`
+11 -5
View File
@@ -9,21 +9,27 @@ export default function ErrorFeedback({ message }: Props) {
Please check the following details and try again:`;
return (
<div className={"flex flex-col"}>
<div className={"mx-auto flex items-center justify-center gap-4"}>
<div className={"flex w-full flex-col"}>
<div
className={
"mx-auto mb-2 flex items-center justify-center gap-4 max-tablet:flex-col"
}
>
<MdWarning className={"h-8 w-8 text-red-500 dark:text-red-400"} />
<p className={"text-start"}>{errorMessage}</p>
</div>
<div className={"divider-horizontal"} />
<div className={"divider-horizontal mx-auto max-w-screen-md"} />
<div
className={
"mx-auto flex w-full max-w-[50%] flex-col items-start justify-center"
"mx-auto mt-2 flex w-full flex-col items-start justify-center"
}
>
<span>Error details:</span>
<pre className={"w-full py-2"}>
<code>{message || "Internal server error"}</code>
<code className={"whitespace-pre-wrap"}>
{message || "Internal server error"}
</code>
</pre>
</div>
</div>
+28 -14
View File
@@ -2,7 +2,6 @@ import { BreadCrumbsResponse, TFileParent } from "types/googleapis";
import Link from "next/link";
import { Fragment } from "react";
import { MdHome } from "react-icons/md";
import ReactLoading from "react-loading";
import siteConfig from "config/site.config";
type Props = {
@@ -11,12 +10,14 @@ type Props = {
};
export default function Breadcrumb({ data, isLoading }: Props) {
const reverseBreadcrumbs = data?.breadcrumbs?.slice().reverse();
return (
<>
{isLoading ? (
<div className='flex h-[1.5rem] w-32 animate-pulse items-center gap-2 rounded bg-zinc-300 dark:bg-zinc-600' />
) : (
<div className={"flex items-center gap-2"}>
<div className={"flex items-center gap-1"}>
{/* Home */}
<Link
href={"/"}
@@ -27,22 +28,35 @@ export default function Breadcrumb({ data, isLoading }: Props) {
</Link>
{data?.isLimitReached && (
<Fragment>
<span>...</span>
<span>{siteConfig.breadcrumb.limiter}</span>
<span>...</span>
</Fragment>
)}
{data?.breadcrumbs.map((parent: TFileParent, index: number) => (
<Link
href={`/folder/${parent.id}`}
key={index}
className={"flex items-center gap-1"}
>
<span>{parent.name}</span>
{index !== data.breadcrumbs.length - 1 && (
<div
className={
"line-clamp-1 flex w-auto flex-grow-0 items-center gap-1 overflow-hidden break-words break-all"
}
>
{reverseBreadcrumbs?.map((parent: TFileParent, index: number) => (
<Fragment key={index}>
<span>{siteConfig.breadcrumb.limiter}</span>
)}
</Link>
))}
<Link
href={`/folder/${parent.id}`}
key={index}
>
<span
className={`${
index === reverseBreadcrumbs.length - 1
? "line-clamp-1 w-auto flex-grow-0 overflow-hidden break-words font-bold"
: "whitespace-nowrap"
}`}
>
{parent.name}
</span>
</Link>
</Fragment>
))}
</div>
</div>
)}
</>
+1 -1
View File
@@ -38,7 +38,7 @@ export default function GridFile({ data }: Props) {
{/* Thumbnail */}
<div
className={`relative mx-auto grid h-40 w-full place-items-center overflow-hidden rounded-lg tablet:h-32 tablet:rounded-xl ${
!allowThumbnail && "border border-zinc-700"
!allowThumbnail && "border border-zinc-300 dark:border-zinc-700"
}`}
>
{data.thumbnailLink && allowThumbnail ? (
@@ -2,10 +2,9 @@ import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import { useEffect, useState } from "react";
import config from "config/site.config";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import H5AudioPlayer from "react-h5-audio-player";
import "react-h5-audio-player/lib/styles.css";
import SWRLayout from "components/layout/SWRLayout";
type Props = {
data: TFile | drive_v3.Schema$File;
@@ -13,13 +12,11 @@ type Props = {
export default function AudioPreview({ data }: Props) {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>("");
const [audioSrc, setAudioSrc] = useState<string>("");
useEffect(() => {
const timeout = setTimeout(() => {
setIsError(true);
setErrorMessage("Audio took too long to load");
setIsLoading(false);
}, config.preview.timeout);
@@ -32,7 +29,6 @@ export default function AudioPreview({ data }: Props) {
clearTimeout(timeout);
};
audio.onerror = (error: any) => {
setIsError(true);
setErrorMessage(error.message);
setIsLoading(false);
clearTimeout(timeout);
@@ -45,17 +41,11 @@ export default function AudioPreview({ data }: Props) {
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading audio preview..."}
useContainer={false}
/>
) : isError ? (
<ErrorFeedback
message={errorMessage}
useContainer={false}
/>
) : (
<SWRLayout
data={data}
error={errorMessage}
isLoading={isLoading}
>
<div className={"preview-audio w-full"}>
<H5AudioPlayer
src={audioSrc}
@@ -64,7 +54,7 @@ export default function AudioPreview({ data }: Props) {
customAdditionalControls={[]}
/>
</div>
)}
</SWRLayout>
</div>
);
}
@@ -1,32 +1,24 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import { useEffect, useState } from "react";
import useSWR from "swr";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import MarkdownRender from "components/utility/MarkdownRender";
import fetcher from "utils/swrFetch";
import { getCodeLanguage } from "utils/mimeTypesHelper";
import { createFileId } from "utils/driveHelper";
import SWRLayout from "components/layout/SWRLayout";
type Props = {
data: TFile | drive_v3.Schema$File;
data: drive_v3.Schema$File;
};
export default function CodePreview({ data }: Props) {
const [codeContent, setCodeContent] = useState<string>("");
const fileId = createFileId(data);
const {
data: swrData,
error,
isLoading,
} = useSWR(`/download/${data.id}/${data.name}`, fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
revalidateIfStale: true,
});
error,
} = useSWR(`/api/files/${fileId}?download=1`);
useEffect(() => {
if (swrData) {
@@ -41,21 +33,15 @@ export default function CodePreview({ data }: Props) {
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading code preview..."}
useContainer={false}
/>
) : error ? (
<ErrorFeedback
message={error.message || "Failed to load code"}
useContainer={false}
/>
) : (
<SWRLayout
data={codeContent}
error={error}
isLoading={isLoading}
>
<div className={"w-full"}>
<MarkdownRender content={codeContent} />
</div>
)}
</SWRLayout>
</div>
);
}
@@ -1,10 +1,9 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import { useEffect, useState } from "react";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import config from "config/site.config";
import { reverseString } from "utils/hashHelper";
import SWRLayout from "components/layout/SWRLayout";
type Props = {
data: TFile | drive_v3.Schema$File;
@@ -13,13 +12,11 @@ type Props = {
export default function ImagePreview({ data, hash }: Props) {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>("");
const [imageSrc, setImageSrc] = useState<string>("");
useEffect(() => {
const timeout = setTimeout(() => {
setIsError(true);
setErrorMessage("Image took too long to load");
setIsLoading(false);
}, config.preview.timeout);
@@ -31,7 +28,6 @@ export default function ImagePreview({ data, hash }: Props) {
clearTimeout(timeout);
};
image.onerror = (error: any) => {
setIsError(true);
setErrorMessage(error.message);
setIsLoading(false);
clearTimeout(timeout);
@@ -44,23 +40,17 @@ export default function ImagePreview({ data, hash }: Props) {
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading image preview..."}
useContainer={false}
/>
) : isError ? (
<ErrorFeedback
message={errorMessage}
useContainer={false}
/>
) : (
<SWRLayout
data={data}
error={errorMessage}
isLoading={isLoading}
>
<img
src={imageSrc}
alt={(data.name as string) || reverseString(hash as string)}
className='max-h-full max-w-full rounded-lg'
/>
)}
</SWRLayout>
</div>
);
}
@@ -1,47 +1,32 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import useSWR from "swr";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import MarkdownRender from "components/utility/MarkdownRender";
import fetcher from "utils/swrFetch";
import { createFileId } from "utils/driveHelper";
import SWRLayout from "components/layout/SWRLayout";
type Props = {
data: TFile | drive_v3.Schema$File;
data: drive_v3.Schema$File;
};
export default function MarkdownPreview({ data }: Props) {
const fileId = createFileId(data);
const {
data: swrData,
error,
isLoading,
} = useSWR(`/download/${data.id}/${data.name}`, fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
revalidateIfStale: true,
});
} = useSWR(`/api/files/${fileId}?download=1`);
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading code preview..."}
useContainer={false}
/>
) : error ? (
<ErrorFeedback
message={error.message || "Failed to load code"}
useContainer={false}
/>
) : (
<SWRLayout
data={swrData}
error={error}
isLoading={isLoading}
>
<div className={"w-full"}>
<MarkdownRender content={swrData as string} />
</div>
)}
</SWRLayout>
</div>
);
}
@@ -17,10 +17,7 @@ export default function ModelPreview({ data }: Props) {
if (!getLoader) {
return (
<div className='flex w-full items-center justify-center'>
<ErrorFeedback
message={"Failed to load model"}
useContainer={false}
/>
<ErrorFeedback message={"Failed to load model"} />
</div>
);
}
+15 -25
View File
@@ -1,28 +1,24 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import { useState } from "react";
import config from "config/site.config";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import { createFileId } from "utils/driveHelper";
import SWRLayout from "components/layout/SWRLayout";
type Props = {
data: TFile | drive_v3.Schema$File;
data: drive_v3.Schema$File;
};
export default function PDFPreview({ data }: Props) {
const fileId = createFileId(data);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>("");
const fileURL = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${data.id}/download`;
const fileURL = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`;
let providerURL;
switch (config.preview.pdfProvider) {
case "google":
providerURL = `https://drive.google.com/viewerng/viewer?embedded=true&url=${fileURL}`;
break;
case "microsoft":
providerURL = `https://view.officeapps.live.com/op/embed.aspx?src=${fileURL}`;
break;
case "mozilla":
providerURL = `https://mozilla.github.io/pdf.js/web/viewer.html?file=${fileURL}`;
break;
@@ -33,34 +29,28 @@ export default function PDFPreview({ data }: Props) {
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading PDF preview..."}
useContainer={false}
/>
) : isError ? (
<ErrorFeedback
message={errorMessage}
useContainer={false}
/>
) : (
<SWRLayout
data={data}
error={errorMessage}
isLoading={isLoading}
>
<></>
)}
</SWRLayout>
<div
className={`${
isLoading || isError ? "hidden" : "flex"
} mx-auto min-h-[70vh] w-full overflow-hidden rounded-lg`}
isLoading ? "hidden" : "block"
} mx-auto h-full w-full overflow-hidden rounded-lg`}
>
<iframe
width={"100%"}
className={"h-auto"}
height={"100%"}
className={"h-[50vh] tablet:h-[75vh]"}
src={providerURL}
title={data.name as string}
onLoad={() => {
setIsLoading(false);
}}
onError={(error: any) => {
setIsError(true);
setErrorMessage(error.message);
setIsLoading(false);
}}
@@ -1,47 +1,32 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import useSWR from "swr";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import MarkdownRender from "components/utility/MarkdownRender";
import fetcher from "utils/swrFetch";
import { createFileId } from "utils/driveHelper";
import SWRLayout from "components/layout/SWRLayout";
type Props = {
data: TFile | drive_v3.Schema$File;
data: drive_v3.Schema$File;
};
export default function TextPreview({ data }: Props) {
const fileId = createFileId(data);
const {
data: swrData,
error,
isLoading,
} = useSWR(`/download/${data.id}/${data.name}`, fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
revalidateIfStale: true,
});
} = useSWR(`/api/files/${fileId}?download=1`);
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading code preview..."}
useContainer={false}
/>
) : error ? (
<ErrorFeedback
message={error.message || "Failed to load code"}
useContainer={false}
/>
) : (
<SWRLayout
data={swrData}
error={error}
isLoading={isLoading}
>
<div className={"w-full"}>
<MarkdownRender content={swrData as string} />
</div>
)}
</SWRLayout>
</div>
);
}
@@ -1,51 +1,22 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import ReactPlayer from "react-player";
import { useState } from "react";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import { createFileId } from "utils/driveHelper";
type Props = {
data: TFile | drive_v3.Schema$File;
data: drive_v3.Schema$File;
};
export default function VideoPreview({ data }: Props) {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>("");
const fileId = createFileId(data);
return (
<div className='flex w-full items-center justify-center'>
{isLoading ? (
<LoadingFeedback
message={"Loading video preview..."}
useContainer={false}
/>
) : isError ? (
<ErrorFeedback
message={errorMessage}
useContainer={false}
/>
) : (
<></>
)}
<div
className={`${
isLoading || isError ? "hidden" : "block"
} aspect-video h-full max-h-[70vh] w-full`}
>
<div className={`aspect-video h-full max-h-[70vh] w-full`}>
<ReactPlayer
url={`/download/${data.id}/${data.name}`}
url={`/api/files/${fileId}?download=1`}
controls={true}
width='100%'
height='100%'
onReady={() => {
setIsLoading(false);
}}
onError={(error) => {
setIsError(true);
setErrorMessage(error.message);
}}
/>
</div>
</div>
@@ -8,9 +8,14 @@ import { BreadCrumbsResponse } from "types/googleapis";
type Props = {
children: ReactNode;
fileId: string;
renderSwitchLayout?: boolean;
};
export default function DefaultLayout({ children, fileId }: Props) {
export default function DefaultLayout({
children,
fileId,
renderSwitchLayout = true,
}: Props) {
const { data, isLoading } = useSWR<BreadCrumbsResponse>(
`/api/files/${fileId}/getPath`,
fetcher,
@@ -18,12 +23,12 @@ export default function DefaultLayout({ children, fileId }: Props) {
return (
<div className={"mx-auto flex max-w-screen-xl flex-col gap-2"}>
<div className={"flex items-center justify-between"}>
<div className={"flex w-full items-center justify-between gap-2"}>
<Breadcrumb
data={fileId === "root" ? undefined : data}
isLoading={isLoading}
/>
<SwitchLayout />
{renderSwitchLayout && <SwitchLayout />}
</div>
{children}
@@ -1,68 +0,0 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import { MdCopyAll, MdDownload } from "react-icons/md";
import Link from "next/link";
import config from "config/site.config";
import { useEffect, useState } from "react";
import useCopyText from "hooks/useCopyText";
type Props = {
data: TFile | drive_v3.Schema$File;
hash?: string;
};
export default function DetailsButtons({ data, hash }: Props) {
const [downloadURL, setDownloadURL] = useState<string>("");
const [viewURL, setViewURL] = useState<string>("");
const copyText = useCopyText();
useEffect(() => {
setDownloadURL(`/download/${data.id}/${data.name}`);
setViewURL(`${window.location.host}/media/${data.id}/${data.name}`);
}, [data]);
return (
<div className={"flex flex-col gap-2"}>
<Link
href={downloadURL}
target={"_blank"}
rel={"noopener noreferrer"}
>
<button
className={
"primary flex w-full items-center justify-center gap-2 py-4 tablet:py-2"
}
>
<MdDownload />
Download file
</button>
</Link>
<button
className={`flex w-full items-center justify-center gap-2 py-4 tablet:py-2 ${
config.files.allowDownloadProtectedWithoutAccess
? "secondary"
: "danger"
}`}
onClick={async (e) => {
e.preventDefault();
copyText(viewURL);
}}
>
<MdCopyAll />
Copy direct link
</button>
{hash && !config.files.allowDownloadProtectedWithoutAccess && (
<div className={"banner warning text-sm"}>
<div className={"flex flex-col gap-2"}>
<div className={"font-bold"}>
Copying the direct link will also copy the access token.
</div>
<p className={"text-sm"}>
Only share the direct link with people you trust.
</p>
</div>
</div>
)}
</div>
);
}
-134
View File
@@ -1,134 +0,0 @@
import { TFile } from "types/googleapis";
import { drive_v3 } from "googleapis";
import LoadingFeedback from "components/APIFeedback/Loading";
import { formatBytes, formatDate, formatDuration } from "utils/formatHelper";
import { useEffect, useState } from "react";
import DetailsButtons from "components/layout/FileDetails/DetailsButtons";
import { getFilePreview } from "utils/mimeTypesHelper";
type Props = {
data: TFile | drive_v3.Schema$File;
hash?: string;
};
export default function FileDetails({ data, hash }: Props) {
const [metadata, setMetadata] = useState<{ label: string; value: string }[]>(
[],
);
const [PreviewComponent, setPreviewComponent] = useState<JSX.Element | null>(
null,
);
useEffect(() => {
const Preview = getFilePreview(
data.fileExtension as string,
data.mimeType as string,
);
setPreviewComponent(
<Preview
data={data}
hash={hash || ""}
/>,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (data) {
const _metadata = [
{
label: "File name",
value: (data.name as string).split(
`.${data.fileExtension as string}`,
)[0],
},
{ label: "Size", value: formatBytes(data.size as string) },
{ label: "Type", value: data.mimeType as string },
{
label: "Created",
value: formatDate(new Date(data.createdTime as string)),
},
{
label: "Modified",
value: formatDate(new Date(data.modifiedTime as string)),
},
];
// Insert at index 3
if (
data.imageMediaMetadata &&
!(data.mimeType as string).endsWith("svg+xml")
) {
const { width, height } = data.imageMediaMetadata;
_metadata.splice(3, 0, {
label: "Dimension",
value: `${width || 0}px x ${height || 0}px`,
});
}
if (data.videoMediaMetadata) {
const { width, height, durationMillis } = data.videoMediaMetadata;
_metadata.splice(3, 0, {
label: "Dimension",
value: `${width || 0}px x ${height || 0}px`,
});
_metadata.splice(4, 0, {
label: "Duration",
value: formatDuration(durationMillis as string),
});
}
setMetadata(_metadata);
}
}, [data]);
return (
<>
{!data ? (
<LoadingFeedback message={"Loading file details..."} />
) : (
<div className={"grid grid-cols-1 gap-4 tablet:grid-cols-4"}>
<div className={"card h-fit tablet:col-span-3"}>
<div className='flex w-full items-center justify-between rounded-lg'>
<span className='font-bold'>Preview</span>
</div>
<div className={"divider-horizontal"} />
{PreviewComponent}
</div>
<div
className={
"col-span-1 flex h-fit flex-col gap-2 tablet:sticky tablet:top-16"
}
>
<div className={"card"}>
<div className='flex w-full items-center justify-between rounded-lg'>
<span className='font-bold'>Details</span>
</div>
<div className={"divider-horizontal"} />
<div className='flex w-full flex-col items-center justify-center gap-4 px-2'>
{metadata.map((item, index) => (
<div
className='flex w-full flex-col justify-center'
key={index}
>
<span className='font-bold text-inherit'>{item.label}</span>
<span className='whitespace-pre-wrap break-words text-inherit'>
{item.value}
</span>
</div>
))}
</div>
</div>
<div className={"card"}>
<DetailsButtons
data={data}
hash={hash || ""}
/>
</div>
</div>
</div>
)}
</>
);
}
+11 -6
View File
@@ -21,13 +21,18 @@ export default function Readme({
}
}, [isReadmeExist, isReadmeLoading, readmeData]);
if (!isMounted) return <></>;
return (
<div className={"card"}>
{isReadmeLoading && <LoadingFeedback useContainer={false} />}
{!isReadmeLoading && readmeData && (
<MarkdownRender content={readmeData} />
<>
{isReadmeLoading && (
<div className={"card"}>
<LoadingFeedback />
</div>
)}
</div>
{!isReadmeLoading && readmeData && isMounted && (
<div className={"card"}>
<MarkdownRender content={readmeData} />
</div>
)}
</>
);
}
+11 -3
View File
@@ -20,9 +20,17 @@ export default function SWRLayout({ data, error, isLoading, children }: Props) {
return (
<>
{isLoading && <LoadingFeedback />}
{!isLoading && error && <ErrorFeedback />}
{isMounted && <>{children}</>}
{isLoading && (
<div className={"card"}>
<LoadingFeedback />
</div>
)}
{!isLoading && error && (
<div className={"card"}>
<ErrorFeedback />
</div>
)}
{!isLoading && isMounted && <>{children}</>}
</>
);
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default function SwitchLayout() {
return (
<div
className={"relative flex w-fit flex-col"}
className={"relative flex w-fit flex-shrink-0 flex-grow-0 flex-col"}
ref={dropdownRef}
>
<div
+1 -1
View File
@@ -41,7 +41,7 @@ export default function useLocalStorage<T>(key: string, initialValue: T) {
const newValue = JSON.parse(event.newValue);
setStoredValue(newValue);
} catch (error) {
console.log(error);
console.error("Error setting localStorage:", error);
}
}
};
+7 -19
View File
@@ -30,28 +30,16 @@ export default initMiddleware(async function handler(
responseTime: Date.now() - _start,
};
const findFolder = await driveClient.files.list({
q: `name = '${name}' and trashed = false and 'me' in owners`,
fields: "files(id, name, mimeType)",
const findBanner = await driveClient.files.list({
q: `name contains '.banner' and trashed = false and 'me' in owners`,
fields: "files(id, name, mimeType, parents)",
});
const folder = findFolder.data.files?.find(
const banner = findBanner.data.files?.find(
(file) =>
file.name === decodeURIComponent(name) &&
(file.id as string).startsWith(partialId),
file.parents?.[0].startsWith(partialId) &&
file.name?.startsWith(".banner"),
);
if (!folder) {
throw new ExtendedError("Folder not found.", 404, "notFound");
}
const listFiles = await driveClient.files.list({
q: `'${folder.id}' in parents and trashed = false and 'me' in owners`,
fields: "files(id, name, mimeType)",
});
const banner = listFiles.data.files?.filter((file) =>
file.name?.startsWith(".banner"),
)[0];
if (!banner || !banner.mimeType?.startsWith("image")) {
if (!banner) {
payload.success = false;
return response.status(200).json(payload);
}
+8 -15
View File
@@ -12,27 +12,20 @@ export default initMiddleware(async function handler(
const _start = Date.now();
try {
const query: string[] = [
"name contains '.banner'",
`parents = '${apiConfig.files.rootFolder}'`,
"trashed = false",
"'me' in owners",
];
const getRootBanner = await driveClient.files.list({
q: query.join(" and "),
fields: "files(id, name, mimeType)",
});
const payload: BannerResponse = {
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
};
const banner = getRootBanner.data.files?.filter((item) =>
item.name?.startsWith(".banner"),
)[0];
if (!banner || !banner.mimeType?.startsWith("image")) {
const findBanner = await driveClient.files.list({
q: `name contains '.banner' and trashed = false and 'me' in owners and parents = '${apiConfig.files.rootFolder}'`,
fields: "files(id, name, mimeType, parents)",
});
const banner = findBanner.data.files?.find((file) =>
file.name?.startsWith(".banner"),
);
if (!banner) {
payload.success = false;
return response.status(200).json(payload);
}
+5 -2
View File
@@ -76,7 +76,7 @@ export default initMiddleware(async function handler(
let tempParent: string[] = file.parents || [];
while (tempParent.length > 0) {
if (breadcrumbs.length === apiConfig.files.breadcrumbDepth) {
if (breadcrumbs.length > apiConfig.files.breadcrumbDepth) {
isLimitReached = true;
break;
}
@@ -99,7 +99,10 @@ export default initMiddleware(async function handler(
success: true,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
breadcrumbs,
breadcrumbs:
breadcrumbs.length > apiConfig.files.breadcrumbDepth
? breadcrumbs.slice(0, apiConfig.files.breadcrumbDepth)
: breadcrumbs,
isLimitReached,
});
} catch (error: any) {
-2
View File
@@ -66,8 +66,6 @@ export default initMiddleware(async function handler(
{ responseType: "stream" },
);
console.log(file);
response.setHeader(
"Content-Type",
file.mimeType || "application/octet-stream",
+36 -18
View File
@@ -32,29 +32,47 @@ export default async function handler(request: NextRequest) {
let fileImage;
if (fileId && isImage) {
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`;
await fetch(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`,
)
.then((res) => {
if (res.headers.get("content-type")?.startsWith("image")) {
return { success: true };
}
return res.json();
})
.then((data) => {
if (!data.success) {
throw new Error("File not found");
}
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?download=1`;
})
.catch((err) => {
throw new Error(err);
});
} else if (fileId && !isImage) {
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?thumbnail=1`;
await fetch(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?thumbnail=1`,
)
.then((res) => {
if (res.headers.get("content-type")?.startsWith("image")) {
return { success: true };
}
return res.json();
})
.then((data) => {
if (!data.success) {
throw new Error("File not found");
}
fileImage = `${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${fileId}?thumbnail=1`;
})
.catch((err) => {
throw new Error(err);
});
} else {
throw new Error("Default image");
}
await fetch(fileImage)
.then((res) => {
if (res.headers.get("content-type")?.startsWith("image")) {
return { success: true };
}
return res.json();
})
.then((data) => {
if (!data.success) {
throw new Error("File not found");
}
})
.catch((err) => {
throw new Error(err);
});
return new ImageResponse(
(
<div
+68
View File
@@ -0,0 +1,68 @@
import { ErrorResponse } from "types/googleapis";
import { NextApiRequest, NextApiResponse } from "next";
import initMiddleware from "utils/apiMiddleware";
import { ExtendedError } from "utils/driveHelper";
import driveClient from "utils/driveClient";
export default initMiddleware(async function handler(
request: NextApiRequest,
response: NextApiResponse,
) {
const _start = Date.now();
try {
const { folderId } = request.query;
const [name, partialId] = (folderId as string).split(":");
if (!name || !partialId || partialId.length !== 8) {
throw new ExtendedError(
"Can't resolve name and id provided.",
400,
"invalidId",
);
}
const findReadme = await driveClient.files.list({
q: `name = '.readme.md' and trashed = false and 'me' in owners`,
fields: "files(id, name, mimeType, parents)",
});
const readme = findReadme.data.files?.find((file) =>
file.parents?.[0].startsWith(partialId),
);
if (!readme) {
return response.status(200);
}
response.setHeader(
"Content-Type",
readme.mimeType || "application/octet-stream",
);
response.setHeader(
"Content-Disposition",
`inline; filename="${readme.name}"`,
);
response.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
const readmeStream = await driveClient.files.get(
{
fileId: readme.id as string,
alt: "media",
},
{ responseType: "text" },
);
return response.status(200).send(readmeStream.data);
} catch (error: any) {
const payload: ErrorResponse = {
success: false,
timestamp: new Date().toISOString(),
responseTime: Date.now() - _start,
code: error.code || 500,
errors: {
message: error.errors?.[0].message || error.message || "Unknown error",
reason: error.errors?.[0].reason || error.cause || "internalError",
},
};
return response.status(payload.code || 500).json(payload);
}
});
+193 -150
View File
@@ -1,193 +1,236 @@
import useSWR from "swr";
import { ErrorResponse, FileResponse, TFileParent } from "types/googleapis";
import Breadcrumb from "components/Breadcrumb";
import { useCallback, useEffect, useState } from "react";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import { useRouter } from "next/router";
import FileDetails from "components/layout/FileDetails";
import useLocalStorage from "hooks/useLocalStorage";
import { GetServerSideProps } from "next";
import axios from "axios";
import { GetServerSidePropsContext } from "next";
import Password from "components/layout/Password";
import { ErrorResponse, FileResponse } from "types/googleapis";
import { useEffect, useState } from "react";
import useSWR from "swr";
import { urlDecrypt } from "utils/encryptionHelper";
import DefaultLayout from "components/layout/DefaultLayout";
import { NextSeo } from "next-seo";
import config from "config/site.config";
import SWRLayout from "components/layout/SWRLayout";
import { getFilePreview, getFileType } from "utils/mimeTypesHelper";
import { capitalize, formatBytes, formatDate } from "utils/formatHelper";
import Link from "next/link";
import useCopyText from "hooks/useCopyText";
type Props = {
passwordParent?: string;
fileName?: string;
id: string;
fileName: string;
};
export default function File({ passwordParent, fileName }: Props) {
const router = useRouter();
const { id } = router.query;
type Metadata = {
label: string;
value: string;
};
export default function File({ id, fileName }: Props) {
const [data, setData] = useState<FileResponse>();
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
const [PreviewComponent, setPreviewComponent] = useState<JSX.Element>();
const [metadata, setMetadata] = useState<Metadata[]>([]);
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
const [password, setPassword] = useState<{ [p: string]: string }>(
passwordStorage,
);
const copyLink = useCopyText();
/**
* ===========================
* START - fetch file data
* ===========================
*/
const {
data: swrData,
error,
isLoading,
isValidating,
mutate,
} = useSWR<FileResponse, ErrorResponse>(
`/api/files/${id}`,
(url, headers) =>
axios
.get<FileResponse>(url, {
headers: {
Authorization: `Bearer ${
password?.[passwordParent as string] ||
password?.[id as string] ||
passwordStorage?.[passwordParent as string] ||
passwordStorage?.[id as string] ||
""
}`,
...headers,
},
})
.then((res) => res.data),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
revalidateIfStale: true,
},
);
} = useSWR<FileResponse, ErrorResponse>(`/api/files/${id}`);
useEffect(() => {
setGlobalLoading(true);
if (swrData) {
const parentsArray: TFileParent[] | undefined = swrData.parents;
parentsArray?.unshift({
id: swrData.file.id as string,
name: swrData.file.name as string,
});
const payload: FileResponse = {
parents: parentsArray,
const decryptedData: FileResponse = {
...swrData,
file: {
...swrData.file,
id: urlDecrypt(swrData.file.id as string),
webContentLink: urlDecrypt(swrData.file.webContentLink as string),
},
};
setData(payload);
setGlobalLoading(false);
setData(decryptedData);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [swrData, error, isLoading, isValidating, password]);
}, [swrData]);
/**
* ===========================
* END - fetch file data
* ===========================
*/
useEffect(() => {
if (!isLoading && !isValidating) {
setGlobalLoading(false);
} else {
setGlobalLoading(true);
if (data) {
const Preview = getFilePreview(
data.file.fileExtension as string,
data.file.mimeType as string,
);
setPreviewComponent(<Preview data={data.file} />);
const defaultMetadata: Metadata[] = [
{
label: "Name",
value: data.file.name as string,
},
{
label: "Type",
value: capitalize(
getFileType(
data.file.fileExtension as string,
data.file.mimeType as string,
),
),
},
{
label: "Size",
value: formatBytes(data.file.size as string),
},
{
label: "Created",
value: formatDate(new Date(data.file.createdTime as string)),
},
{
label: "Modified",
value: formatDate(new Date(data.file.modifiedTime as string)),
},
];
if (data.file.imageMediaMetadata) {
defaultMetadata.push({
label: "Dimensions",
value: `${data.file.imageMediaMetadata.width} x ${data.file.imageMediaMetadata.height}`,
});
}
if (data.file.videoMediaMetadata) {
defaultMetadata.push({
label: "Duration",
value: `${data.file.videoMediaMetadata.durationMillis} ms`,
});
}
setMetadata(defaultMetadata);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, isValidating]);
useEffect(() => {
mutate(swrData, {
revalidate: true,
}).then((r) => {
return r;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [password]);
const inputPassCallback = useCallback(
(data: { [p: string]: string }) => {
setGlobalLoading(true);
setPasswordStorage(data);
setPassword(data);
},
[setPasswordStorage],
);
}, [data]);
return (
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
<DefaultLayout
fileId={id}
renderSwitchLayout={false}
>
<NextSeo
title={fileName || "File preview"}
title={`Viewing ${fileName.split(".").slice(0, -1).join(".")}`}
openGraph={{
type: "website",
title: `${(id as string).split(":")[0]} @${config.siteName}`,
description: config.siteDescription,
url: `${process.env.NEXT_PUBLIC_DOMAIN}/file/${id}`,
title: fileName.split(".").slice(0, -1).join("."),
url: `${process.env.NEXT_PUBLIC_DOMAIN}/file/${encodeURIComponent(
id,
)}`,
images: [
{
url: `${process.env.NEXT_PUBLIC_DOMAIN}/api/og?fileId=${id}`,
url: `${
process.env.NEXT_PUBLIC_DOMAIN
}/api/og?fileId=${encodeURIComponent(id)}`,
alt: fileName,
width: 1200,
height: 630,
alt: config.siteName,
},
],
siteName: config.siteName,
}}
/>
<SWRLayout
data={data}
error={error}
isLoading={isLoading}
>
<div
className={
"relative grid grid-cols-1 gap-2 tablet:grid-cols-4 tablet:gap-4"
}
>
{/* File Preview */}
<div className={"card h-fit tablet:col-span-3"}>
<div className='flex w-full items-center justify-between rounded-lg'>
<span className='font-bold'>Preview</span>
</div>
<div className='flex items-center justify-between'>
<Breadcrumb
data={data?.parents || []}
isLoading={globalLoading}
/>
</div>
{globalLoading && <LoadingFeedback message={"Loading file details..."} />}
{!globalLoading && error && (
<ErrorFeedback message={error.errors?.message} />
)}
{!globalLoading && !error && data && (
<>
{data.passwordRequired && !data.passwordValidated && (
<Password
folderId={(passwordParent as string) || (id as string)}
inputCallback={inputPassCallback}
/>
)}
{(data.passwordValidated || !data.passwordRequired) && (
<>
<FileDetails
data={data.file}
hash={passwordStorage?.[passwordParent as string] || ""}
/>
</>
)}
</>
)}
</div>
<div className={"divider-horizontal"} />
{PreviewComponent}
</div>
{/* Details and download */}
<div
className={
"sticky top-16 flex h-fit flex-col gap-2 max-tablet:flex-col-reverse tablet:col-span-1 tablet:gap-4"
}
>
<div className={"card"}>
<div className='flex w-full items-center justify-between rounded-lg'>
<span className='font-bold'>Details</span>
</div>
<div className={"divider-horizontal"} />
{metadata.map((item, index) => (
<div
key={`fileDetails-${index}`}
className={"mb-2 flex w-full flex-col justify-center"}
>
<span className={"font-bold text-inherit"}>{item.label}</span>
<span
className={"whitespace-pre-wrap break-words text-inherit"}
>
{item.value}
</span>
</div>
))}
</div>
<div className={"card"}>
<div className='flex w-full items-center justify-between rounded-lg'>
<span className='font-bold'>Download</span>
</div>
<div className={"divider-horizontal"} />
<div className={"flex w-full flex-col justify-center gap-2"}>
<Link
href={`/api/files/${id}?download=1`}
className={"w-full"}
target={"_blank"}
rel={"noopener noreferrer"}
>
<button className={"primary w-full"}>Download</button>
</Link>
<button
className={"secondary"}
onClick={() => {
copyLink(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}?download=1`,
);
}}
>
Copy direct link
</button>
</div>
</div>
</div>
</div>
</SWRLayout>
</DefaultLayout>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.query;
const data = await axios.get(
const fetchFileMetadata = await axios.get<FileResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}`,
);
context.res.setHeader(
"Cache-Control",
"public, s-maxage=10, stale-while-revalidate=59",
);
if (data) {
if (!fetchFileMetadata.data.success) {
return {
props: {
passwordParent: data.data.protectedId || null,
fileName: data.data.file.name,
},
};
} else {
return {
props: {
passwordParent: "",
fileName: "",
},
notFound: true,
};
}
}
return {
props: {
id,
fileName: fetchFileMetadata.data.file.name,
},
};
};
+148 -219
View File
@@ -1,46 +1,38 @@
import useSWR from "swr";
import { useContext, useEffect, useState } from "react";
import DefaultLayout from "components/layout/DefaultLayout";
import { LayoutContext, TLayoutContext } from "context/layoutContext";
import useSWRInfinite from "swr/infinite";
import fetcher, { buildNextKey } from "utils/swrFetch";
import { ErrorResponse, FilesResponse, TFile } from "types/googleapis";
import Breadcrumb from "components/Breadcrumb";
import { buildNextKey } from "utils/swrFetch";
import axios, { AxiosHeaders } from "axios";
import { BannerResponse, ErrorResponse, FilesResponse } from "types/googleapis";
import SWRLayout from "components/layout/SWRLayout";
import { drive_v3 } from "googleapis";
import { useCallback, useEffect, useState } from "react";
import MarkdownRender from "components/utility/MarkdownRender";
import config from "config/site.config";
import siteConfig from "config/site.config";
import Readme from "components/layout/Readme";
import GridLayout from "components/layout/Files/GridLayout";
import useLocalStorage from "hooks/useLocalStorage";
import SwitchLayout from "components/utility/SwitchLayout";
import ListLayout from "components/layout/Files/ListLayout";
import LoadingFeedback from "components/APIFeedback/Loading";
import ErrorFeedback from "components/APIFeedback/Error";
import { useRouter } from "next/router";
import axios from "axios";
import Password from "components/layout/Password";
import { GetServerSidePropsContext } from "next";
import { createFileId } from "utils/driveHelper";
import { NextSeo } from "next-seo";
import { GetServerSideProps } from "next";
type Props = {
passwordParent?: string;
folderName?: string;
id: string;
folderName: string;
bannerFileId?: string;
};
export default function Folder({ passwordParent, folderName }: Props) {
const router = useRouter();
const { id } = router.query;
export default function Home({ id, folderName, bannerFileId }: Props) {
const { layout } = useContext<TLayoutContext>(LayoutContext);
const [data, setData] = useState<FilesResponse>();
const [isReadmeExists, setIsReadmeExists] = useState<boolean>(false);
const [renderStyle] = useLocalStorage<"grid" | "list">("renderStyle", "grid");
const [layoutStyle, setLayoutStyle] = useState<"grid" | "list">(renderStyle);
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
const [password, setPassword] = useState<{ [p: string]: string }>(
passwordStorage,
);
const [isReadmeLoading, setIsReadmeLoading] = useState<boolean>(false);
const [readmeData, setReadmeData] = useState<string>();
/**
* ===========================
* START - fetch file data
* ===========================
* **/
const getNextKey = buildNextKey(`/api/files/${id}`);
const {
data: swrData,
@@ -48,231 +40,168 @@ export default function Folder({ passwordParent, folderName }: Props) {
isLoading,
size,
setSize,
isValidating,
mutate,
} = useSWRInfinite<FilesResponse, ErrorResponse>(
getNextKey,
(url, headers) =>
(url: string, headers: AxiosHeaders) =>
axios
.get<FilesResponse>(url, {
headers: {
Authorization: `Bearer ${
password?.[passwordParent as string] ||
password?.[id as string] ||
passwordStorage?.[passwordParent as string] ||
passwordStorage?.[id as string] ||
""
}`,
Authorization: `Bearer TODO:ADD`,
...headers,
},
})
.then((res) => res.data),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
revalidateIfStale: true,
},
);
const {
data: readmeData,
error: readmeError,
isLoading: readmeLoading,
} = useSWR(`/api/readme/${id}`, fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
refreshWhenHidden: false,
refreshInterval: 0,
shouldRetryOnError: false,
});
const isLoadingInitialData = !swrData && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && swrData && typeof swrData[size - 1] === "undefined");
const isEmpty =
swrData?.[0]?.files?.length === 0 && swrData?.[0]?.folders?.length === 0;
const isReachingEnd =
isEmpty ||
(swrData &&
typeof swrData[swrData.length - 1]?.nextPageToken === "undefined");
const filePagination = {
isLoadingInitialData: !swrData && !error,
isLoadingMore:
(!swrData && !error) ||
(size > 0 && swrData && typeof swrData[size - 1] === "undefined"),
isEmpty: swrData?.[0]?.files?.length === 0,
isReachingEnd:
swrData && swrData[swrData.length - 1]?.nextPageToken === undefined,
};
// Since SWRInfinite returning array of response, we need to flatten it.
useEffect(() => {
setGlobalLoading(true);
const files: (TFile | drive_v3.Schema$File)[] | undefined =
swrData?.flatMap((item: FilesResponse) => item.files);
const folders: (TFile | drive_v3.Schema$File)[] | undefined =
swrData?.flatMap((item: FilesResponse) => item.folders);
const newData: FilesResponse = {
...(swrData?.[size - 1] as FilesResponse),
files: (files as drive_v3.Schema$File[]) || [],
folders: (folders as drive_v3.Schema$File[]) || [],
};
setData(newData);
if (newData.isReadmeExists) setIsReadmeExists(true);
setGlobalLoading(false);
if (swrData) {
// Flatten files and folders, in case someone have more folders too.
const files: drive_v3.Schema$File[] = swrData.flatMap(
(item) => item.files,
);
const folders: drive_v3.Schema$File[] = swrData.flatMap(
(item) => item.folders,
);
const flattenData: FilesResponse = {
...swrData[size - 1],
files,
folders,
};
setData(flattenData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [swrData, error, isLoading, size, isValidating, password]);
useEffect(() => {
if (!isLoading && !isValidating) {
setGlobalLoading(false);
} else {
setGlobalLoading(true);
if (flattenData.isReadmeExists) {
setIsReadmeExists(true);
setIsReadmeLoading(true);
axios
.get<string>(`/api/readme/${id}`)
.then((res) => {
setReadmeData(res.data);
})
.finally(() => {
setIsReadmeLoading(false);
});
} else {
setIsReadmeExists(false);
setReadmeData("");
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, isValidating]);
useEffect(() => {
mutate(swrData, {
revalidate: true,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [password]);
const inputPassCallback = useCallback(
(data: { [p: string]: string }) => {
setGlobalLoading(true);
setPasswordStorage(data);
setPassword(data);
},
[setPasswordStorage],
);
}, [swrData, size, id]);
/**
* ===========================
* END - fetch file data
* ===========================
* **/
return (
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
<DefaultLayout fileId={id}>
<NextSeo
title={folderName || "Folder"}
title={folderName}
openGraph={{
type: "website",
title: `${(id as string).split(":")[0]} @${config.siteName}`,
description: config.siteDescription,
title: folderName,
url: `${process.env.NEXT_PUBLIC_DOMAIN}/folder/${id}`,
images: [
{
url: `${process.env.NEXT_PUBLIC_DOMAIN}/api/og-folder?fileId=${id}`,
url: `${
process.env.NEXT_PUBLIC_DOMAIN
}/api/og?fileId=${encodeURIComponent(bannerFileId as string)}`,
alt: siteConfig.siteName,
width: 1200,
height: 630,
alt: config.siteName,
},
],
siteName: config.siteName,
}}
/>
<div className='flex items-center justify-between'>
<Breadcrumb
data={data?.parents || []}
isLoading={globalLoading}
/>
<SwitchLayout setLayoutStyle={setLayoutStyle} />
</div>
{globalLoading && <LoadingFeedback message={"Loading file..."} />}
{!globalLoading && error && (
<ErrorFeedback message={error.errors?.message} />
)}
{!globalLoading && !error && data && (
<>
{data.passwordRequired && !data.passwordValidated && (
<Password
folderId={(passwordParent as string) || (id as string)}
inputCallback={inputPassCallback}
<SWRLayout
data={data}
error={error}
isLoading={isLoading}
>
{siteConfig.readme.position === "start" && (
<Readme
isReadmeExist={isReadmeExists}
isReadmeLoading={isReadmeLoading}
readmeData={readmeData}
/>
)}
<div className={"card"}>
{layout === "grid" && (
<GridLayout
data={data}
pagination={{
swrData,
size,
setSize,
isLoadingMore: filePagination.isLoadingMore,
isReachingEnd: filePagination.isReachingEnd,
}}
/>
)}
{(data.passwordValidated || !data.passwordRequired) && (
<>
{isReadmeExists && config.readme.position === "start" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
)}
<div className={"card"}>
{layoutStyle === "list" && (
<ListLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
{layoutStyle === "grid" && (
<GridLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
</div>
{isReadmeExists && config.readme.position === "end" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
)}
</>
{layout === "list" && (
<ListLayout
data={data}
pagination={{
swrData,
size,
setSize,
isLoadingMore: filePagination.isLoadingMore,
isReachingEnd: filePagination.isReachingEnd,
}}
/>
)}
</>
)}
</div>
</div>
{siteConfig.readme.position === "end" && (
<Readme
isReadmeExist={isReadmeExists}
isReadmeLoading={isReadmeLoading}
readmeData={readmeData}
/>
)}
</SWRLayout>
</DefaultLayout>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.query;
const data = await axios.get(
const fetchFolder = await axios.get<FilesResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/files/${id}`,
);
context.res.setHeader(
"Cache-Control",
"public, s-maxage=10, stale-while-revalidate=59",
);
if (data) {
if (!fetchFolder.data.success) {
return {
props: {
passwordParent: data.data.protectedId || null,
folderName: data.data.parents?.[0]?.name || null,
},
};
} else {
return {
props: {
passwordParent: "",
folderName: "",
},
notFound: true,
};
}
}
const folderName = decodeURIComponent((id as string).split(":")[0]);
const fetchBanner = await axios.get<BannerResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/banner/${id}`,
);
if (!fetchBanner.data.banner) {
return {
props: { id, folderName },
};
}
const bannerFileId = createFileId(fetchBanner.data.banner, true);
return {
props: {
id,
folderName,
bannerFileId,
},
};
};
+2 -2
View File
@@ -13,7 +13,7 @@ import GridLayout from "components/layout/Files/GridLayout";
import ListLayout from "components/layout/Files/ListLayout";
import { createFileId } from "utils/driveHelper";
import { NextSeo } from "next-seo";
import { GetStaticProps } from "next";
import { GetServerSideProps } from "next";
type Props = {
bannerFileId?: string;
@@ -165,7 +165,7 @@ export default function Home({ bannerFileId }: Props) {
);
}
export const getStaticProps: GetStaticProps = async () => {
export const getServerSideProps: GetServerSideProps = async () => {
const fetchBanner = await axios.get<BannerResponse>(
`${process.env.NEXT_PUBLIC_DOMAIN}/api/banner`,
);
+2 -2
View File
@@ -254,10 +254,10 @@ html, body {
}
div.divider-horizontal {
@apply w-full h-px my-2 bg-zinc-400 dark:bg-zinc-500 tablet:my-4;
@apply w-full h-px my-2 bg-zinc-400 dark:bg-zinc-500 tablet:my-2;
}
div.divider-vertical {
@apply h-full w-px mx-2 bg-zinc-400 dark:bg-zinc-500 tablet:mx-4;
@apply h-full w-px mx-2 bg-zinc-400 dark:bg-zinc-500 tablet:mx-2;
}
.grid-auto-fit {
+4
View File
@@ -76,3 +76,7 @@ export function formatRelativeDate(
style: "long",
}).format(diff, unit);
}
export function capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
+9 -11
View File
@@ -1,4 +1,3 @@
import mime from "mime-types";
import { IconType } from "react-icons";
import {
BsBoxFill,
@@ -33,10 +32,6 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
function findMimeType(extension: string): string {
return mime.lookup(extension) || "application/octet-stream";
}
const type = {
"3d": "3d", // model preview
"audio": "audio", // audio preview
@@ -95,12 +90,12 @@ const extToTypeMap: { [key: string]: string } = {
"wav": type.audio,
// Archives
"7z": type.default,
"bz2": type.default,
"gz": type.default,
"rar": type.default,
"tar": type.default,
"zip": type.default,
"7z": type.archive,
"bz2": type.archive,
"gz": type.archive,
"rar": type.archive,
"tar": type.archive,
"zip": type.archive,
// Rich text
"md": type.rich_text,
@@ -255,6 +250,9 @@ export function getFileType(extension: string, mimeType?: string): string {
if (type === "video") {
return "video";
}
if (type === "image") {
return "image";
}
}
return extToTypeMap[extension] || type.default;