add search feature and garbage mobile support

This commit is contained in:
Youwen Wu 2024-02-11 23:09:37 -08:00
parent 9e93907c80
commit c8fd4148ef
18 changed files with 600 additions and 230 deletions

42
package-lock.json generated
View file

@ -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",

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View 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>
}

View 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>
))}
</>
)
}

View 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>
)
}

View 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>
)
}

View 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;
}

View file

@ -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 export const documents: { [key: string]: Document } = {
file: FileType
citation?: string
}
}
export const documents: DocumentDB = {
'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 Rosebuds 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',
},
} }

View file

@ -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>

View file

@ -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 {

View file

@ -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>
) )
} }

View file

@ -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>

View file

@ -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
View 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>
)
}

View file

@ -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>
)
}

View file

@ -0,0 +1,7 @@
'use server'
import { redirect } from 'next/navigation'
export async function navigate(data: FormData) {
redirect(`/posts/${data.get('id')}`)
}

View 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[]
}