From 574984983b800d69751decd43b1cf20630c92f79 Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 16:52:45 -0800 Subject: [PATCH 1/7] add web workers --- src/app/db/loaders.ts | 33 ++++++ src/app/db/workers/documentLoader.worker.ts | 7 ++ .../document/view/[slug]/DocumentViewer.tsx | 101 ++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/app/db/loaders.ts create mode 100644 src/app/db/workers/documentLoader.worker.ts create mode 100644 src/app/document/view/[slug]/DocumentViewer.tsx diff --git a/src/app/db/loaders.ts b/src/app/db/loaders.ts new file mode 100644 index 0000000..4f9f58a --- /dev/null +++ b/src/app/db/loaders.ts @@ -0,0 +1,33 @@ +import { Document } from './data' + +// export const loadDocument(id: string): Promise | undefined => { + +// } +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.' + ) + ) + } + }) +} diff --git a/src/app/db/workers/documentLoader.worker.ts b/src/app/db/workers/documentLoader.worker.ts new file mode 100644 index 0000000..e19ff6f --- /dev/null +++ b/src/app/db/workers/documentLoader.worker.ts @@ -0,0 +1,7 @@ +import { documents } from '../data' + +onmessage = (e) => { + if (e.data === 'LOAD') { + self.postMessage({ documents }) + } +} diff --git a/src/app/document/view/[slug]/DocumentViewer.tsx b/src/app/document/view/[slug]/DocumentViewer.tsx new file mode 100644 index 0000000..a523bb0 --- /dev/null +++ b/src/app/document/view/[slug]/DocumentViewer.tsx @@ -0,0 +1,101 @@ +import { DocumentType, Document } 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' + +const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] }) + +export default function DocumentViewer({ + doc, + slug, +}: Readonly<{ doc: Document; slug: string }>) { + const { manifest, abstract, file, citation } = doc + const { + title, + authors, + topics, + dates, + references, + code, + type, + latest, + reviewers, + status, + } = manifest + + return ( +
+

+ {title} +

+

+ +

+

+ Latest revision published{' '} + + {epoch2datestring(dates[dates.length - 1])} + +

+ + + + Revision {latest} + +
+

Abstract

+

+ {abstract} +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ Cite as: + {citation ? <>{citation} : <>eeXiv:{slug}} +

+ + + +
+ ) +} From 3e97241cf6fdcb2180a65d94f8597e6acb41c895 Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 18:25:19 -0800 Subject: [PATCH 2/7] document promise working --- src/app/db/loaders.ts | 35 +++++- src/app/db/workers/documentLoader.worker.ts | 2 +- src/app/document/view/[slug]/page.tsx | 120 +++----------------- src/app/utils/createResource.ts | 26 +++++ 4 files changed, 76 insertions(+), 107 deletions(-) create mode 100644 src/app/utils/createResource.ts diff --git a/src/app/db/loaders.ts b/src/app/db/loaders.ts index 4f9f58a..ec3e37d 100644 --- a/src/app/db/loaders.ts +++ b/src/app/db/loaders.ts @@ -1,8 +1,39 @@ import { Document } from './data' -// export const loadDocument(id: string): Promise | undefined => { +export const loadDocument = (id: string): Promise => { + 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') { diff --git a/src/app/db/workers/documentLoader.worker.ts b/src/app/db/workers/documentLoader.worker.ts index e19ff6f..b263e79 100644 --- a/src/app/db/workers/documentLoader.worker.ts +++ b/src/app/db/workers/documentLoader.worker.ts @@ -2,6 +2,6 @@ import { documents } from '../data' onmessage = (e) => { if (e.data === 'LOAD') { - self.postMessage({ documents }) + self.postMessage(documents) } } diff --git a/src/app/document/view/[slug]/page.tsx b/src/app/document/view/[slug]/page.tsx index ad5865a..bd3b735 100644 --- a/src/app/document/view/[slug]/page.tsx +++ b/src/app/document/view/[slug]/page.tsx @@ -1,117 +1,29 @@ 'use client' import { Zilla_Slab } from 'next/font/google' -import { DocumentType, documents, reviewer } from '@/app/db/data' -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'] }) +import { DocumentType, reviewer, Document, documents } from '@/app/db/data' +import { loadDocument, loadAllDocuments } from '@/app/db/loaders' +import DocumentViewer from './DocumentViewer' +import createResource from '@/app/utils/createResource' +import { Suspense, useEffect, useMemo } from 'react' export default function Page({ params, }: 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 + // const docResource = createResource(loadDocument(params.slug)) + // const doc = docResource.read() useEffect(() => { - if (status === 'reviewed' && !reviewers) { - toast.warn( - `This document is marked as peer reviewed, but the author - forgot to add a list of reviewers.` - ) + const check = async () => { + const doc = await loadDocument(params.slug) + console.log(doc) } - }, []) + check() + }) return ( -
-

