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) => (
+
+
+
+ ))}
+
+ >
+ )
+ }
+
+ 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}
+
+ )
+ }
+
+ const Bio = () => {
+ const { bio } = data
+ if (!bio) return null
+
+ return (
+ <>
+ Bio:
+ {bio}
+ >
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {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) => (
-
-
-
- ))}
-
- >
- )
- }
-
- 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}
-
- )
- }
-
- const Bio = () => {
- const { bio } = authorData
- if (!bio) return null
-
- return (
- <>
- Bio:
- {authorData.bio}
- >
- )
- }
-
- return (
-
-
-
-
-
-
-
-
- {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.'
+ )
+ )
+ }
+ })
+}