Merge pull request #3 from Team-1280/webworkers
Use webworkers to asynchronously fetch data while triggering react suspense
This commit is contained in:
commit
a63a5f3a63
24 changed files with 967 additions and 464 deletions
14
README.md
14
README.md
|
@ -27,3 +27,17 @@ The dummies guide to maintaining a Next.js project:
|
||||||
- Important: THIS IS NOT JAVASCRIPT! You CANNOT use global variables, window variables, etc, or even stateful variables that are meant to persist beyond a single refresh cycle (which can happen many times per second). Use the STATE for this (see [the module we are using for state management](https://github.com/pmndrs/zustand))
|
- Important: THIS IS NOT JAVASCRIPT! You CANNOT use global variables, window variables, etc, or even stateful variables that are meant to persist beyond a single refresh cycle (which can happen many times per second). Use the STATE for this (see [the module we are using for state management](https://github.com/pmndrs/zustand))
|
||||||
- Try to define modules for your components instead of putting everything in one file to avoid JANK
|
- Try to define modules for your components instead of putting everything in one file to avoid JANK
|
||||||
- You should put all static assets like images, icons, sound bytes, etc, in the `public` folder. These assets will be made available at the root directory `/`. For example, if `eecs-wordmark.png` is in `public`, then I can access it from an image element like so: `<img src="/eecs-wordmark.png" />`.
|
- You should put all static assets like images, icons, sound bytes, etc, in the `public` folder. These assets will be made available at the root directory `/`. For example, if `eecs-wordmark.png` is in `public`, then I can access it from an image element like so: `<img src="/eecs-wordmark.png" />`.
|
||||||
|
- VERY IMPORTANT for performance and UI jank purposes:
|
||||||
|
- As you may have noticed, we store all of our data in a super large TypeScript file at `db/data.ts`. This module contains exports for all of our 5 main "databases."
|
||||||
|
- In order to access these databases from your components, there are two **critical** conventions to follow:
|
||||||
|
- If your component is **server side**, then simply import the components like normal (eg `import { documents } from '@/app/db/data`) and use them in your code.
|
||||||
|
- If your component is **client side**, then you should import the data using the utilities available in `db/loaders.ts`. The `loaders` library contains
|
||||||
|
functions which return a Promise that resolve to the requested data. The function will load the very large objects in a separate thread via a Web Worker, which
|
||||||
|
avoids blocking the main UI thread and freezing everything. You can then use the `useSuspenseQuery` hook from `@tanstack/react-query` to load this data in the background while
|
||||||
|
triggering a React Suspense (if you don't set one up yourself, the default site wide loading bar will be used). This helps vastly reduce UI jank when trying to load the entire
|
||||||
|
mega data object directly into memory.
|
||||||
|
- Not sure whether you're in a client or server side component? If your component has the `'use client'` directive at the top of its file, then it's a client side component.
|
||||||
|
Otherwise, by default, it should be a server side component.
|
||||||
|
- Footnote: why don't I have to use the utilities in `db/loaders.ts` to asynchronously load the data in server side components?
|
||||||
|
- Next.js will automatically pre-render all server side components into static HTML, which means there will be no performance impact (and in fact performance loss at build time)
|
||||||
|
to loading the entire objects into memory.
|
||||||
|
|
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"name": "eexiv-2",
|
"name": "eexiv-2",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.20.2",
|
||||||
"minisearch": "^6.3.0",
|
"minisearch": "^6.3.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
@ -466,6 +467,30 @@
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.20.2.tgz",
|
||||||
|
"integrity": "sha512-sAILwNiyA1I52e6imOsmNDUA/PuOayOzqz5jcLiIB5wBXqVk+HIiriWouPcAkjS8RqARfHUehuoPwcZ7Uzh0GQ==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.20.2.tgz",
|
||||||
|
"integrity": "sha512-949myvMY77cPqwb71m3wRG2ypgwPijshO5kN9w0CDKWrFC0X8Wh1mwSqst88kIr58tWlWNsGy3U40AK23RgYQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.20.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/file-saver": {
|
"node_modules/@types/file-saver": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.20.2",
|
||||||
"minisearch": "^6.3.0",
|
"minisearch": "^6.3.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|
175
src/app/author/[author]/AuthorDisplay.tsx
Normal file
175
src/app/author/[author]/AuthorDisplay.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { affiliations, nationalities, authors } from '../../db/data'
|
||||||
|
import { Zilla_Slab } from 'next/font/google'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
||||||
|
|
||||||
|
export default function AuthorDisplay({
|
||||||
|
author,
|
||||||
|
}: Readonly<{ author: string }>) {
|
||||||
|
const data = authors[author]
|
||||||
|
if (!data) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, affiliation, image, nationality, formerAffiliations } = data
|
||||||
|
|
||||||
|
const MainPosition = () => {
|
||||||
|
const mainAffiliationShort = affiliation[0].split('@')[1]
|
||||||
|
const mainPosition = affiliation[0].split('@')[0]
|
||||||
|
const mainAffiliation = affiliations[mainAffiliationShort]
|
||||||
|
const { website } = data
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>{mainPosition} at </span>
|
||||||
|
<Link href={`/affiliation/${mainAffiliationShort}`}>
|
||||||
|
{mainAffiliation.name}
|
||||||
|
</Link>
|
||||||
|
{website ? (
|
||||||
|
<div className='mt-2'>
|
||||||
|
Visit {name.nickname ? name.nickname : name.first} at:{' '}
|
||||||
|
<a href={website} target='_blank'>
|
||||||
|
{website}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className='my-4 max-h-12 flex flex-wrap'>
|
||||||
|
{affiliation.map((a: string) => (
|
||||||
|
<Link key={a} href={`/affiliation/${a.split('@')[1]}`}>
|
||||||
|
<img
|
||||||
|
src={affiliations[a.split('@')[1]].image}
|
||||||
|
alt={affiliations[a.split('@')[1]].name}
|
||||||
|
className='h-12 mr-2'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OtherPositions = () => {
|
||||||
|
if (affiliation.length === 1) return
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className='text-3xl md:mt-6 mt-4 mb-2 font-serif'>
|
||||||
|
Other Positions:
|
||||||
|
</h1>
|
||||||
|
{affiliation.slice(1).map((a: string, i: number) => {
|
||||||
|
const position = a.split('@')[0]
|
||||||
|
const affiliation = affiliations[a.split('@')[1]].name
|
||||||
|
return (
|
||||||
|
<Fragment key={`${position}@${affiliation}`}>
|
||||||
|
<span className='text-slate-500 text-lg'>
|
||||||
|
{position} at{' '}
|
||||||
|
<Link href={`/affiliation/${a.split('@')[1]}`}>
|
||||||
|
{affiliation}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormerPositions = () => {
|
||||||
|
if (!formerAffiliations) return null
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className='text-3xl md:mt-6 mt-4 mb-2 font-serif'>
|
||||||
|
Former Positions:
|
||||||
|
</h1>
|
||||||
|
{formerAffiliations?.map((a: string, i: number) => {
|
||||||
|
const position = a.split('@')[0]
|
||||||
|
const affiliation = affiliations[a.split('@')[1]].name
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={`${position}@${affiliation}`}>
|
||||||
|
<span className='text-slate-500 text-lg'>
|
||||||
|
{position} at{' '}
|
||||||
|
<Link href={`/affiliation/${a.split('@')[1]}`}>
|
||||||
|
{affiliation}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NationalityDisplay = ({
|
||||||
|
nationality,
|
||||||
|
}: Readonly<{ nationality: string }>) => {
|
||||||
|
const nationalityData = nationalities[nationality]
|
||||||
|
const { demonym, flag } = nationalityData
|
||||||
|
return (
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<img
|
||||||
|
src={flag}
|
||||||
|
className='w-10 border-2 border-slate-200'
|
||||||
|
alt={`${demonym} flag`}
|
||||||
|
/>
|
||||||
|
<span className='mx-3 font-semibold'>{demonym}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bio = () => {
|
||||||
|
const { bio } = data
|
||||||
|
if (!bio) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className='text-3xl md:mt-6 mt-4 mb-2 font-serif'>Bio:</h1>
|
||||||
|
<p>{bio}</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 items-center max-w-3xl mx-auto'>
|
||||||
|
<div className='aspect-square w-[60vw] md:w-[30vw] lg:w-[20vw] 2xl:w-[15vw] overflow-hidden mx-auto mb-4'>
|
||||||
|
<img
|
||||||
|
alt='profile'
|
||||||
|
className='rounded-full mx-auto object-cover w-full h-full border-slate-800 border-4'
|
||||||
|
src={image}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className={`${zillaSlab.className} font-bold text-5xl text-center md:text-left`}
|
||||||
|
>
|
||||||
|
{name.first}
|
||||||
|
{name.nickname ? ` "${name.nickname}"` : null} {name.last}
|
||||||
|
</span>
|
||||||
|
<div className='text-slate-600 text-md sm:text-lg mt-4'>
|
||||||
|
<MainPosition />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='max-w-3xl mx-auto grid grid-cols-1 justify-center'>
|
||||||
|
<hr className='mx-auto w-full h-1 border-0 bg-slate-200 my-2 rounded-md' />
|
||||||
|
<OtherPositions />
|
||||||
|
<FormerPositions />
|
||||||
|
<h1 className='text-3xl md:my-6 my-4 font-serif'>
|
||||||
|
Ethnicity and Nationality:
|
||||||
|
</h1>
|
||||||
|
<div className='flex gap-2 flex-wrap'>
|
||||||
|
{nationality.map((n: string) => (
|
||||||
|
<Fragment key={n}>
|
||||||
|
<NationalityDisplay nationality={n} />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Bio />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,10 +1,5 @@
|
||||||
import { authors, affiliations, nationalities } from '../../db/data'
|
import AuthorDisplay from './AuthorDisplay'
|
||||||
import { Zilla_Slab } from 'next/font/google'
|
import { authors } from '@/app/db/data'
|
||||||
import Link from 'next/link'
|
|
||||||
import { notFound } from 'next/navigation'
|
|
||||||
import { Fragment, Suspense } from 'react'
|
|
||||||
|
|
||||||
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] })
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
const authorsList = Object.keys(authors)
|
const authorsList = Object.keys(authors)
|
||||||
|
@ -16,172 +11,5 @@ export default function Page({
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
params: { author: string }
|
params: { author: string }
|
||||||
}>) {
|
}>) {
|
||||||
const authorData = authors[params.author]
|
return <AuthorDisplay author={params.author} />
|
||||||
// console.log(authorData)
|
|
||||||
if (!authorData) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, affiliation, image, nationality, formerAffiliations } =
|
|
||||||
authorData
|
|
||||||
|
|
||||||
const MainPosition = () => {
|
|
||||||
const mainAffiliationShort = affiliation[0].split('@')[1]
|
|
||||||
const mainPosition = affiliation[0].split('@')[0]
|
|
||||||
const mainAffiliation = affiliations[mainAffiliationShort]
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span>{mainPosition} at </span>
|
|
||||||
<Link href={`/affiliation/${mainAffiliationShort}`}>
|
|
||||||
{mainAffiliation.name}
|
|
||||||
</Link>
|
|
||||||
{authorData.website ? (
|
|
||||||
<div className='mt-2'>
|
|
||||||
Visit{' '}
|
|
||||||
{authorData.name.nickname
|
|
||||||
? authorData.name.nickname
|
|
||||||
: authorData.name.first}{' '}
|
|
||||||
at:{' '}
|
|
||||||
<a href={authorData.website} target='_blank'>
|
|
||||||
{authorData.website}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className='my-4 max-h-12 flex flex-wrap'>
|
|
||||||
{affiliation.map((a: string) => (
|
|
||||||
<Link key={a} href={`/affiliation/${a.split('@')[1]}`}>
|
|
||||||
<img
|
|
||||||
src={affiliations[a.split('@')[1]].image}
|
|
||||||
alt={affiliations[a.split('@')[1]].name}
|
|
||||||
className='h-12 mr-2'
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const OtherPositions = () => {
|
|
||||||
if (affiliation.length === 1) return
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className='text-3xl md:mt-6 mt-4 mb-2 font-serif'>
|
|
||||||
Other Positions:
|
|
||||||
</h1>
|
|
||||||
{affiliation.slice(1).map((a: string, i: number) => {
|
|
||||||
const position = a.split('@')[0]
|
|
||||||
const affiliation = affiliations[a.split('@')[1]].name
|
|
||||||
return (
|
|
||||||
<Fragment key={`${position}@${affiliation}`}>
|
|
||||||
<span className='text-slate-500 text-lg'>
|
|
||||||
{position} at{' '}
|
|
||||||
<Link href={`/affiliation/${a.split('@')[1]}`}>
|
|
||||||
{affiliation}
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormerPositions = () => {
|
|
||||||
if (!formerAffiliations) return null
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className='text-3xl md:mt-6 mt-4 mb-2 font-serif'>
|
|
||||||
Former Positions:
|
|
||||||
</h1>
|
|
||||||
{formerAffiliations?.map((a: string, i: number) => {
|
|
||||||
const position = a.split('@')[0]
|
|
||||||
const affiliation = affiliations[a.split('@')[1]].name
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={`${position}@${affiliation}`}>
|
|
||||||
<span className='text-slate-500 text-lg'>
|
|
||||||
{position} at{' '}
|
|
||||||
<Link href={`/affiliation/${a.split('@')[1]}`}>
|
|
||||||
{affiliation}
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const NationalityDisplay = ({
|
|
||||||
nationality,
|
|
||||||
}: Readonly<{ nationality: string }>) => {
|
|
||||||
const nationalityData = nationalities[nationality]
|
|
||||||
const { demonym, flag } = nationalityData
|
|
||||||
return (
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<img
|
|
||||||
src={flag}
|
|
||||||
className='w-10 border-2 border-slate-200'
|
|
||||||
alt={`${demonym} flag`}
|
|
||||||
/>
|
|
||||||
<span className='mx-3 font-semibold'>{demonym}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Bio = () => {
|
|
||||||
const { bio } = authorData
|
|
||||||
if (!bio) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className='text-3xl md:mt-6 mt-4 mb-2 font-serif'>Bio:</h1>
|
|
||||||
<p>{authorData.bio}</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='grid grid-cols-1 md:grid-cols-2 items-center max-w-3xl mx-auto'>
|
|
||||||
<div className='aspect-square w-[60vw] md:w-[30vw] lg:w-[20vw] 2xl:w-[15vw] overflow-hidden mx-auto mb-4'>
|
|
||||||
<img
|
|
||||||
alt='profile'
|
|
||||||
className='rounded-full mx-auto object-cover w-full h-full border-slate-800 border-4'
|
|
||||||
src={image}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<span
|
|
||||||
className={`${zillaSlab.className} font-bold text-5xl text-center md:text-left`}
|
|
||||||
>
|
|
||||||
{name.first}
|
|
||||||
{name.nickname ? ` "${name.nickname}"` : null} {name.last}
|
|
||||||
</span>
|
|
||||||
<div className='text-slate-600 text-md sm:text-lg mt-4'>
|
|
||||||
<MainPosition />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='max-w-3xl mx-auto grid grid-cols-1 justify-center'>
|
|
||||||
<hr className='mx-auto w-full h-1 border-0 bg-slate-200 my-2 rounded-md' />
|
|
||||||
<OtherPositions />
|
|
||||||
<FormerPositions />
|
|
||||||
<h1 className='text-3xl md:my-6 my-4 font-serif'>
|
|
||||||
Ethnicity and Nationality:
|
|
||||||
</h1>
|
|
||||||
<div className='flex gap-2 flex-wrap'>
|
|
||||||
{nationality.map((n: string) => (
|
|
||||||
<Fragment key={n}>
|
|
||||||
<NationalityDisplay nationality={n} />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Bio />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {
|
import { reviewer } from '@/app/db/data'
|
||||||
topics as topicList,
|
import { loadAllTopics, loadAllAuthors } from '@/app/db/loaders'
|
||||||
authors as authorList,
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
reviewer,
|
|
||||||
} from '@/app/db/data'
|
|
||||||
|
|
||||||
export const Code = ({
|
export const Code = ({
|
||||||
code,
|
code,
|
||||||
|
@ -54,6 +52,18 @@ export const Topics = ({
|
||||||
topics,
|
topics,
|
||||||
showTitle = true,
|
showTitle = true,
|
||||||
}: Readonly<{ topics: string[]; showTitle?: boolean }>) => {
|
}: Readonly<{ topics: string[]; showTitle?: boolean }>) => {
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
const { data, error } = useSuspenseQuery({
|
||||||
|
queryKey: ['topics_all'],
|
||||||
|
queryFn: () => {
|
||||||
|
const data = loadAllTopics()
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (error) throw error
|
||||||
|
const topicList = data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showTitle ? <span className='font-bold'>Topics: </span> : null}
|
{showTitle ? <span className='font-bold'>Topics: </span> : null}
|
||||||
|
@ -72,6 +82,19 @@ export const Topics = ({
|
||||||
export const Authors = ({
|
export const Authors = ({
|
||||||
authors,
|
authors,
|
||||||
}: Readonly<{ authors: string[]; noLink?: boolean }>) => {
|
}: Readonly<{ authors: string[]; noLink?: boolean }>) => {
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
const { data, error } = useSuspenseQuery({
|
||||||
|
queryKey: ['authors_all'],
|
||||||
|
queryFn: () => {
|
||||||
|
const data = loadAllAuthors()
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
const authorList = data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{authors.map((a: string, i) => (
|
{authors.map((a: string, i) => (
|
||||||
|
@ -95,20 +118,16 @@ export const Reviewers = ({
|
||||||
const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => {
|
const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => {
|
||||||
if (r.profile) {
|
if (r.profile) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Link href={`/author/${r.profile}`} target='_blank'>
|
<Link href={`/author/${r.profile}`} target='_blank'>
|
||||||
{r.first} {r.last}
|
{r.first} {r.last}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (r.url) {
|
if (r.url) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<a href={r.url} target='_blank'>
|
<a href={r.url} target='_blank'>
|
||||||
{r.first} {r.last}
|
{r.first} {r.last}
|
||||||
</a>
|
</a>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -13,13 +13,15 @@ export default function News() {
|
||||||
<br className='my-4' />
|
<br className='my-4' />
|
||||||
<ul className='text-slate-50 px-6 list-disc'>
|
<ul className='text-slate-50 px-6 list-disc'>
|
||||||
<li key={1}>
|
<li key={1}>
|
||||||
eeXiv 1.0 has been released! All basic features like search and
|
eeXiv 2.0 has been released! The site should feel significantly more
|
||||||
document viewing are available.
|
responsive. Data cacheing has also been implemented so search results
|
||||||
|
and documents will load instantly the second time.
|
||||||
</li>
|
</li>
|
||||||
<li key={2}>eeXiv is currently under active development!</li>
|
<li key={2}>Mobile support is currently in beta.</li>
|
||||||
<li key={3}>
|
<li key={3}>
|
||||||
There may be major updates, breaking changes, or weird bugs. Report
|
eeXiv is currently under active development! There may be major
|
||||||
bugs, suggest new features, or give us feedback at{' '}
|
updates, breaking changes, or weird bugs. Report bugs, suggest new
|
||||||
|
features, or give us feedback at{' '}
|
||||||
<a href='https://github.com/team-1280/eexiv-2/issues' target='_blank'>
|
<a href='https://github.com/team-1280/eexiv-2/issues' target='_blank'>
|
||||||
our issue tracker.
|
our issue tracker.
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -17,7 +17,6 @@ export default function SearchBar() {
|
||||||
const setSearchBarStore = useSearchBarStore((state) => state.setSearchInput)
|
const setSearchBarStore = useSearchBarStore((state) => state.setSearchInput)
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault()
|
|
||||||
navigate(`/search?q=${searchBarStore.split(' ').join('+')}`)
|
navigate(`/search?q=${searchBarStore.split(' ').join('+')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +26,6 @@ export default function SearchBar() {
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault()
|
|
||||||
navigate(`/search?q=${searchBarStore.split(' ').join('+')}`)
|
navigate(`/search?q=${searchBarStore.split(' ').join('+')}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -282,14 +282,12 @@ On Day 1 we will discuss the ins and outs of the robot. The Electrical Sub team
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Topics {
|
export interface Topic {
|
||||||
[key: string]: {
|
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
wiki: string
|
wiki: string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const topics: Topics = {
|
export const topics: { [key: string]: Topic } = {
|
||||||
frc: {
|
frc: {
|
||||||
name: 'FIRST Robotics Competition',
|
name: 'FIRST Robotics Competition',
|
||||||
description:
|
description:
|
||||||
|
@ -346,8 +344,7 @@ authorName (as a slug): {
|
||||||
website: website url
|
website: website url
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export interface Authors {
|
export interface Author {
|
||||||
[key: string]: {
|
|
||||||
name: {
|
name: {
|
||||||
first: string
|
first: string
|
||||||
last: string
|
last: string
|
||||||
|
@ -359,9 +356,8 @@ export interface Authors {
|
||||||
formerAffiliations?: string[]
|
formerAffiliations?: string[]
|
||||||
bio?: string
|
bio?: string
|
||||||
website?: string
|
website?: string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const authors: Authors = {
|
export const authors: { [key: string]: Author } = {
|
||||||
shasan: {
|
shasan: {
|
||||||
name: {
|
name: {
|
||||||
first: 'Saim',
|
first: 'Saim',
|
||||||
|
@ -501,16 +497,14 @@ export const authors: Authors = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Affiliations {
|
export interface Affiliation {
|
||||||
[key: string]: {
|
|
||||||
name: string
|
name: string
|
||||||
short: string
|
short: string
|
||||||
image: string
|
image: string
|
||||||
description: string
|
description: string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const affiliations: Affiliations = {
|
export const affiliations: { [key: string]: Affiliation } = {
|
||||||
'1280-mech': {
|
'1280-mech': {
|
||||||
name: "Team 1280, the Ragin' C Biscuits, Mechanical Subteam",
|
name: "Team 1280, the Ragin' C Biscuits, Mechanical Subteam",
|
||||||
short: '1280 Mech',
|
short: '1280 Mech',
|
||||||
|
@ -661,14 +655,12 @@ Raid Zero's influence extends beyond the technical achievements in robotics comp
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Nationalities {
|
export interface Nationality {
|
||||||
[key: string]: {
|
|
||||||
name: string
|
name: string
|
||||||
demonym: string
|
demonym: string
|
||||||
flag: string
|
flag: string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const nationalities: Nationalities = {
|
export const nationalities: { [key: string]: Nationality } = {
|
||||||
pak: {
|
pak: {
|
||||||
name: 'Pakistan',
|
name: 'Pakistan',
|
||||||
demonym: 'Pakistani',
|
demonym: 'Pakistani',
|
||||||
|
|
330
src/app/db/loaders.ts
Normal file
330
src/app/db/loaders.ts
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
import { Document, Author, Affiliation, Topic, Nationality } from './data'
|
||||||
|
|
||||||
|
export const loadDocument = (id: string): Promise<Document> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/documentLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Document }>) => {
|
||||||
|
const data = e.data
|
||||||
|
const doc: Document | undefined = data[id]
|
||||||
|
if (!doc) {
|
||||||
|
return reject(new Error('404'))
|
||||||
|
} else {
|
||||||
|
resolve(doc)
|
||||||
|
}
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Web Workers are not supported in this environment. Please avoid using a prehistoric browser.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export const loadAllDocuments = (): Promise<{ [key: string]: Document }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/documentLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Document }>) => {
|
||||||
|
resolve(e.data)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Web Workers are not supported in this environment. Please avoid using a prehistoric browser.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAllAuthors = (): Promise<{ [key: string]: Author }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/authorLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Author }>) => {
|
||||||
|
resolve(e.data)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Web Workers are not supported in this environment. Please avoid using a prehistoric browser.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAuthor = (id: string): Promise<Author> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/authorLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Author }>) => {
|
||||||
|
const data = e.data
|
||||||
|
const author: Author | undefined = data[id]
|
||||||
|
if (!author) {
|
||||||
|
return reject(new Error('404'))
|
||||||
|
} else {
|
||||||
|
resolve(author)
|
||||||
|
}
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Web Workers are not supported in this environment. Please avoid using a prehistoric browser.
|
||||||
|
If nothing else seems wrong, this error message is probably showing up due to ghosts in your browser.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAffiliation = (id: string): Promise<Affiliation> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/affiliationLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Affiliation }>) => {
|
||||||
|
const data = e.data
|
||||||
|
const affiliation: Affiliation | undefined = data[id]
|
||||||
|
if (!affiliation) {
|
||||||
|
return reject(new Error('404'))
|
||||||
|
} else {
|
||||||
|
resolve(affiliation)
|
||||||
|
}
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Web Workers are not supported in this environment. Please avoid using a prehistoric browser.
|
||||||
|
If nothing else seems wrong, this error message is probably showing up due to ghosts in your browser.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAllAffiliations = (): Promise<{
|
||||||
|
[key: string]: Affiliation
|
||||||
|
}> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/affiliationLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Affiliation }>) => {
|
||||||
|
resolve(e.data)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Web Workers are not supported in this environment. Please avoid using a prehistoric browser.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadTopic = (id: string): Promise<Topic> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/topicLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Topic }>) => {
|
||||||
|
const data = e.data
|
||||||
|
const topic: Topic | undefined = data[id]
|
||||||
|
if (!topic) {
|
||||||
|
return reject(new Error('404'))
|
||||||
|
} else {
|
||||||
|
resolve(topic)
|
||||||
|
}
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Web Workers are not supported in this environment. Please avoid using a prehistoric browser.
|
||||||
|
If nothing else seems wrong, this error message is probably showing up due to ghosts in your browser.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAllTopics = (): Promise<{
|
||||||
|
[key: string]: Topic
|
||||||
|
}> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/topicLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Topic }>) => {
|
||||||
|
resolve(e.data)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Web Workers are not supported in this environment. Please avoid using a prehistoric browser.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadNationality = (id: string): Promise<Nationality> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/nationalityLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Nationality }>) => {
|
||||||
|
const data = e.data
|
||||||
|
const nationality: Nationality | undefined = data[id]
|
||||||
|
if (!nationality) {
|
||||||
|
return reject(new Error('404'))
|
||||||
|
} else {
|
||||||
|
resolve(nationality)
|
||||||
|
}
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Web Workers are not supported in this environment. Please avoid using a prehistoric browser.
|
||||||
|
If nothing else seems wrong, this error message is probably showing up due to ghosts in your browser.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAllNationalities = (): Promise<{
|
||||||
|
[key: string]: Nationality
|
||||||
|
}> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./workers/topicLoader.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<{ [key: string]: Nationality }>) => {
|
||||||
|
resolve(e.data)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage('LOAD')
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Web Workers are not supported in this environment. Please avoid using a prehistoric browser.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
2
src/app/db/workers/README
Normal file
2
src/app/db/workers/README
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
These worker files should not be touched unless you know what you are doing! They're simply utilities to be imported
|
||||||
|
by the loaders library to launch asynchronous web worker threads.
|
7
src/app/db/workers/affiliationLoader.worker.ts
Normal file
7
src/app/db/workers/affiliationLoader.worker.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { affiliations } from '../data'
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
if (e.data === 'LOAD') {
|
||||||
|
self.postMessage(affiliations)
|
||||||
|
}
|
||||||
|
}
|
7
src/app/db/workers/authorLoader.worker.ts
Normal file
7
src/app/db/workers/authorLoader.worker.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { authors } from '../data'
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
if (e.data === 'LOAD') {
|
||||||
|
self.postMessage(authors)
|
||||||
|
}
|
||||||
|
}
|
7
src/app/db/workers/documentLoader.worker.ts
Normal file
7
src/app/db/workers/documentLoader.worker.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { documents } from '../data'
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
if (e.data === 'LOAD') {
|
||||||
|
self.postMessage(documents)
|
||||||
|
}
|
||||||
|
}
|
7
src/app/db/workers/nationalityLoader.worker.ts
Normal file
7
src/app/db/workers/nationalityLoader.worker.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { nationalities } from '../data'
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
if (e.data === 'LOAD') {
|
||||||
|
self.postMessage(nationalities)
|
||||||
|
}
|
||||||
|
}
|
7
src/app/db/workers/topicLoader.worker.ts
Normal file
7
src/app/db/workers/topicLoader.worker.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { topics } from '../data'
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
if (e.data === 'LOAD') {
|
||||||
|
self.postMessage(topics)
|
||||||
|
}
|
||||||
|
}
|
110
src/app/document/view/[slug]/DocumentViewer.tsx
Normal file
110
src/app/document/view/[slug]/DocumentViewer.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { DocumentType } from '@/app/db/data'
|
||||||
|
import { Zilla_Slab } from 'next/font/google'
|
||||||
|
import { epoch2datestring } from '@/app/utils/epoch2datestring'
|
||||||
|
import {
|
||||||
|
Code,
|
||||||
|
References,
|
||||||
|
Topics,
|
||||||
|
Authors,
|
||||||
|
Reviewers,
|
||||||
|
} from '@/app/components/DataDisplay'
|
||||||
|
import { ItemBadge, Status } from '@/app/components/Badges'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { loadDocument } from '@/app/db/loaders'
|
||||||
|
|
||||||
|
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] })
|
||||||
|
|
||||||
|
export default function DocumentViewer({ slug }: Readonly<{ slug: string }>) {
|
||||||
|
const { data, error } = useSuspenseQuery({
|
||||||
|
queryKey: [slug],
|
||||||
|
queryFn: () => {
|
||||||
|
const data = loadDocument(slug)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { manifest, abstract, file, citation } = data
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
authors,
|
||||||
|
topics,
|
||||||
|
dates,
|
||||||
|
references,
|
||||||
|
code,
|
||||||
|
type,
|
||||||
|
latest,
|
||||||
|
reviewers,
|
||||||
|
status,
|
||||||
|
} = manifest
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
||||||
|
<h1
|
||||||
|
className={`
|
||||||
|
text-slate-800 font-bold text-5xl mb-4
|
||||||
|
${zillaSlab.className}
|
||||||
|
text-wrap
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className={`text-slate-800 mt-2`}>
|
||||||
|
<Authors authors={authors} />
|
||||||
|
</p>
|
||||||
|
<p className='mt-4 mb-2'>
|
||||||
|
Latest revision published{' '}
|
||||||
|
<span className='font-bold'>
|
||||||
|
{epoch2datestring(dates[dates.length - 1])}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<ItemBadge itemName={type as DocumentType} />
|
||||||
|
<Status statusName={status} />
|
||||||
|
<span className='inline-block border-gray-200 border-2 rounded px-2 py-1.5 mr-2'>
|
||||||
|
Revision {latest}
|
||||||
|
</span>
|
||||||
|
<hr className='my-4' />
|
||||||
|
<h4 className='text-2xl mt-5 font-serif font-semibold'>Abstract</h4>
|
||||||
|
<p className='my-4 text-xl text-slate-600 font-serif text-balance'>
|
||||||
|
{abstract}
|
||||||
|
</p>
|
||||||
|
<p className='my-2'>
|
||||||
|
<Topics topics={topics} />
|
||||||
|
</p>
|
||||||
|
<p className='my-2'>
|
||||||
|
<Code code={code} />
|
||||||
|
</p>
|
||||||
|
<p className='my-2'>
|
||||||
|
<References references={references} />
|
||||||
|
</p>
|
||||||
|
<p className='my-2'>
|
||||||
|
<Reviewers reviewers={reviewers} />
|
||||||
|
</p>
|
||||||
|
<p className='my-2'>
|
||||||
|
<span className='font-bold'>Cite as: </span>
|
||||||
|
{citation ? <>{citation}</> : <>eeXiv:{slug}</>}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href={`/download/${slug}/file${latest}${file === 'other' ? '' : `.${file}`}`}
|
||||||
|
download={`${slug}-rev-${latest}${file === 'other' ? '' : `.${file}`}`}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
<button className='button-default'>
|
||||||
|
Download{' '}
|
||||||
|
{(() => {
|
||||||
|
switch (file) {
|
||||||
|
case 'other':
|
||||||
|
return <></>
|
||||||
|
case 'tar.gz':
|
||||||
|
return 'Tarball'
|
||||||
|
default:
|
||||||
|
return file.toUpperCase()
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,117 +1,13 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { Zilla_Slab } from 'next/font/google'
|
import DocumentViewer from './DocumentViewer'
|
||||||
import { DocumentType, documents, reviewer } from '@/app/db/data'
|
import ErrorBoundary from '@/app/utils/ErrorBoundary'
|
||||||
import Link from 'next/link'
|
|
||||||
import { notFound } from 'next/navigation'
|
|
||||||
import { Fragment, useEffect } from 'react'
|
|
||||||
import { epoch2datestring } from '@/app/utils/epoch2datestring'
|
|
||||||
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'] })
|
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({
|
||||||
params,
|
params,
|
||||||
}: Readonly<{ params: { slug: string } }>) {
|
}: Readonly<{ params: { slug: string } }>) {
|
||||||
const doc = documents[params.slug]
|
|
||||||
if (!doc) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
const { abstract, file, citation } = doc
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
authors,
|
|
||||||
topics,
|
|
||||||
dates,
|
|
||||||
references,
|
|
||||||
code,
|
|
||||||
type,
|
|
||||||
latest,
|
|
||||||
reviewers,
|
|
||||||
status,
|
|
||||||
} = doc.manifest
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === 'reviewed' && !reviewers) {
|
|
||||||
toast.warn(
|
|
||||||
`This document is marked as peer reviewed, but the author
|
|
||||||
forgot to add a list of reviewers.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-4xl lg:max-w-6xl mx-auto'>
|
<ErrorBoundary>
|
||||||
<h1
|
<DocumentViewer slug={params.slug} />
|
||||||
className={`
|
</ErrorBoundary>
|
||||||
text-slate-800 font-bold text-5xl mb-4
|
|
||||||
${zillaSlab.className}
|
|
||||||
text-wrap
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className={`text-slate-800 mt-2`}>
|
|
||||||
<Authors authors={authors} />
|
|
||||||
</p>
|
|
||||||
<p className='mt-4 mb-2'>
|
|
||||||
Latest revision published{' '}
|
|
||||||
<span className='font-bold'>
|
|
||||||
{epoch2datestring(dates[dates.length - 1])}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<ItemBadge itemName={type as DocumentType} />
|
|
||||||
<Status statusName={status} />
|
|
||||||
<span className='inline-block border-gray-200 border-2 rounded px-2 py-1.5 mr-2'>
|
|
||||||
Revision {latest}
|
|
||||||
</span>
|
|
||||||
<hr className='my-4' />
|
|
||||||
<h4 className='text-2xl mt-5 font-serif font-semibold'>Abstract</h4>
|
|
||||||
<p className='my-4 text-xl text-slate-600 font-serif text-balance'>
|
|
||||||
{abstract}
|
|
||||||
</p>
|
|
||||||
<p className='my-2'>
|
|
||||||
<Topics topics={topics} />
|
|
||||||
</p>
|
|
||||||
<p className='my-2'>
|
|
||||||
<Code code={code} />
|
|
||||||
</p>
|
|
||||||
<p className='my-2'>
|
|
||||||
<References references={references} />
|
|
||||||
</p>
|
|
||||||
<p className='my-2'>
|
|
||||||
<Reviewers reviewers={reviewers} />
|
|
||||||
</p>
|
|
||||||
<p className='my-2'>
|
|
||||||
<span className='font-bold'>Cite as: </span>
|
|
||||||
{citation ? <>{citation}</> : <>eeXiv:{params.slug}</>}
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href={`/download/${params.slug}/file${latest}${file === 'other' ? '' : `.${file}`}`}
|
|
||||||
download={`${params.slug}-rev-${latest}${file === 'other' ? '' : `.${file}`}`}
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
<button className='button-default'>
|
|
||||||
Download{' '}
|
|
||||||
{(() => {
|
|
||||||
switch (file) {
|
|
||||||
case 'other':
|
|
||||||
return <></>
|
|
||||||
case 'tar.gz':
|
|
||||||
return 'Tarball'
|
|
||||||
default:
|
|
||||||
return file.toUpperCase()
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Container from './container/Container'
|
||||||
import MobileMenu from './components/MobileMenu'
|
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'
|
||||||
|
import Providers from './providers'
|
||||||
|
|
||||||
/* The default font is Inter. If you want to use Zilla Slab (or any other Google Font,
|
/* The default font is Inter. If you want to use Zilla Slab (or any other Google Font,
|
||||||
which are pre-provided by Next.js in the 'next/font/google' module), you need to
|
which are pre-provided by Next.js in the 'next/font/google' module), you need to
|
||||||
|
@ -31,6 +32,7 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
|
<Providers>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className='max-w-[1200px] flex flex-nowrap mx-auto justify-between items-center'>
|
<div className='max-w-[1200px] flex flex-nowrap mx-auto justify-between items-center'>
|
||||||
|
@ -105,6 +107,7 @@ export default function RootLayout({
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
20
src/app/providers.jsx
Normal file
20
src/app/providers.jsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
export default function Providers({ children }) {
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,27 +1,15 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { Zilla_Slab } from 'next/font/google'
|
import { Zilla_Slab } from 'next/font/google'
|
||||||
import Link from 'next/link'
|
|
||||||
import { Topics, Authors } from '@/app/components/DataDisplay'
|
import { Topics, Authors } from '@/app/components/DataDisplay'
|
||||||
import { Status, ItemBadge } from '@/app/components/Badges'
|
import { Status, ItemBadge } from '@/app/components/Badges'
|
||||||
import { epoch2datestring } from '../utils/epoch2datestring'
|
import { epoch2datestring } from '../utils/epoch2datestring'
|
||||||
import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs'
|
import searchDocs, { CustomSearchResult } from '@/app/utils/searchDocs'
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { create } from 'zustand'
|
|
||||||
import { navigate } from '@/app/actions'
|
import { navigate } from '@/app/actions'
|
||||||
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] })
|
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 SearchResult = ({ result }: { result: CustomSearchResult }) => {
|
||||||
const { manifest, abstract, id } = result
|
const { manifest, abstract, id } = result
|
||||||
const { title, authors, topics, dates, status, type } = manifest
|
const { title, authors, topics, dates, status, type } = manifest
|
||||||
|
@ -66,11 +54,15 @@ const SearchResult = ({ result }: { result: CustomSearchResult }) => {
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const search = searchParams.get('q') as string
|
const search = searchParams.get('q') as string
|
||||||
const searchStore = useSearchStore((state) => state.results)
|
|
||||||
const setSearchStore = useSearchStore((state) => state.setResults)
|
const { data, error } = useSuspenseQuery({
|
||||||
useEffect(() => {
|
queryKey: [search],
|
||||||
setSearchStore(searchDocs(search))
|
queryFn: () => {
|
||||||
}, [search])
|
const data = searchDocs(search)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-4xl mx-auto'>
|
<div className='max-w-4xl mx-auto'>
|
||||||
|
@ -80,12 +72,10 @@ export default function Page() {
|
||||||
{search}
|
{search}
|
||||||
{`"`}
|
{`"`}
|
||||||
</h1>
|
</h1>
|
||||||
{searchStore.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<p className='text-lg px-2'>No results found.</p>
|
<p className='text-lg px-2'>No results found.</p>
|
||||||
) : (
|
) : (
|
||||||
searchStore.map((result) => (
|
data.map((result) => <SearchResult key={result.id} result={result} />)
|
||||||
<SearchResult key={result.id} result={result} />
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
29
src/app/utils/ErrorBoundary.jsx
Normal file
29
src/app/utils/ErrorBoundary.jsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = { hasError: false, error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
// Update state so the next render will show the fallback UI.
|
||||||
|
return { hasError: true, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
// You can also log the error to an error reporting service
|
||||||
|
console.error('Error caught by Error Boundary:', error, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// You can render any custom fallback UI
|
||||||
|
if (this.state.error.message === '404') notFound()
|
||||||
|
return <h1>Something went wrong.</h1>
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
34
src/app/utils/search.worker.ts
Normal file
34
src/app/utils/search.worker.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { documents } from '@/app/db/data'
|
||||||
|
import MiniSearch from 'minisearch'
|
||||||
|
import { CustomSearchResult } from './searchDocs'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
onmessage = (e: MessageEvent<string>) => {
|
||||||
|
postMessage(miniSearch.search(e.data) as CustomSearchResult[])
|
||||||
|
}
|
|
@ -1,32 +1,5 @@
|
||||||
import MiniSearch, { SearchResult } from 'minisearch'
|
import { SearchResult } from 'minisearch'
|
||||||
import { documents, DocumentManifest } from '../db/data'
|
import { 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 {
|
export interface CustomSearchResult extends SearchResult {
|
||||||
manifest: DocumentManifest
|
manifest: DocumentManifest
|
||||||
|
@ -36,6 +9,33 @@ export interface CustomSearchResult extends SearchResult {
|
||||||
export default function searchDocs(
|
export default function searchDocs(
|
||||||
query: string,
|
query: string,
|
||||||
limit = 10
|
limit = 10
|
||||||
): CustomSearchResult[] {
|
): Promise<CustomSearchResult[]> {
|
||||||
return miniSearch.search(query).slice(0, limit) as CustomSearchResult[]
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL('./search.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
worker.onmessage = (e: MessageEvent<CustomSearchResult[]>) => {
|
||||||
|
const data = e.data
|
||||||
|
resolve(data.slice(0, limit))
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
worker.terminate()
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage(query)
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Web Workers are not supported in this environment. Please avoid using a prehistoric browser.
|
||||||
|
If nothing else seems wrong, this error message is probably showing up due to ghosts in your browser.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue