infinite loading via useSWR, close - Pagination #2

This commit is contained in:
spencerwooo
2021-09-05 16:03:27 +01:00
parent 34c3310f12
commit 4d6b41f156
4 changed files with 128 additions and 32 deletions
+77 -20
View File
@@ -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) => (
<div className="hover:bg-gray-100 dark:hover:bg-gray-850 grid grid-cols-12" key={c.id}>
<div
className="col-span-11"
@@ -271,6 +281,54 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
</div>
))}
{!onlyOnePage && (
<div>
<div className="p-3 font-mono text-sm text-center text-gray-400 border-b">
- showing {size} page{size > 1 ? 's' : ''} of {isLoadingMore ? '...' : children.length} files -
</div>
<button
className={`flex items-center justify-center w-full p-3 space-x-2 ${
isLoadingMore || isReachingEnd ? 'opacity-60' : 'hover:bg-gray-100'
}`}
onClick={() => setSize(size + 1)}
disabled={isLoadingMore || isReachingEnd}
>
{isLoadingMore ? (
<>
<span>Loading ...</span>{' '}
<svg
className="animate-spin w-5 h-5 mr-3 -ml-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</>
) : isReachingEnd ? (
<span>No more files</span>
) : (
<>
<span>Load more</span>
<FontAwesomeIcon icon="chevron-circle-down" />
</>
)}
</button>
</div>
)}
{renderReadme && (
<div className="dark:border-gray-700 border-t">
<MarkdownPreview file={readmeFile} path={path} standalone={false} />
@@ -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 <TextPreview file={identityData} />
return <TextPreview file={file} />
case preview.code:
return <CodePreview file={identityData} />
return <CodePreview file={file} />
case preview.markdown:
return <MarkdownPreview file={identityData} path={path} />
return <MarkdownPreview file={file} path={path} />
case preview.video:
return <VideoPreview file={identityData} />
return <VideoPreview file={file} />
case preview.audio:
return <AudioPreview file={identityData} />
return <AudioPreview file={file} />
case preview.pdf:
return <PDFPreview file={identityData} />
return <PDFPreview file={file} />
case preview.office:
return <OfficePreview file={identityData} />
return <OfficePreview file={file} />
default:
return <div className="dark:bg-gray-900 bg-white rounded shadow">{fileName}</div>
@@ -326,7 +384,7 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
<>
<div className="dark:bg-gray-900 p-3 bg-white rounded shadow">
<FourOhFour
errorMsg={`Preview for file ${identityData.name} is not available, download directly with the button below.`}
errorMsg={`Preview for file ${fileName} is not available, download directly with the button below.`}
/>
</div>
<div className="mt-4">
@@ -338,9 +396,8 @@ const FileListing: FunctionComponent<{ query?: ParsedUrlQuery }> = ({ query }) =
return (
<div className="dark:bg-gray-900 p-3 bg-white rounded shadow">
<FourOhFour errorMsg={`Cannot preview ${path}.`} />
<FourOhFour errorMsg={`Cannot preview ${path}`} />
</div>
)
}
export default FileListing
+3 -1
View File
@@ -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) {
+6 -6
View File
@@ -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
}
+42 -5
View File
@@ -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)