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 ? ( +
+ 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/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() {