From 4d6b41f1566481a00502ea0c647d4742dffe8cc8 Mon Sep 17 00:00:00 2001 From: spencerwooo Date: Sun, 5 Sep 2021 16:03:27 +0100 Subject: [PATCH] infinite loading via useSWR, close - Pagination #2 --- components/FileListing.tsx | 97 ++++++++++++++++++++++++++++++-------- pages/_app.tsx | 4 +- pages/api/index.ts | 12 ++--- utils/tools.ts | 47 ++++++++++++++++-- 4 files changed, 128 insertions(+), 32 deletions(-) diff --git a/components/FileListing.tsx b/components/FileListing.tsx index efd4131..108d28e 100644 --- a/components/FileListing.tsx +++ b/components/FileListing.tsx @@ -12,7 +12,8 @@ import dynamic from 'next/dynamic' import { getExtension, getFileIcon, hasKey } from '../utils/getFileIcon' import { extensions, preview } from '../utils/getPreviewType' -import { getBaseUrl, useStaleSWR } from '../utils/tools' +import { getBaseUrl, useProtectedSWRInfinite } from '../utils/tools' + import { VideoPreview } from './previews/VideoPreview' import { AudioPreview } from './previews/AudioPreview' import Loading from './Loading' @@ -106,7 +107,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = const path = queryToPath(query) - const { data, error } = useStaleSWR(`/api?path=${path}`, path) + const { data, error, size, setSize } = useProtectedSWRInfinite(path) if (error) { return ( @@ -133,9 +134,15 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = return false } - if ('folderData' in data) { - const { value } = data.folderData + const responses: any[] = data ? [].concat(...data) : [] + const isLoadingInitialData = !data && !error + const isLoadingMore = isLoadingInitialData || (size > 0 && data && typeof data[size - 1] === 'undefined') + const isEmpty = data?.[0]?.length === 0 + const isReachingEnd = isEmpty || (data && typeof data[data.length - 1]?.next === 'undefined') + const onlyOnePage = data && typeof data[0].next === 'undefined' + + if ('folder' in responses[0]) { // Image preview rendering preparations const imagesInFolder: ImageDecorator[] = [] const imageIndexDict: { [key: string]: number } = {} @@ -145,7 +152,10 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = let renderReadme = false let readmeFile = null - value.forEach((c: any) => { + // Expand list of API returns into flattened file data + const children = [].concat(...responses.map(r => r.folder.value)) + + children.forEach((c: any) => { if (fileIsImage(c.name)) { imagesInFolder.push({ src: c['@microsoft.graph.downloadUrl'], @@ -217,7 +227,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = /> )} - {value.map((c: any) => ( + {children.map((c: any) => (
= ({ query }) =
))} + {!onlyOnePage && ( +
+
+ - showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : children.length} files - +
+ +
+ )} + {renderReadme && (
@@ -280,10 +338,10 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = ) } - if ('identityData' in data) { - const { identityData } = data - const downloadUrl = identityData['@microsoft.graph.downloadUrl'] - const fileName = data.identityData.name + if ('file' in responses[0] && responses.length === 1) { + const { file } = responses[0] + const downloadUrl = file['@microsoft.graph.downloadUrl'] + const fileName = file.name const fileExtension = fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase() if (hasKey(extensions, fileExtension)) { @@ -297,25 +355,25 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = ) case preview.text: - return + return case preview.code: - return + return case preview.markdown: - return + return case preview.video: - return + return case preview.audio: - return + return case preview.pdf: - return + return case preview.office: - return + return default: return
{fileName}
@@ -326,7 +384,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = <>
@@ -338,9 +396,8 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) = return (
- +
) } - export default FileListing diff --git a/pages/_app.tsx b/pages/_app.tsx index cfa38eb..688c01a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -35,6 +35,7 @@ import { faKey, faSignOutAlt, faCloud, + faChevronCircleDown, } from '@fortawesome/free-solid-svg-icons' import { faGithub, faMarkdown, faTelegramPlane } from '@fortawesome/free-brands-svg-icons' @@ -72,7 +73,8 @@ library.add( faSignOutAlt, faEnvelope, faCloud, - faTelegramPlane + faTelegramPlane, + faChevronCircleDown ) function MyApp({ Component, pageProps }: AppProps) { diff --git a/pages/api/index.ts b/pages/api/index.ts index 25f38ce..b7a03da 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -43,7 +43,7 @@ const getAccessToken = async () => { } export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { path = '/', raw = false, t = '' } = req.query + const { path = '/', raw = false, next = '' } = req.query if (path === '[...path]') { res.status(400).json({ error: 'No path specified.' }) return @@ -125,11 +125,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if ('folder' in identityData) { const { data: folderData } = await axios.get(`${requestUrl}:/children`, { headers: { Authorization: `Bearer ${accessToken}` }, - params: t + params: next ? { select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file', top: siteConfig.maxItems, - $skipToken: t, + $skipToken: next, } : { select: '@microsoft.graph.downloadUrl,name,size,id,lastModifiedDateTime,folder,file', @@ -144,13 +144,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Return paging token if specified if (nextPage) { - res.status(200).json({ path, identityData, folderData, nextPage }) + res.status(200).json({ folder: folderData, next: nextPage }) } else { - res.status(200).json({ path, identityData, folderData }) + res.status(200).json({ folder: folderData }) } return } - res.status(200).json({ path, identityData }) + res.status(200).json({ file: identityData }) return } diff --git a/utils/tools.ts b/utils/tools.ts index 0c90c9c..904e263 100644 --- a/utils/tools.ts +++ b/utils/tools.ts @@ -1,6 +1,6 @@ import axios from 'axios' import sha256 from 'crypto-js/sha256' -import useSWR, { cache, Key } from 'swr' +import useSWR, { cache, Key, useSWRInfinite } from 'swr' import siteConfig from '../config/site.json' @@ -38,17 +38,54 @@ export const useStaleSWR = (url: Key, path: string = '') => { revalidateOnReconnect: true, } - const storedToken = - typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : '' - const hashedToken = storedToken ? encryptToken(storedToken) : null - + const hashedToken = getStoredToken(path) return useSWR([url, hashedToken], fetcher, revalidationOptions) } +/** + * Paging with useSWRInfinite + protected token support + * @param path Current query directory path + * @returns useSWRInfinite API + */ +export const useProtectedSWRInfinite = (path: string = '') => { + const hashedToken = getStoredToken(path) + + /** + * Next page infinite loading for useSWR + * @param pageIdx The index of this paging collection + * @param prevPageData Previous page information + * @param path Directory path + * @returns API to the next page + */ + const getNextKey = (pageIndex, previousPageData) => { + // Reached the end of the collection + if (previousPageData && !previousPageData.folder) return null + + // First page with no prevPageData + if (pageIndex === 0) return [`/api?path=${path}`, hashedToken] + + // Add nextPage token to API endpoint + return [`/api?path=${path}&next=${previousPageData.next}`, hashedToken] + } + + const revalidationOptions = { + revalidateOnMount: !(cache.has(`arg@"/api?path=${path}"@null`) || cache.has(`/api?path=${path}`)), + revalidateOnFocus: false, + revalidateOnReconnect: true, + } + return useSWRInfinite(getNextKey, fetcher, revalidationOptions) +} + // Hash password token with SHA256 const encryptToken = (token: string) => { return sha256(token).toString() } +// Fetch stored token from localStorage and encrypt with SHA256 +const getStoredToken = (path: string) => { + const storedToken = + typeof window !== 'undefined' ? JSON.parse(localStorage.getItem(matchProtectedRoute(path)) as string) : '' + return storedToken ? encryptToken(storedToken) : null +} /** * Compares the hash of .password and od-protected-token header * @param odTokenHeader od-protected-token header (sha256 hashed token)