- {title} -

-

- -

-

- Latest revision published{' '} - - {epoch2datestring(dates[dates.length - 1])} - -

- - - - Revision {latest} - -
-

Abstract

-

- {abstract} -

-

- -

-

- -

-

- -

-

- -

-

- Cite as: - {citation ? <>{citation} : <>eeXiv:{params.slug}} -

- - - -
+ <> +
hi
+ + ) } diff --git a/src/app/utils/createResource.ts b/src/app/utils/createResource.ts new file mode 100644 index 0000000..d8dcf7f --- /dev/null +++ b/src/app/utils/createResource.ts @@ -0,0 +1,26 @@ +export default function wrapPromise(promise: Promise) { + let status = 'pending' + let result: any + let suspender = promise.then( + (r) => { + status = 'success' + result = r + }, + (e) => { + status = 'error' + result = e + } + ) + + return { + read() { + if (status === 'pending') { + throw suspender + } else if (status === 'error') { + throw result + } else if (status === 'success') { + return result + } + }, + } +} From 7dc55799b888e91a423fe40ced55e493874b8986 Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 19:42:56 -0800 Subject: [PATCH 3/7] document loading working --- package-lock.json | 25 ++++ package.json | 1 + src/app/db/data.ts | 26 ++-- src/app/db/loaders.ts | 31 +++- .../db/workers/affiliationLoader.worker.ts | 7 + src/app/db/workers/authorLoader.worker.ts | 7 + .../db/workers/nationalityLoader.worker.ts | 7 + src/app/db/workers/topicLoader.worker.ts | 7 + .../document/view/[slug]/DocumentViewer.tsx | 21 ++- src/app/document/view/[slug]/page.tsx | 24 +-- src/app/layout.tsx | 141 +++++++++--------- src/app/providers.jsx | 20 +++ src/app/utils/ErrorBoundary.jsx | 29 ++++ src/app/utils/createResource.ts | 26 ---- 14 files changed, 236 insertions(+), 136 deletions(-) create mode 100644 src/app/db/workers/affiliationLoader.worker.ts create mode 100644 src/app/db/workers/authorLoader.worker.ts create mode 100644 src/app/db/workers/nationalityLoader.worker.ts create mode 100644 src/app/db/workers/topicLoader.worker.ts create mode 100644 src/app/providers.jsx create mode 100644 src/app/utils/ErrorBoundary.jsx delete mode 100644 src/app/utils/createResource.ts diff --git a/package-lock.json b/package-lock.json index 962718f..9f15eeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "eexiv-2", "version": "0.1.0", "dependencies": { + "@tanstack/react-query": "^5.20.2", "minisearch": "^6.3.0", "next": "14.1.0", "react": "^18", @@ -466,6 +467,30 @@ "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", diff --git a/package.json b/package.json index 77e4a3b..65d4611 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "format": "prettier --write ." }, "dependencies": { + "@tanstack/react-query": "^5.20.2", "minisearch": "^6.3.0", "next": "14.1.0", "react": "^18", diff --git a/src/app/db/data.ts b/src/app/db/data.ts index ab5b9bc..5bec8e0 100644 --- a/src/app/db/data.ts +++ b/src/app/db/data.ts @@ -346,22 +346,20 @@ authorName (as a slug): { website: website url } */ -export interface Authors { - [key: string]: { - name: { - first: string - last: string - nickname?: string - } - affiliation: string[] - image: string - nationality: string[] - formerAffiliations?: string[] - bio?: string - website?: string +export interface Author { + name: { + first: string + last: string + nickname?: string } + affiliation: string[] + image: string + nationality: string[] + formerAffiliations?: string[] + bio?: string + website?: string } -export const authors: Authors = { +export const authors: { [key: string]: Author } = { shasan: { name: { first: 'Saim', diff --git a/src/app/db/loaders.ts b/src/app/db/loaders.ts index ec3e37d..af4bcce 100644 --- a/src/app/db/loaders.ts +++ b/src/app/db/loaders.ts @@ -1,4 +1,4 @@ -import { Document } from './data' +import { Document, Author } from './data' export const loadDocument = (id: string): Promise => { return new Promise((resolve, reject) => { @@ -62,3 +62,32 @@ export const loadAllDocuments = (): Promise<{ [key: string]: Document }> => { } }) } + +export const loadAllAuthors = (): Promise<{ [key: string]: Author }> => { + 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]: 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.' + ) + ) + } + }) +} diff --git a/src/app/db/workers/affiliationLoader.worker.ts b/src/app/db/workers/affiliationLoader.worker.ts new file mode 100644 index 0000000..fd489c7 --- /dev/null +++ b/src/app/db/workers/affiliationLoader.worker.ts @@ -0,0 +1,7 @@ +import { affiliations } from '../data' + +onmessage = (e) => { + if (e.data === 'LOAD') { + self.postMessage(affiliations) + } +} diff --git a/src/app/db/workers/authorLoader.worker.ts b/src/app/db/workers/authorLoader.worker.ts new file mode 100644 index 0000000..4ec63fe --- /dev/null +++ b/src/app/db/workers/authorLoader.worker.ts @@ -0,0 +1,7 @@ +import { authors } from '../data' + +onmessage = (e) => { + if (e.data === 'LOAD') { + self.postMessage(authors) + } +} diff --git a/src/app/db/workers/nationalityLoader.worker.ts b/src/app/db/workers/nationalityLoader.worker.ts new file mode 100644 index 0000000..3b9aa9e --- /dev/null +++ b/src/app/db/workers/nationalityLoader.worker.ts @@ -0,0 +1,7 @@ +import { nationalities } from '../data' + +onmessage = (e) => { + if (e.data === 'LOAD') { + self.postMessage(nationalities) + } +} diff --git a/src/app/db/workers/topicLoader.worker.ts b/src/app/db/workers/topicLoader.worker.ts new file mode 100644 index 0000000..080a691 --- /dev/null +++ b/src/app/db/workers/topicLoader.worker.ts @@ -0,0 +1,7 @@ +import { topics } from '../data' + +onmessage = (e) => { + if (e.data === 'LOAD') { + self.postMessage(topics) + } +} diff --git a/src/app/document/view/[slug]/DocumentViewer.tsx b/src/app/document/view/[slug]/DocumentViewer.tsx index a523bb0..62bdb84 100644 --- a/src/app/document/view/[slug]/DocumentViewer.tsx +++ b/src/app/document/view/[slug]/DocumentViewer.tsx @@ -1,4 +1,4 @@ -import { DocumentType, Document } from '@/app/db/data' +import { DocumentType } from '@/app/db/data' import { Zilla_Slab } from 'next/font/google' import { epoch2datestring } from '@/app/utils/epoch2datestring' import { @@ -10,14 +10,21 @@ import { } 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({ - doc, - slug, -}: Readonly<{ doc: Document; slug: string }>) { - const { manifest, abstract, file, citation } = doc +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, @@ -31,6 +38,8 @@ export default function DocumentViewer({ status, } = manifest + if (error) throw error + return (

) { - // const docResource = createResource(loadDocument(params.slug)) - // const doc = docResource.read() - - useEffect(() => { - const check = async () => { - const doc = await loadDocument(params.slug) - console.log(doc) - } - check() - }) - return ( - <> -
hi
- - + + + ) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d9fe98d..2ff9b1e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import Container from './container/Container' import MobileMenu from './components/MobileMenu' import { ToastContainer } from 'react-toastify' 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, which are pre-provided by Next.js in the 'next/font/google' module), you need to @@ -31,80 +32,82 @@ export default function RootLayout({ return ( - -
-
- - EECS - - -
-
-
-
-

- - eeXiv + + +
+
+ + EECS -

-
- -
-
- +
-
- {children} -
-
-
    -
  • - About -
  • -
  • - Help -
  • -
  • - Contact -
  • -
  • - Subscribe -
  • -
  • - Copyright -
  • -
  • - Privacy Policy -
  • -
  • - Accessibility -
  • -
  • - eeXiv status -
  • -
  • - - Get status notifications +
    +
    +

    + + eeXiv -

  • -
+

+
+ +
+
+ +
+
- + {children} +
+
+
    +
  • + About +
  • +
  • + Help +
  • +
  • + Contact +
  • +
  • + Subscribe +
  • +
  • + Copyright +
  • +
  • + Privacy Policy +
  • +
  • + Accessibility +
  • +
  • + eeXiv status +
  • +
  • + + Get status notifications + +
  • +
+
+
+ ) diff --git a/src/app/providers.jsx b/src/app/providers.jsx new file mode 100644 index 0000000..8b311d6 --- /dev/null +++ b/src/app/providers.jsx @@ -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 ( + {children} + ) +} diff --git a/src/app/utils/ErrorBoundary.jsx b/src/app/utils/ErrorBoundary.jsx new file mode 100644 index 0000000..48e99a0 --- /dev/null +++ b/src/app/utils/ErrorBoundary.jsx @@ -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

Something went wrong.

+ } + + return this.props.children + } +} diff --git a/src/app/utils/createResource.ts b/src/app/utils/createResource.ts deleted file mode 100644 index d8dcf7f..0000000 --- a/src/app/utils/createResource.ts +++ /dev/null @@ -1,26 +0,0 @@ -export default function wrapPromise(promise: Promise) { - let status = 'pending' - let result: any - let suspender = promise.then( - (r) => { - status = 'success' - result = r - }, - (e) => { - status = 'error' - result = e - } - ) - - return { - read() { - if (status === 'pending') { - throw suspender - } else if (status === 'error') { - throw result - } else if (status === 'success') { - return result - } - }, - } -} From a074952678d44553894bd4acf24d8b194e9aa9da Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 20:50:00 -0800 Subject: [PATCH 4/7] migrated to web workers for client side components --- README.md | 14 ++ src/app/author/[author]/AuthorDisplay.tsx | 170 +++++++++++++++ src/app/author/[author]/page.tsx | 178 +--------------- src/app/components/DataDisplay.tsx | 49 +++-- src/app/db/data.ts | 38 ++-- src/app/db/loaders.ts | 241 +++++++++++++++++++++- 6 files changed, 476 insertions(+), 214 deletions(-) create mode 100644 src/app/author/[author]/AuthorDisplay.tsx diff --git a/README.md b/README.md index 0d4c5e9..413f291 100644 --- a/README.md +++ b/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)) - 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: ``. +- 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. diff --git a/src/app/author/[author]/AuthorDisplay.tsx b/src/app/author/[author]/AuthorDisplay.tsx new file mode 100644 index 0000000..1c32ba5 --- /dev/null +++ b/src/app/author/[author]/AuthorDisplay.tsx @@ -0,0 +1,170 @@ +import Link from 'next/link' +import { Fragment } from 'react' +import { affiliations, nationalities, authors } from '../../db/data' +import { Zilla_Slab } from 'next/font/google' + +const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] }) + +export default function AuthorDisplay({ + author, +}: Readonly<{ author: string }>) { + const data = authors[author] + 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 ( + <> + {mainPosition} at + + {mainAffiliation.name} + + {website ? ( +
+ Visit {name.nickname ? name.nickname : name.first} at:{' '} + + {website} + +
+ ) : null} +
+ {affiliation.map((a: string) => ( + + {affiliations[a.split('@')[1]].name} + + ))} +
+ + ) + } + + const OtherPositions = () => { + if (affiliation.length === 1) return + return ( + <> +

+ Other Positions: +

+ {affiliation.slice(1).map((a: string, i: number) => { + const position = a.split('@')[0] + const affiliation = affiliations[a.split('@')[1]].name + return ( + + + {position} at{' '} + + {affiliation} + + + + ) + })} + + ) + } + + const FormerPositions = () => { + if (!formerAffiliations) return null + return ( + <> +

+ Former Positions: +

+ {formerAffiliations?.map((a: string, i: number) => { + const position = a.split('@')[0] + const affiliation = affiliations[a.split('@')[1]].name + + return ( + + + {position} at{' '} + + {affiliation} + + + + ) + })} + + ) + } + + const NationalityDisplay = ({ + nationality, + }: Readonly<{ nationality: string }>) => { + const nationalityData = nationalities[nationality] + const { demonym, flag } = nationalityData + return ( +
+ {`${demonym} + {demonym} +
+ ) + } + + const Bio = () => { + const { bio } = data + if (!bio) return null + + return ( + <> +

Bio:

+

{bio}

+ + ) + } + + return ( +
+
+
+ profile +
+
+
+ + {name.first} + {name.nickname ? ` "${name.nickname}"` : null} {name.last} + +
+ +
+
+
+
+
+
+ + +

+ Ethnicity and Nationality: +

+
+ {nationality.map((n: string) => ( + + + + ))} +
+ +
+
+ ) +} diff --git a/src/app/author/[author]/page.tsx b/src/app/author/[author]/page.tsx index 48f02e2..0942ac5 100644 --- a/src/app/author/[author]/page.tsx +++ b/src/app/author/[author]/page.tsx @@ -1,10 +1,5 @@ -import { authors, affiliations, nationalities } from '../../db/data' -import { Zilla_Slab } from 'next/font/google' -import Link from 'next/link' -import { notFound } from 'next/navigation' -import { Fragment, Suspense } from 'react' - -const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500'] }) +import AuthorDisplay from './AuthorDisplay' +import { authors } from '@/app/db/data' export function generateStaticParams() { const authorsList = Object.keys(authors) @@ -16,172 +11,5 @@ export default function Page({ }: Readonly<{ params: { author: string } }>) { - const authorData = authors[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 ( - <> - {mainPosition} at - - {mainAffiliation.name} - - {authorData.website ? ( -
- Visit{' '} - {authorData.name.nickname - ? authorData.name.nickname - : authorData.name.first}{' '} - at:{' '} - - {authorData.website} - -
- ) : null} -
- {affiliation.map((a: string) => ( - - {affiliations[a.split('@')[1]].name} - - ))} -
- - ) - } - - const OtherPositions = () => { - if (affiliation.length === 1) return - return ( - <> -

- Other Positions: -

- {affiliation.slice(1).map((a: string, i: number) => { - const position = a.split('@')[0] - const affiliation = affiliations[a.split('@')[1]].name - return ( - - - {position} at{' '} - - {affiliation} - - - - ) - })} - - ) - } - - const FormerPositions = () => { - if (!formerAffiliations) return null - return ( - <> -

- Former Positions: -

- {formerAffiliations?.map((a: string, i: number) => { - const position = a.split('@')[0] - const affiliation = affiliations[a.split('@')[1]].name - - return ( - - - {position} at{' '} - - {affiliation} - - - - ) - })} - - ) - } - - const NationalityDisplay = ({ - nationality, - }: Readonly<{ nationality: string }>) => { - const nationalityData = nationalities[nationality] - const { demonym, flag } = nationalityData - return ( -
- {`${demonym} - {demonym} -
- ) - } - - const Bio = () => { - const { bio } = authorData - if (!bio) return null - - return ( - <> -

Bio:

-

{authorData.bio}

- - ) - } - - return ( -
-
-
- profile -
-
-
- - {name.first} - {name.nickname ? ` "${name.nickname}"` : null} {name.last} - -
- -
-
-
-
-
-
- - -

- Ethnicity and Nationality: -

-
- {nationality.map((n: string) => ( - - - - ))} -
- -
-
- ) + return } diff --git a/src/app/components/DataDisplay.tsx b/src/app/components/DataDisplay.tsx index 16a6868..ce800b5 100644 --- a/src/app/components/DataDisplay.tsx +++ b/src/app/components/DataDisplay.tsx @@ -1,10 +1,8 @@ import { Fragment } from 'react' import Link from 'next/link' -import { - topics as topicList, - authors as authorList, - reviewer, -} from '@/app/db/data' +import { reviewer } from '@/app/db/data' +import { loadAllTopics, loadAllAuthors } from '@/app/db/loaders' +import { useSuspenseQuery } from '@tanstack/react-query' export const Code = ({ code, @@ -54,6 +52,18 @@ export const Topics = ({ topics, showTitle = true, }: 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 ( <> {showTitle ? Topics: : null} @@ -72,6 +82,19 @@ export const Topics = ({ export const Authors = ({ authors, }: 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 ( <> {authors.map((a: string, i) => ( @@ -95,20 +118,16 @@ export const Reviewers = ({ const ReviewerDisplay = ({ r }: Readonly<{ r: reviewer }>) => { if (r.profile) { return ( - <> - - {r.first} {r.last} - - + + {r.first} {r.last} + ) } if (r.url) { return ( - <> - - {r.first} {r.last} - - + + {r.first} {r.last} + ) } return ( diff --git a/src/app/db/data.ts b/src/app/db/data.ts index 5bec8e0..4ab5ce5 100644 --- a/src/app/db/data.ts +++ b/src/app/db/data.ts @@ -282,14 +282,12 @@ On Day 1 we will discuss the ins and outs of the robot. The Electrical Sub team }, } -export interface Topics { - [key: string]: { - name: string - description: string - wiki: string - } +export interface Topic { + name: string + description: string + wiki: string } -export const topics: Topics = { +export const topics: { [key: string]: Topic } = { frc: { name: 'FIRST Robotics Competition', description: @@ -499,16 +497,14 @@ export const authors: { [key: string]: Author } = { }, } -export interface Affiliations { - [key: string]: { - name: string - short: string - image: string - description: string - } +export interface Affiliation { + name: string + short: string + image: string + description: string } -export const affiliations: Affiliations = { +export const affiliations: { [key: string]: Affiliation } = { '1280-mech': { name: "Team 1280, the Ragin' C Biscuits, Mechanical Subteam", short: '1280 Mech', @@ -659,14 +655,12 @@ Raid Zero's influence extends beyond the technical achievements in robotics comp }, } -export interface Nationalities { - [key: string]: { - name: string - demonym: string - flag: string - } +export interface Nationality { + name: string + demonym: string + flag: string } -export const nationalities: Nationalities = { +export const nationalities: { [key: string]: Nationality } = { pak: { name: 'Pakistan', demonym: 'Pakistani', diff --git a/src/app/db/loaders.ts b/src/app/db/loaders.ts index af4bcce..2af5727 100644 --- a/src/app/db/loaders.ts +++ b/src/app/db/loaders.ts @@ -1,4 +1,4 @@ -import { Document, Author } from './data' +import { Document, Author, Affiliation, Topic, Nationality } from './data' export const loadDocument = (id: string): Promise => { return new Promise((resolve, reject) => { @@ -67,7 +67,7 @@ export const loadAllAuthors = (): Promise<{ [key: string]: Author }> => { return new Promise((resolve, reject) => { if (typeof Worker !== 'undefined') { const worker = new Worker( - new URL('./workers/documentLoader.worker.ts', import.meta.url), + new URL('./workers/authorLoader.worker.ts', import.meta.url), { type: 'module' } ) @@ -91,3 +91,240 @@ export const loadAllAuthors = (): Promise<{ [key: string]: Author }> => { } }) } + +export const loadAuthor = (id: string): Promise => { + 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 => { + 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 => { + 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 => { + 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.' + ) + ) + } + }) +} From 0c4e56ab7493736a7173fe41e850fd085dd44c7c Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 21:21:55 -0800 Subject: [PATCH 5/7] create search web worker --- src/app/author/[author]/AuthorDisplay.tsx | 5 ++ src/app/db/workers/README | 2 + src/app/utils/search.worker.ts | 34 +++++++++++++ src/app/utils/searchDocs.ts | 62 +++++++++++------------ 4 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 src/app/db/workers/README create mode 100644 src/app/utils/search.worker.ts diff --git a/src/app/author/[author]/AuthorDisplay.tsx b/src/app/author/[author]/AuthorDisplay.tsx index 1c32ba5..969d340 100644 --- a/src/app/author/[author]/AuthorDisplay.tsx +++ b/src/app/author/[author]/AuthorDisplay.tsx @@ -2,6 +2,7 @@ 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'] }) @@ -9,6 +10,10 @@ export default function AuthorDisplay({ author, }: Readonly<{ author: string }>) { const data = authors[author] + if (!data) { + notFound() + } + const { name, affiliation, image, nationality, formerAffiliations } = data const MainPosition = () => { diff --git a/src/app/db/workers/README b/src/app/db/workers/README new file mode 100644 index 0000000..6d06a52 --- /dev/null +++ b/src/app/db/workers/README @@ -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. diff --git a/src/app/utils/search.worker.ts b/src/app/utils/search.worker.ts new file mode 100644 index 0000000..b3977e8 --- /dev/null +++ b/src/app/utils/search.worker.ts @@ -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) => { + postMessage(miniSearch.search(e.data) as CustomSearchResult[]) +} diff --git a/src/app/utils/searchDocs.ts b/src/app/utils/searchDocs.ts index bb5d571..7a668e8 100644 --- a/src/app/utils/searchDocs.ts +++ b/src/app/utils/searchDocs.ts @@ -1,32 +1,5 @@ -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) +import { SearchResult } from 'minisearch' +import { DocumentManifest } from '../db/data' export interface CustomSearchResult extends SearchResult { manifest: DocumentManifest @@ -36,6 +9,33 @@ export interface CustomSearchResult extends SearchResult { export default function searchDocs( query: string, limit = 10 -): CustomSearchResult[] { - return miniSearch.search(query).slice(0, limit) as CustomSearchResult[] +): Promise { + 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) => { + 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.` + ) + ) + } + }) } From ba2d6acd25c351b0b826c627bad17f3cf0f1759d Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 21:27:12 -0800 Subject: [PATCH 6/7] search asynchronous working --- src/app/search/page.tsx | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 59f6cd1..15f9b7d 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -1,27 +1,15 @@ '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' +import { useSuspenseQuery } from '@tanstack/react-query' const zillaSlab = Zilla_Slab({ subsets: ['latin'], weight: ['500', '700'] }) -interface SearchState { - results: CustomSearchResult[] - setResults: (newResults: CustomSearchResult[]) => void -} -const useSearchStore = create((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 @@ -66,11 +54,15 @@ const SearchResult = ({ result }: { result: CustomSearchResult }) => { 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]) + + const { data, error } = useSuspenseQuery({ + queryKey: [search], + queryFn: () => { + const data = searchDocs(search) + return data + }, + }) + if (error) throw error return (
@@ -80,12 +72,10 @@ export default function Page() { {search} {`"`} - {searchStore.length === 0 ? ( + {data.length === 0 ? (

No results found.

) : ( - searchStore.map((result) => ( - - )) + data.map((result) => ) )}
) From 595d1771d54a33d8b805a60667b5ef10134177e8 Mon Sep 17 00:00:00 2001 From: Youwen Wu Date: Mon, 12 Feb 2024 21:33:54 -0800 Subject: [PATCH 7/7] tested working in production, update news --- src/app/components/News.tsx | 12 +++++++----- src/app/components/SearchBar.tsx | 2 -- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/components/News.tsx b/src/app/components/News.tsx index bb8373e..93e0683 100644 --- a/src/app/components/News.tsx +++ b/src/app/components/News.tsx @@ -13,13 +13,15 @@ export default function News() {
  • - eeXiv 1.0 has been released! All basic features like search and - document viewing are available. + eeXiv 2.0 has been released! The site should feel significantly more + responsive. Data cacheing has also been implemented so search results + and documents will load instantly the second time.
  • -
  • eeXiv is currently under active development!
  • +
  • Mobile support is currently in beta.
  • - There may be major updates, breaking changes, or weird bugs. Report - bugs, suggest new features, or give us feedback at{' '} + eeXiv is currently under active development! There may be major + updates, breaking changes, or weird bugs. Report bugs, suggest new + features, or give us feedback at{' '} our issue tracker. diff --git a/src/app/components/SearchBar.tsx b/src/app/components/SearchBar.tsx index d1c4ccb..8218bbd 100644 --- a/src/app/components/SearchBar.tsx +++ b/src/app/components/SearchBar.tsx @@ -17,7 +17,6 @@ export default function SearchBar() { const setSearchBarStore = useSearchBarStore((state) => state.setSearchInput) const handleClick = (event: React.MouseEvent) => { - event.preventDefault() navigate(`/search?q=${searchBarStore.split(' ').join('+')}`) } @@ -27,7 +26,6 @@ export default function SearchBar() { const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - e.preventDefault() navigate(`/search?q=${searchBarStore.split(' ').join('+')}`) } }