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/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/author/[author]/AuthorDisplay.tsx b/src/app/author/[author]/AuthorDisplay.tsx
new file mode 100644
index 0000000..969d340
--- /dev/null
+++ b/src/app/author/[author]/AuthorDisplay.tsx
@@ -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 (
+ <>
+ {mainPosition} at
+
+ {mainAffiliation.name}
+
+ {website ? (
+
- 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('+')}`)
}
}
diff --git a/src/app/db/data.ts b/src/app/db/data.ts
index ab5b9bc..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:
@@ -346,22 +344,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',
@@ -501,16 +497,14 @@ export const authors: Authors = {
},
}
-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',
@@ -661,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
new file mode 100644
index 0000000..2af5727
--- /dev/null
+++ b/src/app/db/loaders.ts
@@ -0,0 +1,330 @@
+import { Document, Author, Affiliation, Topic, Nationality } from './data'
+
+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') {
+ 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 => {
+ 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.'
+ )
+ )
+ }
+ })
+}
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/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/documentLoader.worker.ts b/src/app/db/workers/documentLoader.worker.ts
new file mode 100644
index 0000000..b263e79
--- /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/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
new file mode 100644
index 0000000..62bdb84
--- /dev/null
+++ b/src/app/document/view/[slug]/DocumentViewer.tsx
@@ -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 (
+
+
+
+
)
}
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 (
-
-
-
-
-
-
-
- We gratefully acknowledge support from our volunteer peer
- reviewers, member institutions, and all{' '}
-
- open-source contributors
-
- .
-
-
-
-
-
-
-
- eeXiv
+
+
+
+
+
+
-
-
-
-
-
-
+
+ We gratefully acknowledge support from our volunteer peer
+ reviewers, member institutions, and all{' '}
+
+ open-source contributors
+
+ .
+