mirror of
https://github.com/Nezumi-2711/onedrive-vercel-index.git
synced 2026-09-22 13:38:45 +00:00
Merge pull request #283 from spencerwooo/native-search
This commit is contained in:
@@ -34,7 +34,8 @@ import VideoPreview from './previews/VideoPreview'
|
|||||||
import PDFPreview from './previews/PDFPreview'
|
import PDFPreview from './previews/PDFPreview'
|
||||||
import URLPreview from './previews/URLPreview'
|
import URLPreview from './previews/URLPreview'
|
||||||
import DefaultPreview from './previews/DefaultPreview'
|
import DefaultPreview from './previews/DefaultPreview'
|
||||||
import { PreviewContainer } from './previews/Containers'
|
import { DownloadBtnContainer, PreviewContainer } from './previews/Containers'
|
||||||
|
import DownloadButtonGroup from './DownloadBtnGtoup'
|
||||||
|
|
||||||
// Disabling SSR for some previews (image gallery view, and PDF view)
|
// Disabling SSR for some previews (image gallery view, and PDF view)
|
||||||
const ReactViewer = dynamic(() => import('react-viewer'), { ssr: false })
|
const ReactViewer = dynamic(() => import('react-viewer'), { ssr: false })
|
||||||
@@ -545,10 +546,15 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
|
|||||||
switch (extensions[fileExtension]) {
|
switch (extensions[fileExtension]) {
|
||||||
case preview.image:
|
case preview.image:
|
||||||
return (
|
return (
|
||||||
<PreviewContainer>
|
<>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
<PreviewContainer>
|
||||||
<img className="mx-auto" src={downloadUrl} alt={fileName} />
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
</PreviewContainer>
|
<img className="mx-auto" src={downloadUrl} alt={fileName} />
|
||||||
|
</PreviewContainer>
|
||||||
|
<DownloadBtnContainer>
|
||||||
|
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
|
||||||
|
</DownloadBtnContainer>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
case preview.text:
|
case preview.text:
|
||||||
|
|||||||
+32
-3
@@ -2,6 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
import { IconName } from '@fortawesome/fontawesome-svg-core'
|
import { IconName } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import toast, { Toaster } from 'react-hot-toast'
|
import toast, { Toaster } from 'react-hot-toast'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
@@ -9,12 +10,21 @@ import { useRouter } from 'next/router'
|
|||||||
import { Fragment, useEffect, useState } from 'react'
|
import { Fragment, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import siteConfig from '../config/site.json'
|
import siteConfig from '../config/site.json'
|
||||||
|
import SearchModal from './SearchModal'
|
||||||
|
import useDeviceOS from '../utils/useDeviceOS'
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const os = useDeviceOS()
|
||||||
|
|
||||||
const [tokenPresent, setTokenPresent] = useState(false)
|
const [tokenPresent, setTokenPresent] = useState(false)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
|
const openSearchBox = () => setSearchOpen(true)
|
||||||
|
|
||||||
|
useHotkeys(`${os === 'mac' ? 'cmd' : 'ctrl'}+k`, openSearchBox)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedToken = () => {
|
const storedToken = () => {
|
||||||
for (const r of siteConfig.protectedRoutes) {
|
for (const r of siteConfig.protectedRoutes) {
|
||||||
@@ -44,15 +54,34 @@ const Navbar = () => {
|
|||||||
<div className="bg-white dark:bg-gray-900 dark:border-gray-500/30 sticky top-0 bg-opacity-80 border-b border-gray-900/10 backdrop-blur-md z-[100]">
|
<div className="bg-white dark:bg-gray-900 dark:border-gray-500/30 sticky top-0 bg-opacity-80 border-b border-gray-900/10 backdrop-blur-md z-[100]">
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full mx-auto px-4 py-1">
|
<SearchModal searchOpen={searchOpen} setSearchOpen={setSearchOpen} />
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 justify-between w-full mx-auto px-4 py-1">
|
||||||
<Link href="/" passHref>
|
<Link href="/" passHref>
|
||||||
<a className="dark:text-white hover:opacity-80 flex items-center p-2 space-x-2">
|
<a className="dark:text-white hover:opacity-80 flex items-center py-2 md:p-2 space-x-2">
|
||||||
<Image src={siteConfig.icon} alt="icon" width="25" height="25" priority />
|
<Image src={siteConfig.icon} alt="icon" width="25" height="25" priority />
|
||||||
<span className="sm:block hidden font-bold">{siteConfig.title}</span>
|
<span className="sm:block hidden font-bold">{siteConfig.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 text-gray-700">
|
<div className="flex items-center flex-1 md:flex-initial space-x-4 text-gray-700">
|
||||||
|
<button
|
||||||
|
className="flex-1 flex items-center justify-between rounded-lg bg-gray-100 dark:bg-gray-800 md:w-48 px-2.5 py-1.5 dark:text-white hover:opacity-80"
|
||||||
|
onClick={openSearchBox}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FontAwesomeIcon icon="search" />
|
||||||
|
<span className="text-sm font-medium">Search ...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">
|
||||||
|
{os === 'mac' ? '⌘' : 'Ctrl'}
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">K</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
{siteConfig.links.length !== 0 &&
|
{siteConfig.links.length !== 0 &&
|
||||||
siteConfig.links.map((l: { name: string; link: string }) => (
|
siteConfig.links.map((l: { name: string; link: string }) => (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||||
|
import { useAsync } from 'react-async-hook'
|
||||||
|
import useConstant from 'use-constant'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
|
||||||
|
import { Dispatch, FC, Fragment, SetStateAction, useState } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { OdSearchResult } from '../types'
|
||||||
|
import { getFileIcon } from '../utils/getFileIcon'
|
||||||
|
import siteConfig from '../config/site.json'
|
||||||
|
import { LoadingIcon } from './Loading'
|
||||||
|
|
||||||
|
function useDriveItemSearch() {
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const searchDriveItem = async (q: string) => {
|
||||||
|
const { data } = await axios.get<OdSearchResult>(`/api/search?q=${q}`)
|
||||||
|
|
||||||
|
// Extract the searched item's path and convert it to the absolute path in onedrive-vercel-index
|
||||||
|
function mapAbsolutePath(path: string): string {
|
||||||
|
return siteConfig.baseDirectory === '/' ? path.split('root:')[1] : path.split(siteConfig.baseDirectory)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map parentReference to the absolute path of the search result
|
||||||
|
data.map(item => {
|
||||||
|
item['path'] = `${mapAbsolutePath(item.parentReference.path)}/${encodeURIComponent(item.name)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedNotionSearch = useConstant(() => AwesomeDebouncePromise(searchDriveItem, 1000))
|
||||||
|
const results = useAsync(async () => {
|
||||||
|
if (query.length === 0) {
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
return debouncedNotionSearch(query)
|
||||||
|
}
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchModal({
|
||||||
|
searchOpen,
|
||||||
|
setSearchOpen,
|
||||||
|
}: {
|
||||||
|
searchOpen: boolean
|
||||||
|
setSearchOpen: Dispatch<SetStateAction<boolean>>
|
||||||
|
}) {
|
||||||
|
const closeSearchBox = () => setSearchOpen(false)
|
||||||
|
|
||||||
|
const { query, setQuery, results } = useDriveItemSearch()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition appear show={searchOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="inset-0 z-[200] fixed overflow-y-auto" onClose={closeSearchBox}>
|
||||||
|
<div className="min-h-screen text-center px-4">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-100"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="bg-white/80 inset-0 fixed dark:bg-gray-800/80" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-100"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div className="border rounded border-gray-400/30 shadow-xl my-12 text-left w-full max-w-3xl transform transition-all inline-block overflow-hidden">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="flex items-center space-x-4 dark:text-white border-b bg-gray-50 border-gray-400/30 p-4 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="search" className="w-4 h-4" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-box"
|
||||||
|
className="w-full bg-transparent focus:outline-none focus-visible:outline-none"
|
||||||
|
placeholder="Search ..."
|
||||||
|
value={query}
|
||||||
|
onChange={e => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">ESC</div>
|
||||||
|
</Dialog.Title>
|
||||||
|
|
||||||
|
<div className="bg-white dark:text-white dark:bg-gray-900 max-h-[80vh] overflow-x-hidden overflow-y-scroll">
|
||||||
|
{results.loading && (
|
||||||
|
<div className="text-center px-4 py-12 text-sm font-medium">
|
||||||
|
<LoadingIcon className="animate-spin w-4 h-4 mr-2 inline-block svg-inline--fa" />
|
||||||
|
<span>Loading ...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{results.error && (
|
||||||
|
<div className="text-center px-4 py-12 text-sm font-medium">Error: {results.error.message}</div>
|
||||||
|
)}
|
||||||
|
{results.result && (
|
||||||
|
<>
|
||||||
|
{results.result.length === 0 ? (
|
||||||
|
<div className="text-center px-4 py-12 text-sm font-medium">Nothing here.</div>
|
||||||
|
) : (
|
||||||
|
results.result.map(result => (
|
||||||
|
<Link href={result.path} key={result.id} passHref>
|
||||||
|
<div
|
||||||
|
className="flex items-center space-x-4 border-b cursor-pointer border-gray-400/30 px-4 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-850"
|
||||||
|
onClick={closeSearchBox}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={result.file ? getFileIcon(result.name) : ['far', 'folder']} />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium leading-8">{result.name}</div>
|
||||||
|
<div className="text-xs font-mono opacity-60 truncate overflow-hidden">
|
||||||
|
{decodeURIComponent(result.path)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchModal
|
||||||
Generated
+147
-1
@@ -14,6 +14,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"@headlessui/react": "^1.4.0",
|
"@headlessui/react": "^1.4.0",
|
||||||
|
"awesome-debounce-promise": "^2.1.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"emoji-regex": "^9.2.2",
|
"emoji-regex": "^9.2.2",
|
||||||
@@ -24,9 +25,11 @@
|
|||||||
"preview-office-docs": "^1.0.2",
|
"preview-office-docs": "^1.0.2",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.23.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-async-hook": "^4.0.0",
|
||||||
"react-copy-to-clipboard": "^5.0.3",
|
"react-copy-to-clipboard": "^5.0.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-hot-toast": "^2.0.0",
|
"react-hot-toast": "^2.0.0",
|
||||||
|
"react-hotkeys-hook": "^3.4.4",
|
||||||
"react-markdown": "^6.0.2",
|
"react-markdown": "^6.0.2",
|
||||||
"react-player": "^2.9.0",
|
"react-player": "^2.9.0",
|
||||||
"react-reader": "^0.20.4",
|
"react-reader": "^0.20.4",
|
||||||
@@ -36,7 +39,8 @@
|
|||||||
"remark-gfm": "^1.0.0",
|
"remark-gfm": "^1.0.0",
|
||||||
"remark-math": "^4.0.0",
|
"remark-math": "^4.0.0",
|
||||||
"swr": "^0.5.6",
|
"swr": "^0.5.6",
|
||||||
"use-clipboard-copy": "^0.2.0"
|
"use-clipboard-copy": "^0.2.0",
|
||||||
|
"use-constant": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.0.2",
|
"@types/crypto-js": "^4.0.2",
|
||||||
@@ -619,6 +623,11 @@
|
|||||||
"integrity": "sha512-sCVniU+h3GcGqxOmng11BRvf9TfN9yIs8KKjB8C8d75W69cpTfZG80gau9yTx5SxF3gvHGbJhdESzzvnjtf3Og==",
|
"integrity": "sha512-sCVniU+h3GcGqxOmng11BRvf9TfN9yIs8KKjB8C8d75W69cpTfZG80gau9yTx5SxF3gvHGbJhdESzzvnjtf3Og==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/debounce-promise": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/debounce-promise/-/debounce-promise-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-9SEVY3nsz+uMN2DwDocftB5TAgZe7D0cOzxxRhpotWs6T4QFqRaTXpXbOSzbk31/7iYcfCkJJPwWGzTxyuGhCg=="
|
||||||
|
},
|
||||||
"node_modules/@types/hast": {
|
"node_modules/@types/hast": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
|
||||||
@@ -1139,6 +1148,42 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/awesome-debounce-promise": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/awesome-debounce-promise/-/awesome-debounce-promise-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-0Dv4j2wKk5BrNZh4jgV2HUdznaeVgEK/WTvcHhZWUElhmQ1RR+iURRoLEwICFyR0S/5VtxfcvY6gR+qSe95jNg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/debounce-promise": "^3.1.1",
|
||||||
|
"awesome-imperative-promise": "^1.0.1",
|
||||||
|
"awesome-only-resolves-last-promise": "^1.0.3",
|
||||||
|
"debounce-promise": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/awesome-imperative-promise": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/awesome-imperative-promise/-/awesome-imperative-promise-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-EmPr3FqbQGqlNh+WxMNcF9pO9uDQJnOC4/3rLBQNH9m4E9qI+8lbfHCmHpVAsmGqPJPKhCjJLHUQzQW/RBHRdQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/awesome-only-resolves-last-promise": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/awesome-only-resolves-last-promise/-/awesome-only-resolves-last-promise-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-7q4WPsYiD8Omvi/yHL314DkvsD/lM//Z2/KcU1vWk0xJotiV0GMJTgHTpWl3n90HJqpXKg7qX+VVNs5YbQyPRQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"awesome-imperative-promise": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axe-core": {
|
"node_modules/axe-core": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz",
|
||||||
@@ -1804,6 +1849,11 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/debounce-promise": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||||
@@ -3397,6 +3447,11 @@
|
|||||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/hotkeys-js": {
|
||||||
|
"version": "3.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
|
||||||
|
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg=="
|
||||||
|
},
|
||||||
"node_modules/html-void-elements": {
|
"node_modules/html-void-elements": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.0.tgz",
|
||||||
@@ -5697,6 +5752,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-async-hook": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-async-hook/-/react-async-hook-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-97lgjFkOcHCTYSrsKBpsXg3iVWM0LnzedB749iP76sb3/8Ouu4nHIkCLEOrQWHVYqrYxjF05NN6GHoXWFkB3Kw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-copy-to-clipboard": {
|
"node_modules/react-copy-to-clipboard": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
|
||||||
@@ -5742,6 +5809,18 @@
|
|||||||
"react-dom": ">=16"
|
"react-dom": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hotkeys-hook": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"hotkeys-js": "3.8.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.1",
|
||||||
|
"react-dom": ">=16.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
@@ -7202,6 +7281,14 @@
|
|||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-constant": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-constant/-/use-constant-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-yrflEfv7Xv/W8WlYV6nwRH01K+2BpR4cWxuzY03yPRjYZuHixhGlvnJN5O2bRYrXGpJ4zy8QjFABGIQ2QXeBOA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-subscription": {
|
"node_modules/use-subscription": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz",
|
||||||
@@ -7798,6 +7885,11 @@
|
|||||||
"integrity": "sha512-sCVniU+h3GcGqxOmng11BRvf9TfN9yIs8KKjB8C8d75W69cpTfZG80gau9yTx5SxF3gvHGbJhdESzzvnjtf3Og==",
|
"integrity": "sha512-sCVniU+h3GcGqxOmng11BRvf9TfN9yIs8KKjB8C8d75W69cpTfZG80gau9yTx5SxF3gvHGbJhdESzzvnjtf3Og==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/debounce-promise": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/debounce-promise/-/debounce-promise-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-9SEVY3nsz+uMN2DwDocftB5TAgZe7D0cOzxxRhpotWs6T4QFqRaTXpXbOSzbk31/7iYcfCkJJPwWGzTxyuGhCg=="
|
||||||
|
},
|
||||||
"@types/hast": {
|
"@types/hast": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
|
||||||
@@ -8192,6 +8284,30 @@
|
|||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
||||||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
||||||
},
|
},
|
||||||
|
"awesome-debounce-promise": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/awesome-debounce-promise/-/awesome-debounce-promise-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-0Dv4j2wKk5BrNZh4jgV2HUdznaeVgEK/WTvcHhZWUElhmQ1RR+iURRoLEwICFyR0S/5VtxfcvY6gR+qSe95jNg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/debounce-promise": "^3.1.1",
|
||||||
|
"awesome-imperative-promise": "^1.0.1",
|
||||||
|
"awesome-only-resolves-last-promise": "^1.0.3",
|
||||||
|
"debounce-promise": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"awesome-imperative-promise": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/awesome-imperative-promise/-/awesome-imperative-promise-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-EmPr3FqbQGqlNh+WxMNcF9pO9uDQJnOC4/3rLBQNH9m4E9qI+8lbfHCmHpVAsmGqPJPKhCjJLHUQzQW/RBHRdQ=="
|
||||||
|
},
|
||||||
|
"awesome-only-resolves-last-promise": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/awesome-only-resolves-last-promise/-/awesome-only-resolves-last-promise-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-7q4WPsYiD8Omvi/yHL314DkvsD/lM//Z2/KcU1vWk0xJotiV0GMJTgHTpWl3n90HJqpXKg7qX+VVNs5YbQyPRQ==",
|
||||||
|
"requires": {
|
||||||
|
"awesome-imperative-promise": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"axe-core": {
|
"axe-core": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz",
|
||||||
@@ -8710,6 +8826,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
||||||
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="
|
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="
|
||||||
},
|
},
|
||||||
|
"debounce-promise": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||||
@@ -9931,6 +10052,11 @@
|
|||||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"hotkeys-js": {
|
||||||
|
"version": "3.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.8.7.tgz",
|
||||||
|
"integrity": "sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg=="
|
||||||
|
},
|
||||||
"html-void-elements": {
|
"html-void-elements": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.0.tgz",
|
||||||
@@ -11620,6 +11746,12 @@
|
|||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-async-hook": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-async-hook/-/react-async-hook-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-97lgjFkOcHCTYSrsKBpsXg3iVWM0LnzedB749iP76sb3/8Ouu4nHIkCLEOrQWHVYqrYxjF05NN6GHoXWFkB3Kw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-copy-to-clipboard": {
|
"react-copy-to-clipboard": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz",
|
||||||
@@ -11652,6 +11784,14 @@
|
|||||||
"goober": "^2.0.35"
|
"goober": "^2.0.35"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-hotkeys-hook": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==",
|
||||||
|
"requires": {
|
||||||
|
"hotkeys-js": "3.8.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
@@ -12741,6 +12881,12 @@
|
|||||||
"clipboard-copy": "^3.0.0"
|
"clipboard-copy": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"use-constant": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-constant/-/use-constant-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-yrflEfv7Xv/W8WlYV6nwRH01K+2BpR4cWxuzY03yPRjYZuHixhGlvnJN5O2bRYrXGpJ4zy8QjFABGIQ2QXeBOA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"use-subscription": {
|
"use-subscription": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz",
|
||||||
|
|||||||
+5
-1
@@ -15,6 +15,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"@headlessui/react": "^1.4.0",
|
"@headlessui/react": "^1.4.0",
|
||||||
|
"awesome-debounce-promise": "^2.1.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"emoji-regex": "^9.2.2",
|
"emoji-regex": "^9.2.2",
|
||||||
@@ -25,9 +26,11 @@
|
|||||||
"preview-office-docs": "^1.0.2",
|
"preview-office-docs": "^1.0.2",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.23.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-async-hook": "^4.0.0",
|
||||||
"react-copy-to-clipboard": "^5.0.3",
|
"react-copy-to-clipboard": "^5.0.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-hot-toast": "^2.0.0",
|
"react-hot-toast": "^2.0.0",
|
||||||
|
"react-hotkeys-hook": "^3.4.4",
|
||||||
"react-markdown": "^6.0.2",
|
"react-markdown": "^6.0.2",
|
||||||
"react-player": "^2.9.0",
|
"react-player": "^2.9.0",
|
||||||
"react-reader": "^0.20.4",
|
"react-reader": "^0.20.4",
|
||||||
@@ -37,7 +40,8 @@
|
|||||||
"remark-gfm": "^1.0.0",
|
"remark-gfm": "^1.0.0",
|
||||||
"remark-math": "^4.0.0",
|
"remark-math": "^4.0.0",
|
||||||
"swr": "^0.5.6",
|
"swr": "^0.5.6",
|
||||||
"use-clipboard-copy": "^0.2.0"
|
"use-clipboard-copy": "^0.2.0",
|
||||||
|
"use-constant": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.0.2",
|
"@types/crypto-js": "^4.0.2",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
} from '@fortawesome/free-regular-svg-icons'
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
import {
|
import {
|
||||||
|
faSearch,
|
||||||
faCheck,
|
faCheck,
|
||||||
faPlus,
|
faPlus,
|
||||||
faMinus,
|
faMinus,
|
||||||
@@ -92,6 +93,7 @@ library.add(
|
|||||||
faHome,
|
faHome,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
|
faSearch,
|
||||||
...iconList
|
...iconList
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import { getOdAuthTokens, storeOdAuthTokens } from '../../utils/odAuthTokenStore
|
|||||||
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
const basePath = pathPosix.resolve('/', siteConfig.baseDirectory)
|
||||||
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
const clientSecret = revealObfuscatedToken(apiConfig.obfuscatedClientSecret)
|
||||||
|
|
||||||
const encodePath = (path: string) => {
|
export function encodePath(path: string): string {
|
||||||
let encodedPath = pathPosix.join(basePath, pathPosix.resolve('/', path))
|
let encodedPath = pathPosix.join(basePath, pathPosix.resolve('/', path))
|
||||||
if (encodedPath === '/' || encodedPath === '') {
|
if (encodedPath === '/' || encodedPath === '') {
|
||||||
return ''
|
return ''
|
||||||
@@ -21,7 +21,7 @@ const encodePath = (path: string) => {
|
|||||||
return `:${encodeURIComponent(encodedPath)}`
|
return `:${encodeURIComponent(encodedPath)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAccessToken(): Promise<any> {
|
export async function getAccessToken(): Promise<string> {
|
||||||
const { accessToken, refreshToken } = await getOdAuthTokens()
|
const { accessToken, refreshToken } = await getOdAuthTokens()
|
||||||
|
|
||||||
// Return in storage access token if it is still valid
|
// Return in storage access token if it is still valid
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
import { encodePath, getAccessToken } from '.'
|
||||||
|
import apiConfig from '../../config/api.json'
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// Get access token from storage
|
||||||
|
const accessToken = await getAccessToken()
|
||||||
|
|
||||||
|
// Query parameter from request
|
||||||
|
const { q: searchQuery = '' } = req.query
|
||||||
|
|
||||||
|
if (typeof searchQuery === 'string') {
|
||||||
|
// Construct Microsoft Graph Search API URL, and perform search only under the base dir
|
||||||
|
const encodedPath = encodePath('/') === '' ? encodePath('/') : encodePath('/') + ':'
|
||||||
|
const searchApi = `${apiConfig.driveApi}/root${encodedPath}/search(q='${encodeURIComponent(searchQuery)}')`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(searchApi, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
params: {
|
||||||
|
select: 'id,name,file,folder,parentReference',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
res.status(200).json(data.value)
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(error.response.status).json({ error: error.response.data })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(200).json([])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
Vendored
+31
@@ -13,3 +13,34 @@ export type OdFileObject = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OdFolderObject = {
|
||||||
|
'@odata.count': number
|
||||||
|
value: Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
lastModifiedDateTime: string
|
||||||
|
size: number
|
||||||
|
folder: {
|
||||||
|
childCount: number
|
||||||
|
view: {
|
||||||
|
sortBy: 'name'
|
||||||
|
sortOrder: 'ascending'
|
||||||
|
viewType: 'thumbnails'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OdSearchResult = Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
file?: OdFileObject
|
||||||
|
folder?: OdFolderObject
|
||||||
|
path: string
|
||||||
|
parentReference: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export default function useDeviceOS(): string {
|
||||||
|
const [os, setOS] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userAgent = window.navigator.userAgent
|
||||||
|
|
||||||
|
if (userAgent.indexOf('Windows') > -1) {
|
||||||
|
setOS('windows')
|
||||||
|
} else if (userAgent.indexOf('Mac OS') > -1) {
|
||||||
|
setOS('mac')
|
||||||
|
} else if (userAgent.indexOf('Linux') > -1) {
|
||||||
|
setOS('linux')
|
||||||
|
} else {
|
||||||
|
setOS('other')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return os
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user