mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-23 04:10:03 +00:00
Add Next SEO and clean some code
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# You can generate encryption key from setup page.
|
||||
ENCRYPTION_KEY=""
|
||||
# You can use any random string for JWT_KEY, it is recommended to use a strong key by combining characters, numbers and symbols.
|
||||
JWT_KEY=""
|
||||
# Domain name of your website. Used for fetching the og-image from public directory.
|
||||
# Example: https://drive.example.com
|
||||
NEXT_PUBLIC_DOMAIN=""
|
||||
+2
-4
@@ -25,8 +25,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env file
|
||||
.env*
|
||||
.env*.local
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -34,10 +34,8 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
!/src/pages/api/test.ts
|
||||
|
||||
# personal docs
|
||||
/docs
|
||||
/.next
|
||||
/.vscode
|
||||
/.idea
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -13,7 +13,6 @@ type Props = {
|
||||
export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
const limitItem = 2;
|
||||
const [limitedPath, setLimitedPath] = useState<TFileParent[]>([]);
|
||||
const [slicedPath, setSlicedPath] = useState<TFileParent>();
|
||||
const [isLimited, setIsLimited] = useState<boolean>();
|
||||
// const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
@@ -21,12 +20,12 @@ export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
// setIsLoading(true);
|
||||
if (data.length > 0) {
|
||||
const findRoot = data.find((item) => item.id === config.files.rootFolder);
|
||||
let _data = data;
|
||||
if (findRoot) {
|
||||
data = data.filter((item) => item.id !== config.files.rootFolder);
|
||||
_data = _data.filter((item) => item.id !== config.files.rootFolder);
|
||||
}
|
||||
setLimitedPath(data.slice(0, limitItem).reverse());
|
||||
setSlicedPath(data.slice(limitItem)[0]);
|
||||
setIsLimited(data.length > limitItem);
|
||||
setLimitedPath(_data.slice(0, limitItem).reverse());
|
||||
setIsLimited(_data.length > limitItem);
|
||||
// setIsLoading(false);
|
||||
}
|
||||
}, [data]);
|
||||
@@ -61,7 +60,7 @@ export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
<span className={"cursor-default"}>/</span>
|
||||
|
||||
{idx === limitedPath.length - 1 ? (
|
||||
<span className='flex cursor-default cursor-default items-center gap-2 font-bold'>
|
||||
<span className='flex cursor-default items-center gap-2 font-bold'>
|
||||
{parent.name}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function GridFile({ data }: Props) {
|
||||
{data.videoMediaMetadata && (
|
||||
<>
|
||||
<MdPlayCircleFilled className='center absolute h-8 w-8 text-white/50 transition-colors duration-300 group-hover:text-white/80' />
|
||||
<span className='absolute bottom-1 right-1 rounded-lg bg-zinc-950/50 px-1 py-0.5 text-xs text-white'>
|
||||
<span className='absolute bottom-0 right-0 rounded-lg rounded-bl-none rounded-tr-none bg-zinc-950/75 px-1 py-0.5 text-xs text-white'>
|
||||
{formatDuration(data.videoMediaMetadata.durationMillis!)}
|
||||
</span>
|
||||
</>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function DetailsButtons({ data, hash }: Props) {
|
||||
<MdCopyAll />
|
||||
Copy direct link
|
||||
</button>
|
||||
{!config.files.allowDownloadProtectedWithoutAccess && (
|
||||
{hash && !config.files.allowDownloadProtectedWithoutAccess && (
|
||||
<div className={"banner warning text-sm"}>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<div className={"font-bold"}>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { ErrorResponse, FileResponse, TFile } from "@/types/googleapis";
|
||||
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 ImagePreview from "@components/FilePreview/ImagePreview";
|
||||
import MarkdownRender from "@components/utility/MarkdownRender";
|
||||
import fetcher from "@utils/swrFetch";
|
||||
import useSWR from "swr";
|
||||
import { getFilePreview } from "@utils/mimeTypesHelper";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
@@ -18,6 +15,20 @@ 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);
|
||||
setPreviewComponent(
|
||||
<Preview
|
||||
data={data}
|
||||
hash={hash || ""}
|
||||
/>,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -73,12 +84,13 @@ export default function FileDetails({ data, hash }: Props) {
|
||||
|
||||
<div className={"divider-horizontal"} />
|
||||
|
||||
{data.mimeType?.startsWith("image") && (
|
||||
<ImagePreview
|
||||
data={data}
|
||||
hash={hash || ""}
|
||||
/>
|
||||
)}
|
||||
{PreviewComponent}
|
||||
{/*{data.mimeType?.startsWith("image") && (*/}
|
||||
{/* <ImagePreview*/}
|
||||
{/* data={data}*/}
|
||||
{/* hash={hash || ""}*/}
|
||||
{/* />*/}
|
||||
{/*)}*/}
|
||||
{/*{data.mimeType?.startsWith("audio") && (*/}
|
||||
<div className='flex w-full items-center justify-center'>
|
||||
{/*<video*/}
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function GridLayout({ data, pagination }: Props) {
|
||||
<button
|
||||
disabled={isLoadingMore}
|
||||
onClick={() => {
|
||||
setSize(size + 1);
|
||||
setSize(size + 1).then((r) => r);
|
||||
}}
|
||||
className='w-full rounded-lg'
|
||||
>
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function ListLayout({ data, pagination }: Props) {
|
||||
<button
|
||||
disabled={isLoadingMore}
|
||||
onClick={() => {
|
||||
setSize(size + 1);
|
||||
setSize(size + 1).then((r) => r);
|
||||
}}
|
||||
className='w-full rounded-lg'
|
||||
>
|
||||
|
||||
@@ -55,6 +55,15 @@ export default function Navbar() {
|
||||
const { data, error, isLoading } = useSWR<SearchResponse, ErrorResponse>(
|
||||
`/api/search?query=${debouncedSearchQuery}`,
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import useLocalStorage from "@hooks/useLocalStorage";
|
||||
import { IoMdEye, IoMdEyeOff } from "react-icons/io";
|
||||
import { MdLock } from "react-icons/md";
|
||||
import { useRouter } from "next/router";
|
||||
import { hashToken } from "@utils/hashHelper";
|
||||
import ReactLoading from "react-loading";
|
||||
|
||||
type Props = {
|
||||
folderId: string;
|
||||
inputCallback: (data: { [p: string]: string }) => void;
|
||||
};
|
||||
export default function Password({ folderId, inputCallback }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
const [passwordStorage] = useLocalStorage<{
|
||||
[key: string]: string;
|
||||
}>("passwordStorage", {});
|
||||
|
||||
@@ -63,7 +59,7 @@ export default function Password({ folderId, inputCallback }: Props) {
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
}}
|
||||
onKeyPress={(e) => {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ const config = {
|
||||
/* Site MetaData */
|
||||
// The name of the site
|
||||
siteName: "My Drive Linker",
|
||||
// The description of the site
|
||||
// Used in meta tags and document head
|
||||
siteDescription: "My Drive Linker",
|
||||
// Fav icon of the site
|
||||
// Also used as the logo of the site on the navbar
|
||||
siteIcon: "/favicon.ico",
|
||||
|
||||
+31
-1
@@ -1,5 +1,4 @@
|
||||
import type { AppProps } from "next/app";
|
||||
import { SWRConfig } from "swr";
|
||||
import Navbar from "@/components/layout/Navbar";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import NextNProgress from "nextjs-progressbar";
|
||||
@@ -14,6 +13,8 @@ import { Exo_2, Source_Sans_Pro, JetBrains_Mono } from "next/font/google";
|
||||
import { IconContext } from "react-icons";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import useLocalStorage from "@hooks/useLocalStorage";
|
||||
import { DefaultSeo, DefaultSeoProps } from "next-seo";
|
||||
import config from "@config/site.config";
|
||||
|
||||
const exo2 = Exo_2({
|
||||
weight: ["300", "400", "600", "700"],
|
||||
@@ -39,10 +40,39 @@ const jetBrainsMono = JetBrains_Mono({
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const [isDarkMode] = useLocalStorage<boolean>("isDarkMode", false);
|
||||
|
||||
const SEOConfig: DefaultSeoProps = {
|
||||
titleTemplate: `%s | ${config.siteName}`,
|
||||
defaultTitle: config.siteName,
|
||||
description: config.siteDescription,
|
||||
dangerouslySetAllPagesToNoFollow: true,
|
||||
dangerouslySetAllPagesToNoIndex: true,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
title: config.siteName,
|
||||
description: config.siteDescription,
|
||||
images: [
|
||||
{
|
||||
url: `${process.env.NEXT_PUBLIC_DOMAIN}/og-image.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: config.siteName,
|
||||
},
|
||||
],
|
||||
siteName: config.siteName,
|
||||
},
|
||||
twitter: {
|
||||
handle: "@mbaharip_",
|
||||
site: "@mbaharip_",
|
||||
cardType: "summary_large_image",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`${exo2.variable} ${sourceSansPro.variable} ${jetBrainsMono.variable} font-body`}
|
||||
>
|
||||
<DefaultSeo {...SEOConfig} />
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
size: "18px",
|
||||
|
||||
@@ -4,8 +4,6 @@ import { NextApiRequest, NextApiResponse } from "next";
|
||||
import config from "@config/site.config";
|
||||
import { validateProtected } from "@utils/driveHelper";
|
||||
import { ExtendedError } from "@/types/default";
|
||||
import { decrypt } from "@utils/encryptionHelper";
|
||||
import { verify } from "jsonwebtoken";
|
||||
import { reverseString } from "@utils/hashHelper";
|
||||
|
||||
export default async function handler(
|
||||
@@ -14,7 +12,6 @@ export default async function handler(
|
||||
) {
|
||||
try {
|
||||
const { id, hash } = request.query;
|
||||
const { vector, data } = request.query;
|
||||
const { authorization } = request.headers;
|
||||
const headerHash = authorization?.split(" ")[1] || null;
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import drive from "@utils/driveClient";
|
||||
import { hashToken } from "@utils/hashHelper";
|
||||
|
||||
export default async function handler(
|
||||
request: NextApiRequest,
|
||||
response: NextApiResponse,
|
||||
) {
|
||||
const files = await drive.files.list({
|
||||
q: "'1VMU0sQOkuI06icRRJFof-6V-NLyBWlp5' in parents",
|
||||
fields: "files(id, name, mimeType, size)",
|
||||
});
|
||||
const { password } = request.query;
|
||||
return response.status(200).json({
|
||||
hash: hashToken(password as string),
|
||||
file: files.data.files,
|
||||
});
|
||||
}
|
||||
+11
-6
@@ -10,11 +10,13 @@ import useLocalStorage from "@hooks/useLocalStorage";
|
||||
import axios from "axios";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Password from "@components/layout/Password";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
type Props = {
|
||||
passwordParent?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
export default function File({ passwordParent }: Props) {
|
||||
export default function File({ passwordParent, fileName }: Props) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
@@ -93,6 +95,8 @@ export default function File({ passwordParent }: Props) {
|
||||
useEffect(() => {
|
||||
mutate(swrData, {
|
||||
revalidate: true,
|
||||
}).then((r) => {
|
||||
return r;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [password]);
|
||||
@@ -108,6 +112,7 @@ export default function File({ passwordParent }: Props) {
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<NextSeo title={fileName || "File preview"} />
|
||||
{globalLoading && <LoadingFeedback message={"Loading file details..."} />}
|
||||
{!globalLoading && error && (
|
||||
<ErrorFeedback message={error.errors?.message} />
|
||||
@@ -143,25 +148,25 @@ export default function File({ passwordParent }: Props) {
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { id } = context.query;
|
||||
const passwordParent = await axios.get(
|
||||
`http://localhost:5000/api/files/${id}`,
|
||||
);
|
||||
const data = await axios.get(`http://localhost:5000/api/files/${id}`);
|
||||
|
||||
context.res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59",
|
||||
);
|
||||
|
||||
if (passwordParent) {
|
||||
if (data) {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: passwordParent.data.protectedId || null,
|
||||
passwordParent: data.data.protectedId || null,
|
||||
fileName: data.data.file.name,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: "",
|
||||
fileName: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+12
-14
@@ -1,16 +1,10 @@
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import fetcher, { buildNextKey } from "@utils/swrFetch";
|
||||
import { ErrorResponse, FilesResponse, TFile } from "@/types/googleapis";
|
||||
import Breadcrumb from "@/components/Breadcrumb";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import MarkdownRender from "@/components/utility/MarkdownRender";
|
||||
import config from "@config/site.config";
|
||||
import GridLayout from "@components/layout/Files/GridLayout";
|
||||
@@ -23,11 +17,13 @@ import { useRouter } from "next/router";
|
||||
import axios from "axios";
|
||||
import Password from "@components/layout/Password";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
type Props = {
|
||||
passwordParent?: string;
|
||||
folderName?: string;
|
||||
};
|
||||
export default function Folder({ passwordParent }: Props) {
|
||||
export default function Folder({ passwordParent, folderName }: Props) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
@@ -150,6 +146,8 @@ export default function Folder({ passwordParent }: Props) {
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<NextSeo title={folderName || "Folder"} />
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
@@ -236,25 +234,25 @@ export default function Folder({ passwordParent }: Props) {
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { id } = context.query;
|
||||
const passwordParent = await axios.get(
|
||||
`http://localhost:5000/api/files/${id}`,
|
||||
);
|
||||
const data = await axios.get(`http://localhost:5000/api/files/${id}`);
|
||||
|
||||
context.res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59",
|
||||
);
|
||||
|
||||
if (passwordParent) {
|
||||
if (data) {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: passwordParent.data.protectedId || null,
|
||||
passwordParent: data.data.protectedId || null,
|
||||
folderName: data.data.parents?.[0]?.name || null,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
props: {
|
||||
passwordParent: "",
|
||||
folderName: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -94,15 +94,6 @@ export interface SearchResponse extends APIResponse {
|
||||
files: (TFile | drive_v3.Schema$File)[];
|
||||
}
|
||||
|
||||
export interface ReadmeResponse extends APIResponse {
|
||||
file: TFile | drive_v3.Schema$File;
|
||||
}
|
||||
|
||||
export interface PasswordResponse extends APIResponse {
|
||||
passwordRequired: boolean;
|
||||
passwordValidated: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorResponse extends APIResponse {
|
||||
code: number;
|
||||
errors: {
|
||||
|
||||
@@ -9,8 +9,3 @@ export interface ExpiredJWTPayload {
|
||||
exp: number;
|
||||
isExpired: boolean;
|
||||
}
|
||||
|
||||
export interface ProtectionPayload {
|
||||
fileId: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -33,44 +33,6 @@ export function buildQuery({
|
||||
return query.join(" and ");
|
||||
}
|
||||
|
||||
export async function _checkProtected(id: string) {
|
||||
try {
|
||||
const files = await drive.files.list({
|
||||
q: id ? buildQuery({ id }) : buildQuery({}),
|
||||
fields: "files(id)",
|
||||
});
|
||||
|
||||
return {
|
||||
protected: files.data.files?.length,
|
||||
id: files.data.files?.[0].id || null,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
protected: false,
|
||||
id: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function _validateFolderPassword(
|
||||
passwordFileId: string,
|
||||
password: string,
|
||||
) {
|
||||
try {
|
||||
const folderPassword = await drive.files.get(
|
||||
{
|
||||
fileId: passwordFileId,
|
||||
alt: "media",
|
||||
},
|
||||
{ responseType: "text" },
|
||||
);
|
||||
|
||||
return folderPassword.data === password;
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateProtected(
|
||||
fileId: string | TFileParent[],
|
||||
passwordHash?: string,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
BsBoxFill,
|
||||
BsDatabaseFill,
|
||||
BsFileEarmarkBinaryFill,
|
||||
BsFileEarmarkBreakFill,
|
||||
BsFileEarmarkCodeFill,
|
||||
BsFileEarmarkFill,
|
||||
BsFileEarmarkFontFill,
|
||||
@@ -13,14 +12,11 @@ import {
|
||||
BsFileEarmarkPdfFill,
|
||||
BsFileEarmarkPlayFill,
|
||||
BsFileEarmarkRichtextFill,
|
||||
BsFileEarmarkRuledFill,
|
||||
BsFileEarmarkSlidesFill,
|
||||
BsFileEarmarkSpreadsheetFill,
|
||||
BsFileEarmarkTextFill,
|
||||
BsFileEarmarkWordFill,
|
||||
BsFileEarmarkZipFill,
|
||||
BsFillDatabaseFill,
|
||||
BsFolderFill,
|
||||
} from "react-icons/bs";
|
||||
import ModelPreview from "@components/FilePreview/ModelPreview";
|
||||
import AudioPreview from "@components/FilePreview/AudioPreview";
|
||||
@@ -33,7 +29,7 @@ import CodePreview from "@components/FilePreview/CodePreview";
|
||||
import TextPreview from "@components/FilePreview/TextPreview";
|
||||
import VideoPreview from "@components/FilePreview/VideoPreview";
|
||||
|
||||
export default function findMimeType(extension: string): string {
|
||||
function findMimeType(extension: string): string {
|
||||
return mime.lookup(extension) || "application/octet-stream";
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,6 @@ import { FilesResponse } from "@/types/googleapis";
|
||||
const fetcher = async <T>(url: string, headers: Record<string, string> = {}) =>
|
||||
axios.get<T>(url, { headers }).then((res) => res.data);
|
||||
|
||||
function getNextKey(
|
||||
pageIndex: number,
|
||||
previousPageData: FilesResponse,
|
||||
): string | null {
|
||||
if (previousPageData && !previousPageData.nextPageToken) {
|
||||
return null;
|
||||
}
|
||||
const pageToken = previousPageData ? previousPageData.nextPageToken : "";
|
||||
return `/api/files?pageToken=${pageToken}`;
|
||||
}
|
||||
|
||||
export function buildNextKey(apiURL: string) {
|
||||
return (pageIndex: number, previousPageData: FilesResponse) => {
|
||||
if (previousPageData && !previousPageData.nextPageToken) {
|
||||
|
||||
Reference in New Issue
Block a user