add search feature and garbage mobile support
This commit is contained in:
parent
9e93907c80
commit
c8fd4148ef
18 changed files with 600 additions and 230 deletions
42
package-lock.json
generated
42
package-lock.json
generated
|
@ -8,13 +8,16 @@
|
||||||
"name": "eexiv-2",
|
"name": "eexiv-2",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"minisearch": "^6.3.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-icons": "^5.0.1",
|
||||||
"react-toastify": "^10.0.4",
|
"react-toastify": "^10.0.4",
|
||||||
"zustand": "^4.5.0"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@redux-devtools/extension": "^3.3.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
|
@ -436,6 +439,19 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@redux-devtools/extension": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.23.2",
|
||||||
|
"immutable": "^4.3.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^3.1.0 || ^4.0.0 || ^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rushstack/eslint-patch": {
|
"node_modules/@rushstack/eslint-patch": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz",
|
||||||
|
@ -2413,6 +2429,12 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immutable": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
|
@ -3067,6 +3089,11 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minisearch": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ=="
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -3725,6 +3752,14 @@
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-icons": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
@ -3764,6 +3799,13 @@
|
||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
|
||||||
|
|
|
@ -10,13 +10,16 @@
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"minisearch": "^6.3.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-icons": "^5.0.1",
|
||||||
"react-toastify": "^10.0.4",
|
"react-toastify": "^10.0.4",
|
||||||
"zustand": "^4.5.0"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@redux-devtools/extension": "^3.3.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
|
|
BIN
public/img/logos/fia.jpg
Normal file
BIN
public/img/logos/fia.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 368 KiB |
65
src/app/components/Badges.tsx
Normal file
65
src/app/components/Badges.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { DocumentStatus, DocumentType } from '@/app/db/data'
|
||||||
|
|
||||||
|
export const Status = ({
|
||||||
|
statusName,
|
||||||
|
}: Readonly<{ statusName: DocumentStatus }>) => {
|
||||||
|
let text = ''
|
||||||
|
let itemStyle: string = ''!
|
||||||
|
switch (statusName) {
|
||||||
|
case 'draft':
|
||||||
|
text = 'Draft'
|
||||||
|
itemStyle += 'badge-draft'
|
||||||
|
break
|
||||||
|
case 'published no review':
|
||||||
|
text = 'Published'
|
||||||
|
itemStyle += 'badge-published'
|
||||||
|
break
|
||||||
|
case 'reviewed':
|
||||||
|
text = 'Peer Reviewed'
|
||||||
|
itemStyle += 'badge-reviewed'
|
||||||
|
break
|
||||||
|
case 'under review':
|
||||||
|
text = 'Pending Review'
|
||||||
|
itemStyle = 'badge-under-review'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return <span className={itemStyle}>{text}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemBadge = ({
|
||||||
|
itemName,
|
||||||
|
}: Readonly<{ itemName: DocumentType }>) => {
|
||||||
|
let text = ''
|
||||||
|
let itemStyle: string = ''!
|
||||||
|
switch (itemName) {
|
||||||
|
case 'report':
|
||||||
|
itemStyle = 'badge-report'
|
||||||
|
text = 'Report'
|
||||||
|
break
|
||||||
|
case 'presentation':
|
||||||
|
text = 'Presentation'
|
||||||
|
itemStyle = 'badge-presentation'
|
||||||
|
break
|
||||||
|
case 'white paper':
|
||||||
|
text = 'White Paper'
|
||||||
|
itemStyle = 'badge-white-paper'
|
||||||
|
break
|
||||||
|
case 'datasheet':
|
||||||
|
text = 'Datasheet'
|
||||||
|
itemStyle = 'badge-datasheet'
|
||||||
|
break
|
||||||
|
case 'dwm':
|
||||||
|
text = 'DWM'
|
||||||
|
itemStyle = 'badge-dwm'
|
||||||
|
break
|
||||||
|
case 'guide':
|
||||||
|
text = 'Guide'
|
||||||
|
itemStyle = 'badge-guide'
|
||||||
|
break
|
||||||
|
case 'other':
|
||||||
|
text = 'Other'
|
||||||
|
itemStyle = 'badge-other'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return <span className={itemStyle}>{text}</span>
|
||||||
|
}
|
133
src/app/components/DataDisplay.tsx
Normal file
133
src/app/components/DataDisplay.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import {
|
||||||
|
topics as topicList,
|
||||||
|
authors as authorList,
|
||||||
|
reviewer,
|
||||||
|
} from '@/app/db/data'
|
||||||
|
|
||||||
|
export const Code = ({
|
||||||
|
code,
|
||||||
|
showTitle = true,
|
||||||
|
}: {
|
||||||
|
code: string[] | undefined
|
||||||
|
showTitle?: boolean
|
||||||
|
}) => {
|
||||||
|
if (code) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showTitle ? <span className='font-bold'>Code: </span> : null}
|
||||||
|
{code.map((c: string, i) => (
|
||||||
|
<Fragment key={c}>
|
||||||
|
<Link href={c} target='_blank'>
|
||||||
|
{c}
|
||||||
|
</Link>
|
||||||
|
{i !== code.length - 1 ? ', ' : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const References = ({
|
||||||
|
references,
|
||||||
|
showTitle = true,
|
||||||
|
}: Readonly<{ references: string[] | undefined; showTitle?: boolean }>) => {
|
||||||
|
if (!references) return null
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showTitle ? <span className='font-bold'>References: </span> : null}
|
||||||
|
{references.map((r: string, i) => (
|
||||||
|
<Fragment key={r}>
|
||||||
|
<Link href={r} target='_blank'>
|
||||||
|
{r}
|
||||||
|
</Link>
|
||||||
|
{i !== references.length - 1 ? ', ' : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Topics = ({
|
||||||
|
topics,
|
||||||
|
showTitle = true,
|
||||||
|
}: Readonly<{ topics: string[]; showTitle?: boolean }>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showTitle ? <span className='font-bold'>Topics: </span> : null}
|
||||||
|
{topics.map((t: string, i) => (
|
||||||
|
<Fragment key={t}>
|
||||||
|
<Link href={topicList[t].wiki} target='_blank'>
|
||||||
|
{topicList[t].name}
|
||||||
|
</Link>
|
||||||
|
{i !== topics.length - 1 ? ', ' : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Authors = ({
|
||||||
|
authors,
|
||||||
|
}: Readonly<{ authors: string[]; noLink?: boolean }>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{authors.map((a: string, i) => (
|
||||||
|
<Fragment key={a}>
|
||||||
|
<Link href={`/author/${a}`} target='_blank'>
|
||||||
|
{authorList[a].name.first} {authorList[a].name.last}
|
||||||
|
</Link>
|
||||||
|
{i !== authors.length - 1 && authors.length > 2 ? ', ' : null}
|
||||||
|
{i === authors.length - 2 ? ' and ' : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Reviewers = ({
|
||||||
|
reviewers,
|
||||||
|
showTitle = true,
|
||||||
|
}: Readonly<{ reviewers: reviewer[] | undefined; showTitle?: boolean }>) => {
|
||||||
|
if (!reviewers) return null
|
||||||
|
const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => {
|
||||||
|
if (r.profile) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link href={`/author/${r.profile}`} target='_blank'>
|
||||||
|
{r.first} {r.last}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (r.url) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href={r.url} target='_blank'>
|
||||||
|
{r.first} {r.last}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{r.first} {r.last}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showTitle ? <span className='font-bold'>Reviewed by: </span> : null}
|
||||||
|
{reviewers.map((r: reviewer, i) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<ReviewerDisplay r={r} />
|
||||||
|
{i !== reviewers.length - 1 && reviewers.length > 2 ? ', ' : null}
|
||||||
|
{i === reviewers.length - 2 ? ' and ' : null}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
61
src/app/components/MobileMenu.tsx
Normal file
61
src/app/components/MobileMenu.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
'use client'
|
||||||
|
import { RxHamburgerMenu } from 'react-icons/rx'
|
||||||
|
import styles from './mobileMenu.module.css'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import SearchBar from '@/app/components/SearchBar'
|
||||||
|
|
||||||
|
interface MobileMenuState {
|
||||||
|
isOpen: boolean
|
||||||
|
searchInput: string
|
||||||
|
setSearchInput: (newInput: string) => void
|
||||||
|
setIsOpen: (newState: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMobileMenuState = create<MobileMenuState>((set) => ({
|
||||||
|
isOpen: false,
|
||||||
|
searchInput: '',
|
||||||
|
setSearchInput: (newInput: string) => set({ searchInput: newInput }),
|
||||||
|
setIsOpen: (newState: boolean) => set({ isOpen: newState }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function MobileMenu() {
|
||||||
|
const isOpen = useMobileMenuState((state) => state.isOpen)
|
||||||
|
const setIsOpen = useMobileMenuState((state) => state.setIsOpen)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'auto'
|
||||||
|
setIsOpen(false)
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
setIsOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-20'>
|
||||||
|
<button
|
||||||
|
className='p-2 rounded-xl hover:bg-blue-400'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<RxHamburgerMenu size={40} />
|
||||||
|
</button>
|
||||||
|
<div className={`${isOpen ? '' : styles['menu-hidden']} ${styles.menu}`}>
|
||||||
|
<span className={styles['search-bar']}>
|
||||||
|
<SearchBar />
|
||||||
|
</span>
|
||||||
|
<p className='text-slate-600 mx-4 my-4'>
|
||||||
|
We gratefully acknowledge support from our volunteer peer reviewers,
|
||||||
|
member institutions, and all{' '}
|
||||||
|
<a
|
||||||
|
href='https://github.com/couscousdude/eeXiv-2/graphs/contributors'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
open-source contributors
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
55
src/app/components/SearchBar.tsx
Normal file
55
src/app/components/SearchBar.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
'use client'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { navigate } from '@/app/actions'
|
||||||
|
|
||||||
|
interface SearchBarState {
|
||||||
|
searchInput: string
|
||||||
|
setSearchInput: (newInput: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSearchBarStore = create<SearchBarState>((set) => ({
|
||||||
|
searchInput: '',
|
||||||
|
setSearchInput: (newInput) => set({ searchInput: newInput }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function SearchBar() {
|
||||||
|
const searchBarStore = useSearchBarStore((state) => state.searchInput)
|
||||||
|
const setSearchBarStore = useSearchBarStore((state) => state.setSearchInput)
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
navigate(`/search?q=${searchBarStore.split(' ').join('+')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchBarStore(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
navigate(`/search?q=${searchBarStore.split(' ').join('+')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-nowrap'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='py-3 px-5 rounded-xl text-slate-800 flex-grow'
|
||||||
|
name='q'
|
||||||
|
placeholder='Search...'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={searchBarStore}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type='submit'
|
||||||
|
className='p-2.5 mx-4 border-2 rounded-xl hover:bg-blue-300 flex-shrink'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
16
src/app/components/mobileMenu.module.css
Normal file
16
src/app/components/mobileMenu.module.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.menu {
|
||||||
|
@apply w-[100vw] h-full overflow-x-hidden left-[0] top-[235px] z-10 absolute bg-slate-200;
|
||||||
|
@apply transition-transform duration-300 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
@apply w-full flex justify-center z-10 px-4 py-2 bg-slate-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
@apply border-slate-600 text-slate-600 bg-slate-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-hidden {
|
||||||
|
@apply translate-y-[100vh] invisible;
|
||||||
|
}
|
|
@ -47,9 +47,13 @@ export type DocumentStatus =
|
||||||
| 'under review'
|
| 'under review'
|
||||||
| 'reviewed'
|
| 'reviewed'
|
||||||
| 'published no review'
|
| 'published no review'
|
||||||
export interface DocumentDB {
|
export interface Document {
|
||||||
[key: string]: {
|
manifest: DocumentManifest
|
||||||
manifest: {
|
abstract: string
|
||||||
|
file: FileType
|
||||||
|
citation?: string
|
||||||
|
}
|
||||||
|
export interface DocumentManifest {
|
||||||
title: string
|
title: string
|
||||||
authors: string[]
|
authors: string[]
|
||||||
topics: string[]
|
topics: string[]
|
||||||
|
@ -58,16 +62,11 @@ export interface DocumentDB {
|
||||||
code?: string[]
|
code?: string[]
|
||||||
type: DocumentType
|
type: DocumentType
|
||||||
latest: number
|
latest: number
|
||||||
keywords?: string[]
|
keywords: string[]
|
||||||
status: DocumentStatus
|
status: DocumentStatus
|
||||||
reviewers?: reviewer[]
|
reviewers?: reviewer[]
|
||||||
}
|
|
||||||
abstract: string
|
|
||||||
file: FileType
|
|
||||||
citation?: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const documents: DocumentDB = {
|
export const documents: { [key: string]: Document } = {
|
||||||
'day-5-principles': {
|
'day-5-principles': {
|
||||||
manifest: {
|
manifest: {
|
||||||
title: 'Day 5 Principles',
|
title: 'Day 5 Principles',
|
||||||
|
@ -486,6 +485,20 @@ export const authors: Authors = {
|
||||||
nationality: ['chn', 'usa'],
|
nationality: ['chn', 'usa'],
|
||||||
image: '/img/profiles/nluo.png',
|
image: '/img/profiles/nluo.png',
|
||||||
},
|
},
|
||||||
|
jchan: {
|
||||||
|
name: {
|
||||||
|
first: 'Jason',
|
||||||
|
last: 'Chan',
|
||||||
|
},
|
||||||
|
affiliation: [
|
||||||
|
'Chief Executive Officer@fia',
|
||||||
|
'Intern@1280-eecs',
|
||||||
|
'Student@srvhs',
|
||||||
|
],
|
||||||
|
nationality: ['jpn', 'chn', 'usa'],
|
||||||
|
image: '/img/profiles/default.png',
|
||||||
|
website: 'https://futureinspireacademy.com',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Affiliations {
|
export interface Affiliations {
|
||||||
|
@ -636,6 +649,16 @@ Raid Zero's influence extends beyond the technical achievements in robotics comp
|
||||||
[linebreak]
|
[linebreak]
|
||||||
Gonzaga University's School of Engineering and Applied Science stands out for its commitment to excellence, ethics, and the holistic development of its students. Graduates leave Gonzaga not only as skilled engineers but as compassionate leaders ready to make meaningful contributions to society.`,
|
Gonzaga University's School of Engineering and Applied Science stands out for its commitment to excellence, ethics, and the holistic development of its students. Graduates leave Gonzaga not only as skilled engineers but as compassionate leaders ready to make meaningful contributions to society.`,
|
||||||
},
|
},
|
||||||
|
fia: {
|
||||||
|
name: 'Future Inspire Academy',
|
||||||
|
short: 'FIA',
|
||||||
|
image: '/img/logos/fia.jpg',
|
||||||
|
description: `
|
||||||
|
Future Inspire Academy (FIA) is a non-profit organization that strives to teach students how to apply their coding skills to game development in a fun and efficient way. Our mission is to create a platform to reward members who start their game development journey early. We give members all the resources to learn quickly and reward their efforts with points which can be used to upgrade their game jam prizes. Become a member today and reap all the benefits by joining our Discord Server!
|
||||||
|
[linebreak]
|
||||||
|
Our organization not only impacts our members from around the world but also our partners as we help promote their business and improve their products. Our new vision has been to help develop companies that would contribute to the future of game development and promote accessibility. Recently, we launched exclusive early access to Rosebud’s game maker platform for all of our members to try. In the future, we plan to host more exclusive events that revolve around our partners.
|
||||||
|
`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Nationalities {
|
export interface Nationalities {
|
||||||
|
@ -686,4 +709,9 @@ export const nationalities: Nationalities = {
|
||||||
demonym: 'Russian',
|
demonym: 'Russian',
|
||||||
flag: 'https://upload.wikimedia.org/wikipedia/en/thumb/f/f3/Flag_of_Russia.svg/2560px-Flag_of_Russia.svg.png',
|
flag: 'https://upload.wikimedia.org/wikipedia/en/thumb/f/f3/Flag_of_Russia.svg/2560px-Flag_of_Russia.svg.png',
|
||||||
},
|
},
|
||||||
|
jpn: {
|
||||||
|
name: 'Japan',
|
||||||
|
demonym: 'Japanese',
|
||||||
|
flag: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Flag_of_Japan.svg/1280px-Flag_of_Japan.svg.png',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { Zilla_Slab } from 'next/font/google'
|
import { Zilla_Slab } from 'next/font/google'
|
||||||
import {
|
import { DocumentType, documents, reviewer } from '@/app/db/data'
|
||||||
DocumentType,
|
|
||||||
documents,
|
|
||||||
topics as topicList,
|
|
||||||
authors as authorList,
|
|
||||||
DocumentStatus,
|
|
||||||
reviewer,
|
|
||||||
} from '@/app/db/data'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import { Fragment, useEffect } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import { epoch2datestring } from '@/app/utils/epoch2datestring'
|
import { epoch2datestring } from '@/app/utils/epoch2datestring'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { ItemBadge, Status } from '@/app/components/Badges'
|
||||||
|
import {
|
||||||
|
Code,
|
||||||
|
References,
|
||||||
|
Topics,
|
||||||
|
Authors,
|
||||||
|
Reviewers,
|
||||||
|
} from '@/app/components/DataDisplay'
|
||||||
|
|
||||||
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
||||||
|
|
||||||
|
@ -40,181 +41,12 @@ export default function Page({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'reviewed' && !reviewers) {
|
if (status === 'reviewed' && !reviewers) {
|
||||||
toast.warn(
|
toast.warn(
|
||||||
`This document is marked reviewed, but the author
|
`This document is marked as peer reviewed, but the author
|
||||||
forgot to add a list of reviewers.`
|
forgot to add a list of reviewers.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const Topics = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className='font-bold'>Topics: </span>
|
|
||||||
{topics.map((t: string, i) => (
|
|
||||||
<Fragment key={t}>
|
|
||||||
<Link href={topicList[t].wiki} target='_blank'>
|
|
||||||
{topicList[t].name}
|
|
||||||
</Link>
|
|
||||||
{i !== topics.length - 1 ? ', ' : null}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Code = () => {
|
|
||||||
if (code) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className='font-bold'>Code: </span>
|
|
||||||
{code.map((c: string, i) => (
|
|
||||||
<Fragment key={c}>
|
|
||||||
<Link href={c} target='_blank'>
|
|
||||||
{c}
|
|
||||||
</Link>
|
|
||||||
{i !== code.length - 1 ? ', ' : null}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Authors = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{authors.map((a: string, i) => (
|
|
||||||
<Fragment key={a}>
|
|
||||||
<Link href={`/author/${a}`} target='_blank'>
|
|
||||||
{authorList[a].name.first} {authorList[a].name.last}
|
|
||||||
</Link>
|
|
||||||
{i !== authors.length - 1 && authors.length > 2 ? ', ' : null}
|
|
||||||
{i === authors.length - 2 ? ' and ' : null}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const References = () => {
|
|
||||||
if (!references) return null
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className='font-bold'>References: </span>
|
|
||||||
{references.map((r: string, i) => (
|
|
||||||
<Fragment key={r}>
|
|
||||||
<Link href={r} target='_blank'>
|
|
||||||
{r}
|
|
||||||
</Link>
|
|
||||||
{i !== references.length - 1 ? ', ' : null}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Status = ({ statusName }: Readonly<{ statusName: DocumentStatus }>) => {
|
|
||||||
let text = ''
|
|
||||||
let itemStyle: string = ''!
|
|
||||||
switch (statusName) {
|
|
||||||
case 'draft':
|
|
||||||
text = 'Draft'
|
|
||||||
itemStyle += 'badge-draft'
|
|
||||||
break
|
|
||||||
case 'published no review':
|
|
||||||
text = 'Published'
|
|
||||||
itemStyle += 'badge-published'
|
|
||||||
break
|
|
||||||
case 'reviewed':
|
|
||||||
text = 'Peer Reviewed'
|
|
||||||
itemStyle += 'badge-reviewed'
|
|
||||||
break
|
|
||||||
case 'under review':
|
|
||||||
text = 'Pending Review'
|
|
||||||
itemStyle = 'badge-under-review'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return <span className={itemStyle}>{text}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemBadge = ({ itemName }: Readonly<{ itemName: DocumentType }>) => {
|
|
||||||
let text = ''
|
|
||||||
let itemStyle: string = ''!
|
|
||||||
switch (itemName) {
|
|
||||||
case 'report':
|
|
||||||
itemStyle = 'badge-report'
|
|
||||||
text = 'Report'
|
|
||||||
break
|
|
||||||
case 'presentation':
|
|
||||||
text = 'Presentation'
|
|
||||||
itemStyle = 'badge-presentation'
|
|
||||||
break
|
|
||||||
case 'white paper':
|
|
||||||
text = 'White Paper'
|
|
||||||
itemStyle = 'badge-white-paper'
|
|
||||||
break
|
|
||||||
case 'datasheet':
|
|
||||||
text = 'Datasheet'
|
|
||||||
itemStyle = 'badge-datasheet'
|
|
||||||
break
|
|
||||||
case 'dwm':
|
|
||||||
text = 'DWM'
|
|
||||||
itemStyle = 'badge-dwm'
|
|
||||||
break
|
|
||||||
case 'guide':
|
|
||||||
text = 'Guide'
|
|
||||||
itemStyle = 'badge-guide'
|
|
||||||
break
|
|
||||||
case 'other':
|
|
||||||
text = 'Other'
|
|
||||||
itemStyle = 'badge-other'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return <span className={itemStyle}>{text}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Reviewers = () => {
|
|
||||||
if (!reviewers) return null
|
|
||||||
const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => {
|
|
||||||
if (r.profile) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Link href={`/author/${r.profile}`} target='_blank'>
|
|
||||||
{r.first} {r.last}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (r.url) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a href={r.url} target='_blank'>
|
|
||||||
{r.first} {r.last}
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{r.first} {r.last}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className='font-bold'>Reviewers: </span>
|
|
||||||
{reviewers.map((r: reviewer, i) => (
|
|
||||||
<Fragment key={i}>
|
|
||||||
<ReviewerDisplay r={r} />
|
|
||||||
{i !== reviewers.length - 1 && reviewers.length > 2 ? ', ' : null}
|
|
||||||
{i === reviewers.length - 2 ? ' and ' : null}
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
||||||
<h1
|
<h1
|
||||||
|
@ -227,9 +59,9 @@ export default function Page({
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className={`text-slate-800 mt-2`}>
|
<p className={`text-slate-800 mt-2`}>
|
||||||
<Authors />
|
<Authors authors={authors} />
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-4'>
|
<p className='mt-4 mb-2'>
|
||||||
Latest revision published{' '}
|
Latest revision published{' '}
|
||||||
<span className='font-bold'>
|
<span className='font-bold'>
|
||||||
{epoch2datestring(dates[dates.length - 1])}
|
{epoch2datestring(dates[dates.length - 1])}
|
||||||
|
@ -246,16 +78,16 @@ export default function Page({
|
||||||
{abstract}
|
{abstract}
|
||||||
</p>
|
</p>
|
||||||
<p className='my-2'>
|
<p className='my-2'>
|
||||||
<Topics />
|
<Topics topics={topics} />
|
||||||
</p>
|
</p>
|
||||||
<p className='my-2'>
|
<p className='my-2'>
|
||||||
<Code />
|
<Code code={code} />
|
||||||
</p>
|
</p>
|
||||||
<p className='my-2'>
|
<p className='my-2'>
|
||||||
<References />
|
<References references={references} />
|
||||||
</p>
|
</p>
|
||||||
<p className='my-2'>
|
<p className='my-2'>
|
||||||
<Reviewers />
|
<Reviewers reviewers={reviewers} />
|
||||||
</p>
|
</p>
|
||||||
<p className='my-2'>
|
<p className='my-2'>
|
||||||
<span className='font-bold'>Cite as: </span>
|
<span className='font-bold'>Cite as: </span>
|
||||||
|
|
|
@ -25,7 +25,7 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-base {
|
.badge-base {
|
||||||
@apply px-3 py-1.5 rounded inline-block w-fit mr-2 mt-4 text-slate-50 border-2;
|
@apply px-3 py-1.5 rounded inline-block w-fit mr-2 my-1 text-slate-50 border-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-draft {
|
.badge-draft {
|
||||||
|
|
|
@ -11,6 +11,18 @@ export default function Page() {
|
||||||
<a href='https://github.com/Team-1280/eeXiv/pull/new'>pull request</a>{' '}
|
<a href='https://github.com/Team-1280/eeXiv/pull/new'>pull request</a>{' '}
|
||||||
on GitHub.
|
on GitHub.
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
It has also come to our attention that we may not be able to support
|
||||||
|
low-spec devices such as old phones, computers, or other devices with
|
||||||
|
little RAM. This is because we load the entire database of documents,
|
||||||
|
authors, topics, affiliations, and other data/metadata directly into
|
||||||
|
memory via JavaScript. As a result, the site may be slow or unusable on
|
||||||
|
low-spec devices and it can only get worse. If you would like to remedy
|
||||||
|
this issue, we again recommend you open a{' '}
|
||||||
|
<a href='https://github.com/Team-1280/eeXiv/pull/new'>pull request</a>{' '}
|
||||||
|
and port our in memory database to an actual remote database.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { Inter, Zilla_Slab } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import styles from './home.module.css'
|
import styles from './home.module.css'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import SearchBar from './searchBar/SearchBar'
|
import SearchBar from './components/SearchBar'
|
||||||
import Container from './container/Container'
|
import Container from './container/Container'
|
||||||
|
import MobileMenu from './components/MobileMenu'
|
||||||
import { ToastContainer } from 'react-toastify'
|
import { ToastContainer } from 'react-toastify'
|
||||||
import 'react-toastify/dist/ReactToastify.css'
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
|
|
||||||
|
@ -63,6 +64,9 @@ export default function RootLayout({
|
||||||
<div className='hidden md:inline'>
|
<div className='hidden md:inline'>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='md:hidden'>
|
||||||
|
<MobileMenu />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Container>{children}</Container>
|
<Container>{children}</Container>
|
||||||
|
|
|
@ -48,6 +48,9 @@ export default function Home() {
|
||||||
case 'dwm':
|
case 'dwm':
|
||||||
typeString = 'DWM'
|
typeString = 'DWM'
|
||||||
break
|
break
|
||||||
|
case 'guide':
|
||||||
|
typeString = 'guide'
|
||||||
|
break
|
||||||
case 'other':
|
case 'other':
|
||||||
typeString = 'document'
|
typeString = 'document'
|
||||||
break
|
break
|
||||||
|
|
92
src/app/search/page.tsx
Normal file
92
src/app/search/page.tsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
'use client'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { Zilla_Slab } from 'next/font/google'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Topics, Authors } from '@/app/components/DataDisplay'
|
||||||
|
import { Status, ItemBadge } from '@/app/components/Badges'
|
||||||
|
import { epoch2datestring } from '../utils/epoch2datestring'
|
||||||
|
import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { navigate } from '@/app/actions'
|
||||||
|
|
||||||
|
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] })
|
||||||
|
|
||||||
|
interface SearchState {
|
||||||
|
results: CustomSearchResult[]
|
||||||
|
setResults: (newResults: CustomSearchResult[]) => void
|
||||||
|
}
|
||||||
|
const useSearchStore = create<SearchState>((set) => ({
|
||||||
|
results: [],
|
||||||
|
setResults: (newResults: CustomSearchResult[]) =>
|
||||||
|
set({ results: newResults }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const SearchResult = ({ result }: { result: CustomSearchResult }) => {
|
||||||
|
const { manifest, abstract, id } = result
|
||||||
|
const { title, authors, topics, dates, status, type } = manifest
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
let target = event.target as HTMLElement
|
||||||
|
while (target != null) {
|
||||||
|
if (target.nodeName === 'A') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target = target.parentNode as HTMLElement
|
||||||
|
}
|
||||||
|
navigate(`/document/view/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='border-4 rounded-lg border-gray-300 hover:border-blue-500 p-5 my-4 w-full cursor-pointer'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<h2 className={`${zillaSlab.className} text-3xl`}>{title}</h2>
|
||||||
|
<p className='text-slate-500 py-2 text-md mt-2'>
|
||||||
|
<Authors authors={authors} noLink />
|
||||||
|
</p>
|
||||||
|
<p className='mb-2 text-slate-700 text-md'>
|
||||||
|
Last updated on {epoch2datestring(dates[dates.length - 1])}
|
||||||
|
</p>
|
||||||
|
<p className='mb-2'>
|
||||||
|
<Topics topics={topics} showTitle />
|
||||||
|
</p>
|
||||||
|
<div className='mb-4'>
|
||||||
|
<ItemBadge itemName={type} /> <Status statusName={status} />
|
||||||
|
</div>
|
||||||
|
<h2 className={`${zillaSlab.className} text-2xl`}>Abstract</h2>
|
||||||
|
<p className='py-2 text-md text-slate-700 font-serif text-lg text-balance'>
|
||||||
|
{abstract}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const search = searchParams.get('q') as string
|
||||||
|
const searchStore = useSearchStore((state) => state.results)
|
||||||
|
const setSearchStore = useSearchStore((state) => state.setResults)
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchStore(searchDocs(search))
|
||||||
|
}, [search])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='max-w-4xl mx-auto'>
|
||||||
|
<h1 className='text-xl mb-4 p-2'>
|
||||||
|
<span className='font-bold'>Showing results for: </span>
|
||||||
|
{`"`}
|
||||||
|
{search}
|
||||||
|
{`"`}
|
||||||
|
</h1>
|
||||||
|
{searchStore.length === 0 ? (
|
||||||
|
<p className='text-lg px-2'>No results found.</p>
|
||||||
|
) : (
|
||||||
|
searchStore.map((result) => (
|
||||||
|
<SearchResult key={result.id} result={result} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
'use client'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
export default function SearchBar() {
|
|
||||||
const handleClick = () => {
|
|
||||||
toast.warn('This feature is currently under active development!')
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className='width-[40vw]'>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
className='py-3 px-5 rounded-xl text-slate-800'
|
|
||||||
name='q'
|
|
||||||
placeholder='Search...'
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type='submit'
|
|
||||||
className='p-2.5 mx-4 border-2 rounded-xl hover:bg-blue-300'
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
7
src/app/utils/navigate.ts
Normal file
7
src/app/utils/navigate.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
'use server'
|
||||||
|
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export async function navigate(data: FormData) {
|
||||||
|
redirect(`/posts/${data.get('id')}`)
|
||||||
|
}
|
41
src/app/utils/searchDocs.ts
Normal file
41
src/app/utils/searchDocs.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import MiniSearch, { SearchResult } from 'minisearch'
|
||||||
|
import { documents, DocumentManifest } from '../db/data'
|
||||||
|
|
||||||
|
const docs = Object.entries(documents).map(([key, value]) => ({
|
||||||
|
id: key,
|
||||||
|
keywords: value.manifest.keywords.join(' '),
|
||||||
|
abstract: value.abstract,
|
||||||
|
topics: value.manifest.topics.join(' '),
|
||||||
|
authors: value.manifest.authors.join(' '),
|
||||||
|
title: value.manifest.title,
|
||||||
|
manifest: value.manifest,
|
||||||
|
type: value.manifest.type,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const miniSearch = new MiniSearch({
|
||||||
|
fields: ['abstract', 'keywords', 'topics', 'authors', 'title', 'type'],
|
||||||
|
storeFields: ['key', 'abstract', 'manifest'],
|
||||||
|
searchOptions: {
|
||||||
|
boost: {
|
||||||
|
title: 2,
|
||||||
|
keywords: 2,
|
||||||
|
topics: 1,
|
||||||
|
authors: 2,
|
||||||
|
},
|
||||||
|
fuzzy: 0.2,
|
||||||
|
prefix: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
miniSearch.addAll(docs)
|
||||||
|
|
||||||
|
export interface CustomSearchResult extends SearchResult {
|
||||||
|
manifest: DocumentManifest
|
||||||
|
abstract: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function searchDocs(
|
||||||
|
query: string,
|
||||||
|
limit = 10
|
||||||
|
): CustomSearchResult[] {
|
||||||
|
return miniSearch.search(query).slice(0, limit) as CustomSearchResult[]
|
||||||
|
}
|
Loading…
Reference in a new issue