From d71df2b0d70de804f430d9bd6fc7c2d21a01ef33 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 16:28:10 -0800 Subject: [PATCH 01/13] Explicitly set port for production At least on our machine, 3000 conflicts with the API --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac4d8332..e53ad34a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "next dev -p 1234", "build": "next build", - "start": "next start", + "start": "next start -p 2345", "lint": "next lint" }, "dependencies": { From 49166600270cf57fdd47c4431a558eed5751f020 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 16:28:23 -0800 Subject: [PATCH 02/13] Add ErrorSection component This lets us render errors gracefully --- components/ErrorSection/index.scss | 22 ++++++++++++++ components/ErrorSection/index.tsx | 48 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 components/ErrorSection/index.scss create mode 100644 components/ErrorSection/index.tsx diff --git a/components/ErrorSection/index.scss b/components/ErrorSection/index.scss new file mode 100644 index 00000000..587d9ff9 --- /dev/null +++ b/components/ErrorSection/index.scss @@ -0,0 +1,22 @@ +section.Error { + align-items: center; + display: flex; + flex-direction: column; + gap: $unit; + margin: 0 auto; + max-width: 50vw; + justify-content: center; + height: 60vh; + text-align: center; + + .Code { + color: var(--text-secondary); + font-size: $font-tiny; + font-weight: $bold; + } + + .Button { + margin-top: $unit-2x; + width: fit-content; + } +} diff --git a/components/ErrorSection/index.tsx b/components/ErrorSection/index.tsx new file mode 100644 index 00000000..f0b904c6 --- /dev/null +++ b/components/ErrorSection/index.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from 'react' +import Link from 'next/link' +import { useTranslation } from 'next-i18next' + +import Button from '~components/Button' +import { ResponseStatus } from '~types' + +import './index.scss' + +interface Props { + status: ResponseStatus +} + +const ErrorSection = ({ status }: Props) => { + // Import translations + const { t } = useTranslation('common') + + const [statusText, setStatusText] = useState('') + + useEffect(() => { + setStatusText(status.text.replace(' ', '_').toLowerCase()) + }, [status.text]) + + const errorBody = () => { + return ( + <> +
{status.code}
+

{t(`errors.${statusText}.title`)}

+

{t(`errors.${statusText}.description`)}

+ + ) + } + + return ( +
+ {errorBody()} + {[401, 404].includes(status.code) ? ( + +
+ ) +} + +export default ErrorSection From 7b06a2900e4131ea3ccdcd4fbb6f1f72db374453 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 16:28:42 -0800 Subject: [PATCH 03/13] Add a line-height for headers and paragraphs I'm sick of setting `line-height` --- styles/globals.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/globals.scss b/styles/globals.scss index 694c6f47..d9d12f8a 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -75,6 +75,7 @@ h2, h3, p { color: var(--text-primary); + line-height: 1.3; } h1 { From 9476f592f00c24952ba04d410a9141962ecc869e Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 16:46:32 -0800 Subject: [PATCH 04/13] Add status to appState and new types --- types/index.d.ts | 15 +++++++++++++++ utils/appState.tsx | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 73ec031c..21337840 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -70,3 +70,18 @@ interface PerpetuityObject { perpetuity: boolean } } + +interface PageContextObj { + party?: Party + jobs: Job[] + jobSkills: JobSkill[] + raids: Raid[] + sortedRaids: Raid[][] + weaponKeys: GroupedWeaponKeys + meta: { [key: string]: string } +} + +interface ResponseStatus { + code: number + text: string +} diff --git a/utils/appState.tsx b/utils/appState.tsx index 816ba19a..d9e9db07 100644 --- a/utils/appState.tsx +++ b/utils/appState.tsx @@ -1,5 +1,5 @@ import { proxy } from 'valtio' -import { JobSkillObject } from '~types' +import { JobSkillObject, ResponseStatus } from '~types' import { GroupedWeaponKeys } from './groupWeaponKeys' const emptyJob: Job = { @@ -86,6 +86,7 @@ interface AppState { jobSkills: JobSkill[] weaponKeys: GroupedWeaponKeys version: AppUpdate + status?: ResponseStatus } export const initialAppState: AppState = { @@ -156,6 +157,7 @@ export const initialAppState: AppState = { update_type: '', updated_at: '', }, + status: undefined, } export const appState = proxy(initialAppState) From 6f51e21b5898c4178968d0f9d4d757e2bbf80705 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 16:46:48 -0800 Subject: [PATCH 05/13] Don't show Header context when there is an error --- components/Header/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/Header/index.tsx b/components/Header/index.tsx index de1c5810..57a27956 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -10,7 +10,6 @@ import Link from 'next/link' import api from '~utils/api' import { accountState, initialAccountState } from '~utils/accountState' import { appState } from '~utils/appState' -import capitalizeFirstLetter from '~utils/capitalizeFirstLetter' import { DropdownMenu, @@ -303,7 +302,7 @@ const Header = () => { - {pageTitle()} + {!appState.errorCode ? pageTitle() : ''} ) } @@ -313,10 +312,13 @@ const Header = () => {
{router.route === '/p/[party]' && account.user && - (!party.user || party.user.id !== account.user.id) + (!party.user || party.user.id !== account.user.id) && + !appState.errorCode ? saveButton() : ''} - {router.route === '/p/[party]' ? remixButton() : ''} + {router.route === '/p/[party]' && !appState.errorCode + ? remixButton() + : ''} Date: Sat, 28 Jan 2023 17:20:44 -0800 Subject: [PATCH 06/13] Refactor [party] page This is a blueprint for all the other pages --- components/PartyHead/index.tsx | 73 +++++++++++ pages/p/[party].tsx | 232 ++++++++++++++------------------- types/index.d.ts | 11 +- utils/elementEmoji.tsx | 14 ++ utils/generateTitle.tsx | 2 +- utils/getElementForParty.tsx | 8 ++ 6 files changed, 199 insertions(+), 141 deletions(-) create mode 100644 components/PartyHead/index.tsx create mode 100644 utils/elementEmoji.tsx create mode 100644 utils/getElementForParty.tsx diff --git a/components/PartyHead/index.tsx b/components/PartyHead/index.tsx new file mode 100644 index 00000000..3f8a764d --- /dev/null +++ b/components/PartyHead/index.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import Head from 'next/head' +import { useRouter } from 'next/router' +import { useTranslation } from 'next-i18next' + +import generateTitle from '~utils/generateTitle' + +interface Props { + party: Party + meta: { [key: string]: string } +} + +const PartyHead = ({ party, meta }: Props) => { + // Import translations + const { t } = useTranslation('common') + + // Set up router + const router = useRouter() + const locale = + router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' + + return ( + + {/* HTML */} + + {generateTitle(meta.element, party.user?.username, party.name)} + + + + + {/* OpenGraph */} + + + + + + {/* Twitter */} + + + + + + ) +} + +export default PartyHead diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index b7e16f22..0722556f 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -1,43 +1,40 @@ import React, { useEffect, useState } from 'react' -import Head from 'next/head' import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import Party from '~components/Party' +import ErrorSection from '~components/ErrorSection' +import PartyHead from '~components/PartyHead' import api from '~utils/api' -import generateTitle from '~utils/generateTitle' +import elementEmoji from '~utils/elementEmoji' +import fetchLatestVersion from '~utils/fetchLatestVersion' import organizeRaids from '~utils/organizeRaids' import setUserToken from '~utils/setUserToken' import { appState } from '~utils/appState' import { groupWeaponKeys } from '~utils/groupWeaponKeys' -import { GridType } from '~utils/enums' -import { printError } from '~utils/reportError' +import { GridType } from '~utils/enums' import type { NextApiRequest, NextApiResponse } from 'next' -import type { GroupedWeaponKeys } from '~utils/groupWeaponKeys' +import type { PageContextObj, ResponseStatus } from '~types' +import type { AxiosError } from 'axios' interface Props { - party: Party - jobs: Job[] - jobSkills: JobSkill[] - raids: Raid[] - sortedRaids: Raid[][] - weaponKeys: GroupedWeaponKeys - meta: { [key: string]: string } + context?: PageContextObj + version: AppUpdate + error: boolean + status?: ResponseStatus } -const PartyRoute: React.FC = (props: Props) => { - // Import translations - const { t } = useTranslation('common') - - // Set up router +const PartyRoute: React.FC = ({ + context, + version, + error, + status, +}: Props) => { + // Set up state to save selected tab and + // update when router changes const router = useRouter() - const locale = - router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' - - // URL state const [selectedTab, setSelectedTab] = useState(GridType.Weapon) useEffect(() => { @@ -57,86 +54,45 @@ const PartyRoute: React.FC = (props: Props) => { } }, [router.asPath]) - // Static data + // Set the initial data from props useEffect(() => { - persistStaticData() - }, [persistStaticData]) + if (context && !error) { + appState.raids = context.raids + appState.jobs = context.jobs ? context.jobs : [] + appState.jobSkills = context.jobSkills ? context.jobSkills : [] + appState.weaponKeys = context.weaponKeys + } - function persistStaticData() { - appState.raids = props.raids - appState.jobs = props.jobs - appState.jobSkills = props.jobSkills - appState.weaponKeys = props.weaponKeys + if (status && error) { + appState.status = status + } + + appState.version = version + }, []) + + // Methods: Page component rendering + function pageHead() { + if (context && context.party && context.meta) + return } - return ( - - - - {/* HTML */} - - {generateTitle( - props.meta.element, - props.party.user?.username, - props.party.name - )} - - - + function pageError() { + if (status) return + else return
+ } - {/* OpenGraph */} - + {pageHead()} + - - - - - {/* Twitter */} - - - - - - - ) + + ) + } else return pageError() } export const getServerSidePaths = async () => { @@ -154,47 +110,28 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex // Set headers for server-side requests setUserToken(req, res) - function getElement(party?: Party) { - if (party) { - const mainhand = party.weapons.find((weapon) => weapon.mainhand) - if (mainhand && mainhand.object.element === 0) { - return mainhand.element - } else { - return mainhand?.object.element - } - } else { - return 0 - } - } - - function elementEmoji(party?: Party) { - const element = getElement(party) - - if (element === 0) return 'โšช' - else if (element === 1) return '๐ŸŸข' - else if (element === 2) return '๐Ÿ”ด' - else if (element === 3) return '๐Ÿ”ต' - else if (element === 4) return '๐ŸŸค' - else if (element === 5) return '๐ŸŸฃ' - else if (element === 6) return '๐ŸŸก' - else return 'โšช' - } + // Fetch latest version + const version = await fetchLatestVersion() try { + // Fetch and organize raids let { raids, sortedRaids } = await api.endpoints.raids .getAll() .then((response) => organizeRaids(response.data)) - let jobs = await api.endpoints.jobs.getAll().then((response) => { - return response.data - }) + // Fetch jobs and job skills + let jobs = await api.endpoints.jobs + .getAll() + .then((response) => response.data) let jobSkills = await api.allJobSkills().then((response) => response.data) + // Fetch and organize weapon keys let weaponKeys = await api.endpoints.weapon_keys .getAll() .then((response) => groupWeaponKeys(response.data)) + // Fetch the party let party: Party | undefined = undefined if (query.party) { let response = await api.endpoints.parties.getOne({ @@ -202,26 +139,49 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex }) party = response.data.party } else { - console.log('No party code') + console.error('No party code') } + // Consolidate data into context object + const context: PageContextObj = { + party: party, + jobs: jobs, + jobSkills: jobSkills, + raids: raids, + sortedRaids: sortedRaids, + weaponKeys: weaponKeys, + meta: { + element: elementEmoji(party), + }, + } + + // Pass to the page component as props return { props: { - party: party, - jobs: jobs, - jobSkills: jobSkills, - raids: raids, - sortedRaids: sortedRaids, - weaponKeys: weaponKeys, - meta: { - element: elementEmoji(party), - }, + context: context, + version: version, + error: false, ...(await serverSideTranslations(locale, ['common', 'roadmap'])), - // Will be passed to the page component as props }, } } catch (error) { - printError(error, 'axios') + // Extract the underlying Axios error + const axiosError = error as AxiosError + const response = axiosError.response + + // Pass to the page component as props + return { + props: { + context: null, + error: true, + version: version, + status: { + code: response?.status, + text: response?.statusText, + }, + ...(await serverSideTranslations(locale, ['common', 'roadmap'])), + }, + } } } diff --git a/types/index.d.ts b/types/index.d.ts index 21337840..fd93e089 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -72,13 +72,16 @@ interface PerpetuityObject { } interface PageContextObj { + user?: User + teams?: Party[] party?: Party - jobs: Job[] - jobSkills: JobSkill[] + jobs?: Job[] + jobSkills?: JobSkill[] raids: Raid[] sortedRaids: Raid[][] - weaponKeys: GroupedWeaponKeys - meta: { [key: string]: string } + weaponKeys?: GroupedWeaponKeys + pagination?: PaginationObject + meta?: { [key: string]: string } } interface ResponseStatus { diff --git a/utils/elementEmoji.tsx b/utils/elementEmoji.tsx new file mode 100644 index 00000000..25195603 --- /dev/null +++ b/utils/elementEmoji.tsx @@ -0,0 +1,14 @@ +import getElementForParty from './getElementForParty' + +export default function elementEmoji(party?: Party) { + const element = party ? getElementForParty(party) : 0 + + if (element === 0) return 'โšช' + else if (element === 1) return '๐ŸŸข' + else if (element === 2) return '๐Ÿ”ด' + else if (element === 3) return '๐Ÿ”ต' + else if (element === 4) return '๐ŸŸค' + else if (element === 5) return '๐ŸŸฃ' + else if (element === 6) return '๐ŸŸก' + else return 'โšช' +} diff --git a/utils/generateTitle.tsx b/utils/generateTitle.tsx index 70582204..ee6b9c5a 100644 --- a/utils/generateTitle.tsx +++ b/utils/generateTitle.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'next-i18next' export default function generateTitle( - element: string, + element?: string, username?: string, name?: string ) { diff --git a/utils/getElementForParty.tsx b/utils/getElementForParty.tsx new file mode 100644 index 00000000..9e2595ae --- /dev/null +++ b/utils/getElementForParty.tsx @@ -0,0 +1,8 @@ +export default function getElementForParty(party: Party) { + const mainhand = party.weapons.find((weapon) => weapon.mainhand) + if (mainhand && mainhand.object.element === 0) { + return mainhand.element + } else { + return mainhand?.object.element + } +} From 96763defc3622ad852abcc3a81abd57a521f493a Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 17:42:54 -0800 Subject: [PATCH 07/13] Fix ErrorSection text replacement --- components/ErrorSection/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ErrorSection/index.tsx b/components/ErrorSection/index.tsx index f0b904c6..5f8c4cae 100644 --- a/components/ErrorSection/index.tsx +++ b/components/ErrorSection/index.tsx @@ -18,7 +18,7 @@ const ErrorSection = ({ status }: Props) => { const [statusText, setStatusText] = useState('') useEffect(() => { - setStatusText(status.text.replace(' ', '_').toLowerCase()) + setStatusText(status.text.replaceAll(' ', '_').toLowerCase()) }, [status.text]) const errorBody = () => { From 337808a922263c710d195edad18a3be8efaf3cca Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 17:43:02 -0800 Subject: [PATCH 08/13] Fix ErrorSection maxWidth --- components/ErrorSection/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ErrorSection/index.scss b/components/ErrorSection/index.scss index 587d9ff9..be07a46c 100644 --- a/components/ErrorSection/index.scss +++ b/components/ErrorSection/index.scss @@ -4,7 +4,7 @@ section.Error { flex-direction: column; gap: $unit; margin: 0 auto; - max-width: 50vw; + max-width: 30vw; justify-content: center; height: 60vh; text-align: center; From 9e3f15c28616a7eadd11ebb89c30f504312d5710 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 17:58:12 -0800 Subject: [PATCH 09/13] Refactor [username] and extract ProfileHead --- components/ProfileHead/index.tsx | 59 ++++++++ pages/[username].tsx | 240 +++++++++++++++---------------- 2 files changed, 177 insertions(+), 122 deletions(-) create mode 100644 components/ProfileHead/index.tsx diff --git a/components/ProfileHead/index.tsx b/components/ProfileHead/index.tsx new file mode 100644 index 00000000..f196c09e --- /dev/null +++ b/components/ProfileHead/index.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import Head from 'next/head' +import { useTranslation } from 'next-i18next' + +interface Props { + user: User +} + +const ProfileHead = ({ user }: Props) => { + // Import translations + const { t } = useTranslation('common') + + return ( + + {/* HTML */} + {t('page.titles.profile', { username: user.username })} + + + + {/* OpenGraph */} + + + + + + {/* Twitter */} + + + + + + ) +} + +export default ProfileHead diff --git a/pages/[username].tsx b/pages/[username].tsx index fc75944a..0b13f655 100644 --- a/pages/[username].tsx +++ b/pages/[username].tsx @@ -1,42 +1,48 @@ import React, { useCallback, useEffect, useState } from 'react' -import Head from 'next/head' - +import InfiniteScroll from 'react-infinite-scroll-component' import { queryTypes, useQueryState } from 'next-usequerystate' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' -import InfiniteScroll from 'react-infinite-scroll-component' - import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import api from '~utils/api' - -import setUserToken from '~utils/setUserToken' import extractFilters from '~utils/extractFilters' import fetchLatestVersion from '~utils/fetchLatestVersion' import organizeRaids from '~utils/organizeRaids' +import setUserToken from '~utils/setUserToken' import useDidMountEffect from '~utils/useDidMountEffect' import { appState } from '~utils/appState' import { elements, allElement } from '~data/elements' import { emptyPaginationObject } from '~utils/emptyStates' -import { printError } from '~utils/reportError' import GridRep from '~components/GridRep' import GridRepCollection from '~components/GridRepCollection' +import ErrorSection from '~components/ErrorSection' import FilterBar from '~components/FilterBar' +import ProfileHead from '~components/ProfileHead' import type { NextApiRequest, NextApiResponse } from 'next' -import type { FilterObject, PaginationObject } from '~types' +import type { + FilterObject, + PageContextObj, + PaginationObject, + ResponseStatus, +} from '~types' +import { AxiosError } from 'axios' interface Props { - user?: User - teams?: Party[] - meta: PaginationObject - raids: Raid[] - sortedRaids: Raid[][] + context?: PageContextObj version: AppUpdate + error: boolean + status?: ResponseStatus } -const ProfileRoute: React.FC = (props: Props) => { +const ProfileRoute: React.FC = ({ + context, + version, + error, + status, +}: Props) => { // Set up router const router = useRouter() const { username } = router.query @@ -99,11 +105,11 @@ const ProfileRoute: React.FC = (props: Props) => { // Set the initial parties from props useEffect(() => { - if (props.teams) { - setTotalPages(props.meta.totalPages) - setRecordCount(props.meta.count) - replaceResults(props.meta.count, props.teams) - appState.version = props.version + if (context && context.teams && context.pagination) { + setTotalPages(context.pagination.totalPages) + setRecordCount(context.pagination.count) + replaceResults(context.pagination.count, context.teams) + appState.version = version } setCurrentPage(1) }, []) @@ -229,6 +235,16 @@ const ProfileRoute: React.FC = (props: Props) => { router.push(`/p/${shortcode}`) } + // Methods: Page component rendering + function pageHead() { + if (context && context.user) return + } + + function pageError() { + if (status) return + else return
+ } + // TODO: Add save functions function renderParties() { @@ -250,95 +266,54 @@ const ProfileRoute: React.FC = (props: Props) => { }) } - return ( -
- - {/* HTML */} - - {t('page.titles.profile', { username: props.user?.username })} - - - - - {/* OpenGraph */} - - - - - - {/* Twitter */} - - - - - - -
- {props.user?.avatar.picture} -

{props.user?.username}

-
-
- -
- 0 ? parties.length : 0} - next={() => setCurrentPage(currentPage + 1)} - hasMore={totalPages > currentPage} - loader={ -
-

Loading...

-
- } + if (context) { + return ( +
+ {pageHead()} + - {renderParties()} - - - {parties.length == 0 ? ( -
-

{t('teams.not_found')}

+
+ {context.user?.avatar.picture} +

{context.user?.username}

- ) : ( - '' - )} -
-
- ) + + +
+ 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={ +
+

Loading...

+
+ } + > + {renderParties()} +
+ + {parties.length == 0 ? ( +
+

{t('teams.not_found')}

+
+ ) : ( + '' + )} +
+
+ ) + } else return pageError() } export const getServerSidePaths = async () => { @@ -356,10 +331,10 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex // Set headers for server-side requests setUserToken(req, res) - try { - // Fetch latest version - const version = await fetchLatestVersion() + // Fetch latest version + const version = await fetchLatestVersion() + try { // Fetch and organize raids let { raids, sortedRaids } = await api.endpoints.raids .getAll() @@ -372,9 +347,9 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex } // Set up empty variables - let user: User | null = null - let teams: Party[] | null = null - let meta: PaginationObject = emptyPaginationObject + let user: User | undefined = undefined + let teams: Party[] | undefined = undefined + let pagination: PaginationObject = emptyPaginationObject // Perform a request only if we received a username if (query.username) { @@ -389,25 +364,46 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex if (response.data.profile.parties) teams = response.data.profile.parties else teams = [] - meta.count = response.data.meta.count - meta.totalPages = response.data.meta.total_pages - meta.perPage = response.data.meta.per_page + pagination.count = response.data.meta.count + pagination.totalPages = response.data.meta.total_pages + pagination.perPage = response.data.meta.per_page } + // Consolidate data into context object + const context: PageContextObj = { + user: user, + teams: teams, + raids: raids, + sortedRaids: sortedRaids, + pagination: pagination, + } + + // Pass to the page component as props return { props: { - user: user, - teams: teams, - meta: meta, - raids: raids, - sortedRaids: sortedRaids, + context: context, version: version, + error: false, ...(await serverSideTranslations(locale, ['common', 'roadmap'])), - // Will be passed to the page component as props }, } } catch (error) { - printError(error, 'axios') + // Extract the underlying Axios error + const axiosError = error as AxiosError + const response = axiosError.response + + // Pass to the page component as props + return { + props: { + context: null, + error: true, + status: { + code: response?.status, + text: response?.statusText, + }, + ...(await serverSideTranslations(locale, ['common', 'roadmap'])), + }, + } } } From 73de22cbbc84a2f746c16e310d50a5933cab6f31 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 18:00:34 -0800 Subject: [PATCH 10/13] Refactor teams and extract TeamsHead --- components/TeamsHead/index.tsx | 37 +++++++ pages/teams.tsx | 194 ++++++++++++++++++--------------- 2 files changed, 143 insertions(+), 88 deletions(-) create mode 100644 components/TeamsHead/index.tsx diff --git a/components/TeamsHead/index.tsx b/components/TeamsHead/index.tsx new file mode 100644 index 00000000..db2ac60d --- /dev/null +++ b/components/TeamsHead/index.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import Head from 'next/head' +import { useTranslation } from 'next-i18next' + +const TeamsHead = () => { + // Import translations + const { t } = useTranslation('common') + + return ( + + {/* HTML */} + {t('page.titles.discover')} + + + + {/* OpenGraph */} + + + + + + {/* Twitter */} + + + + + + ) +} + +export default TeamsHead diff --git a/pages/teams.tsx b/pages/teams.tsx index b680412a..199cb1a3 100644 --- a/pages/teams.tsx +++ b/pages/teams.tsx @@ -1,11 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react' -import Head from 'next/head' - +import InfiniteScroll from 'react-infinite-scroll-component' import { queryTypes, useQueryState } from 'next-usequerystate' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' -import InfiniteScroll from 'react-infinite-scroll-component' - import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import clonedeep from 'lodash.clonedeep' @@ -18,23 +15,35 @@ import useDidMountEffect from '~utils/useDidMountEffect' import { appState } from '~utils/appState' import { elements, allElement } from '~data/elements' import { emptyPaginationObject } from '~utils/emptyStates' -import { printError } from '~utils/reportError' +import ErrorSection from '~components/ErrorSection' import GridRep from '~components/GridRep' import GridRepCollection from '~components/GridRepCollection' import FilterBar from '~components/FilterBar' +import TeamsHead from '~components/TeamsHead' +import type { AxiosError } from 'axios' import type { NextApiRequest, NextApiResponse } from 'next' -import type { FilterObject, PaginationObject } from '~types' +import type { + FilterObject, + PageContextObj, + PaginationObject, + ResponseStatus, +} from '~types' interface Props { - teams?: Party[] - meta: PaginationObject - sortedRaids: Raid[][] + context?: PageContextObj version: AppUpdate + error: boolean + status?: ResponseStatus } -const TeamsRoute: React.FC = (props: Props) => { +const TeamsRoute: React.FC = ({ + context, + version, + error, + status, +}: Props) => { // Set up router const router = useRouter() @@ -96,11 +105,11 @@ const TeamsRoute: React.FC = (props: Props) => { // Set the initial parties from props useEffect(() => { - if (props.teams) { - setTotalPages(props.meta.totalPages) - setRecordCount(props.meta.count) - replaceResults(props.meta.count, props.teams) - appState.version = props.version + if (context && context.teams && context.pagination) { + setTotalPages(context.pagination.totalPages) + setRecordCount(context.pagination.count) + replaceResults(context.pagination.count, context.teams) + appState.version = version } setCurrentPage(1) }, []) @@ -268,6 +277,16 @@ const TeamsRoute: React.FC = (props: Props) => { router.push(`/p/${shortcode}`) } + // Methods: Page component rendering + function pageHead() { + if (context && context.user) return + } + + function pageError() { + if (status) return + else return
+ } + function renderParties() { return parties.map((party, i) => { return ( @@ -290,67 +309,45 @@ const TeamsRoute: React.FC = (props: Props) => { }) } - return ( -
- - {/* HTML */} - {t('page.titles.discover')} - - - - {/* OpenGraph */} - - - - - - {/* Twitter */} - - - - - - - -

{t('teams.title')}

-
- -
- 0 ? parties.length : 0} - next={() => setCurrentPage(currentPage + 1)} - hasMore={totalPages > currentPage} - loader={ -
-

Loading...

-
- } + if (context) { + return ( +
+ {pageHead()} + - {renderParties()} - +

{t('teams.title')}

+
- {parties.length == 0 ? ( -
-

{t('teams.not_found')}

-
- ) : ( - '' - )} -
-
- ) +
+ 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={ +
+

Loading...

+
+ } + > + {renderParties()} +
+ + {parties.length == 0 ? ( +
+

{t('teams.not_found')}

+
+ ) : ( + '' + )} +
+
+ ) + } else return pageError() } export const getServerSidePaths = async () => { @@ -368,10 +365,10 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex // Set headers for server-side requests setUserToken(req, res) + // Fetch latest version + const version = await fetchLatestVersion() + try { - // Fetch latest version - const version = await fetchLatestVersion() - // Fetch and organize raids let { raids, sortedRaids } = await api.endpoints.raids .getAll() @@ -384,31 +381,52 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex } // Set up empty variables - let teams: Party[] | null = null - let meta: PaginationObject = emptyPaginationObject + let teams: Party[] | undefined = undefined + let pagination: PaginationObject = emptyPaginationObject // Fetch initial set of parties const response = await api.endpoints.parties.getAll(params) // Assign values to pass to props teams = response.data.results - meta.count = response.data.meta.count - meta.totalPages = response.data.meta.total_pages - meta.perPage = response.data.meta.per_page + pagination.count = response.data.meta.count + pagination.totalPages = response.data.meta.total_pages + pagination.perPage = response.data.meta.per_page + // Consolidate data into context object + const context: PageContextObj = { + teams: teams, + raids: raids, + sortedRaids: sortedRaids, + pagination: pagination, + } + + // Pass to the page component as props return { props: { - teams: teams, - meta: meta, - raids: raids, - sortedRaids: sortedRaids, + context: context, version: version, + error: false, ...(await serverSideTranslations(locale, ['common', 'roadmap'])), - // Will be passed to the page component as props }, } } catch (error) { - printError(error, 'axios') + // Extract the underlying Axios error + const axiosError = error as AxiosError + const response = axiosError.response + + // Pass to the page component as props + return { + props: { + context: null, + error: true, + status: { + code: response?.status, + text: response?.statusText, + }, + ...(await serverSideTranslations(locale, ['common', 'roadmap'])), + }, + } } } From 01af39fdfea6460a8a5d5a3993ca8e1931eeb3b0 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 18:07:26 -0800 Subject: [PATCH 11/13] Refactor saved and extract SavedHead --- components/SavedHead/index.tsx | 25 +++++ pages/saved.tsx | 184 +++++++++++++++++++-------------- 2 files changed, 131 insertions(+), 78 deletions(-) create mode 100644 components/SavedHead/index.tsx diff --git a/components/SavedHead/index.tsx b/components/SavedHead/index.tsx new file mode 100644 index 00000000..01505e1a --- /dev/null +++ b/components/SavedHead/index.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import Head from 'next/head' +import { useTranslation } from 'next-i18next' + +const SavedHead = () => { + // Import translations + const { t } = useTranslation('common') + + return ( + + {t('page.titles.saved')} + + + + + + + + + + + ) +} + +export default SavedHead diff --git a/pages/saved.tsx b/pages/saved.tsx index 09b2ec93..0328fb19 100644 --- a/pages/saved.tsx +++ b/pages/saved.tsx @@ -1,11 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react' -import Head from 'next/head' - +import InfiniteScroll from 'react-infinite-scroll-component' import { queryTypes, useQueryState } from 'next-usequerystate' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' -import InfiniteScroll from 'react-infinite-scroll-component' - import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import clonedeep from 'lodash.clonedeep' @@ -18,24 +15,35 @@ import useDidMountEffect from '~utils/useDidMountEffect' import { appState } from '~utils/appState' import { elements, allElement } from '~data/elements' import { emptyPaginationObject } from '~utils/emptyStates' -import { printError } from '~utils/reportError' +import ErrorSection from '~components/ErrorSection' import GridRep from '~components/GridRep' import GridRepCollection from '~components/GridRepCollection' import FilterBar from '~components/FilterBar' +import SavedHead from '~components/SavedHead' +import type { AxiosError } from 'axios' import type { NextApiRequest, NextApiResponse } from 'next' -import type { FilterObject, PaginationObject } from '~types' +import type { + FilterObject, + PageContextObj, + PaginationObject, + ResponseStatus, +} from '~types' interface Props { - teams?: Party[] - meta: PaginationObject - raids: Raid[] - sortedRaids: Raid[][] + context?: PageContextObj version: AppUpdate + error: boolean + status?: ResponseStatus } -const SavedRoute: React.FC = (props: Props) => { +const SavedRoute: React.FC = ({ + context, + version, + error, + status, +}: Props) => { // Set up router const router = useRouter() @@ -97,11 +105,11 @@ const SavedRoute: React.FC = (props: Props) => { // Set the initial parties from props useEffect(() => { - if (props.teams) { - setTotalPages(props.meta.totalPages) - setRecordCount(props.meta.count) - replaceResults(props.meta.count, props.teams) - appState.version = props.version + if (context && context.teams && context.pagination) { + setTotalPages(context.pagination.totalPages) + setRecordCount(context.pagination.count) + replaceResults(context.pagination.count, context.teams) + appState.version = version } setCurrentPage(1) }, []) @@ -269,6 +277,16 @@ const SavedRoute: React.FC = (props: Props) => { router.push(`/p/${shortcode}`) } + // Methods: Page component rendering + function pageHead() { + if (context && context.user) return + } + + function pageError() { + if (status) return + else return
+ } + function renderParties() { return parties.map((party, i) => { return ( @@ -291,55 +309,45 @@ const SavedRoute: React.FC = (props: Props) => { }) } - return ( -
- - {t('page.titles.saved')} - - - - - - - - - - - - -

{t('saved.title')}

-
- -
- 0 ? parties.length : 0} - next={() => setCurrentPage(currentPage + 1)} - hasMore={totalPages > currentPage} - loader={ -
-

Loading...

-
- } + if (context) { + return ( +
+ {pageHead()} + - {renderParties()} - +

{t('saved.title')}

+
- {parties.length == 0 ? ( -
-

{t('saved.not_found')}

-
- ) : ( - '' - )} -
-
- ) +
+ 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={ +
+

Loading...

+
+ } + > + {renderParties()} +
+ + {parties.length == 0 ? ( +
+

{t('saved.not_found')}

+
+ ) : ( + '' + )} +
+
+ ) + } else return pageError() } export const getServerSidePaths = async () => { @@ -357,10 +365,10 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex // Set headers for server-side requests setUserToken(req, res) - try { - // Fetch latest version - const version = await fetchLatestVersion() + // Fetch latest version + const version = await fetchLatestVersion() + try { // Fetch and organize raids let { raids, sortedRaids } = await api.endpoints.raids .getAll() @@ -373,32 +381,52 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex } // Set up empty variables - let teams: Party[] | null = null - let meta: PaginationObject = emptyPaginationObject + let teams: Party[] | undefined = undefined + let pagination: PaginationObject = emptyPaginationObject // Fetch initial set of saved parties const response = await api.savedTeams(params) // Assign values to pass to props teams = response.data.results - meta.count = response.data.meta.count - meta.totalPages = response.data.meta.total_pages - meta.perPage = response.data.meta.per_page + pagination.count = response.data.meta.count + pagination.totalPages = response.data.meta.total_pages + pagination.perPage = response.data.meta.per_page + // Consolidate data into context object + const context: PageContextObj = { + teams: teams, + raids: raids, + sortedRaids: sortedRaids, + pagination: pagination, + } + + // Pass to the page component as props return { props: { - teams: teams, - meta: meta, - raids: raids, - sortedRaids: sortedRaids, + context: context, version: version, + error: false, ...(await serverSideTranslations(locale, ['common', 'roadmap'])), - // Will be passed to the page component as props }, } } catch (error) { - printError(error, 'axios') + // Extract the underlying Axios error + const axiosError = error as AxiosError + const response = axiosError.response + + // Pass to the page component as props + return { + props: { + context: null, + error: true, + status: { + code: response?.status, + text: response?.statusText, + }, + ...(await serverSideTranslations(locale, ['common', 'roadmap'])), + }, + } } } - export default SavedRoute From c930bc348b7bfbe10636beb85305b4274536565c Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 18:15:25 -0800 Subject: [PATCH 12/13] Refactor new and extract NewHead --- components/NewHead/index.tsx | 31 +++++++++ pages/new/index.tsx | 131 ++++++++++++++++++++--------------- pages/p/[party].tsx | 3 +- 3 files changed, 107 insertions(+), 58 deletions(-) create mode 100644 components/NewHead/index.tsx diff --git a/components/NewHead/index.tsx b/components/NewHead/index.tsx new file mode 100644 index 00000000..1864b7c4 --- /dev/null +++ b/components/NewHead/index.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import Head from 'next/head' +import { useTranslation } from 'next-i18next' + +const NewHead = () => { + // Import translations + const { t } = useTranslation('common') + + return ( + + {/* HTML */} + {t('page.titles.new')} + + + + {/* OpenGraph */} + + + + + + {/* Twitter */} + + + + + + ) +} + +export default NewHead diff --git a/pages/new/index.tsx b/pages/new/index.tsx index b3c2c225..86907577 100644 --- a/pages/new/index.tsx +++ b/pages/new/index.tsx @@ -1,11 +1,11 @@ import React, { useEffect } from 'react' import { useRouter } from 'next/router' -import Head from 'next/head' -import { useTranslation } from 'next-i18next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import clonedeep from 'lodash.clonedeep' +import ErrorSection from '~components/ErrorSection' import Party from '~components/Party' +import NewHead from '~components/NewHead' import api from '~utils/api' import fetchLatestVersion from '~utils/fetchLatestVersion' @@ -13,24 +13,24 @@ import organizeRaids from '~utils/organizeRaids' import setUserToken from '~utils/setUserToken' import { appState, initialAppState } from '~utils/appState' import { groupWeaponKeys } from '~utils/groupWeaponKeys' -import { printError } from '~utils/reportError' +import type { AxiosError } from 'axios' import type { NextApiRequest, NextApiResponse } from 'next' -import type { GroupedWeaponKeys } from '~utils/groupWeaponKeys' +import type { PageContextObj, ResponseStatus } from '~types' interface Props { - jobs: Job[] - jobSkills: JobSkill[] - raids: Raid[] - sortedRaids: Raid[][] - weaponKeys: GroupedWeaponKeys + context?: PageContextObj version: AppUpdate + error: boolean + status?: ResponseStatus } -const NewRoute: React.FC = (props: Props) => { - // Import translations - const { t } = useTranslation('common') - +const NewRoute: React.FC = ({ + context, + version, + error, + status, +}: Props) => { // Set up router const router = useRouter() @@ -40,8 +40,14 @@ const NewRoute: React.FC = (props: Props) => { } useEffect(() => { - persistStaticData() - }, [persistStaticData]) + if (context && context.jobs && context.jobSkills) { + appState.raids = context.raids + appState.jobs = context.jobs + appState.jobSkills = context.jobSkills + appState.weaponKeys = context.weaponKeys + } + appState.version = version + }, []) useEffect(() => { // Clean state @@ -53,37 +59,24 @@ const NewRoute: React.FC = (props: Props) => { appState.party.editable = true }, []) - function persistStaticData() { - appState.raids = props.raids - appState.jobs = props.jobs - appState.jobSkills = props.jobSkills - appState.weaponKeys = props.weaponKeys - appState.version = props.version + // Methods: Page component rendering + function pageHead() { + if (context && context.user) return } - return ( - - - {/* HTML */} - {t('page.titles.new')} - - + function pageError() { + if (status) return + else return
+ } - {/* OpenGraph */} - - - - - - {/* Twitter */} - - - - - - - - ) + if (context) { + return ( + + {pageHead()} + + + ) + } else return pageError() } export const getServerSidePaths = async () => { @@ -101,39 +94,63 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex // Set headers for server-side requests setUserToken(req, res) - try { - // Fetch latest version - const version = await fetchLatestVersion() + // Fetch latest version + const version = await fetchLatestVersion() + try { // Fetch and organize raids let { raids, sortedRaids } = await api.endpoints.raids .getAll() .then((response) => organizeRaids(response.data)) - let jobs = await api.endpoints.jobs.getAll().then((response) => { - return response.data - }) + // Fetch jobs and job skills + let jobs = await api.endpoints.jobs + .getAll() + .then((response) => response.data) - let jobSkills = await api.allJobSkills().then((response) => response.data) + let jobSkills = await api.allJobSkills() + .then((response) => response.data) + // Fetch and organize weapon keys let weaponKeys = await api.endpoints.weapon_keys .getAll() .then((response) => groupWeaponKeys(response.data)) + // Consolidate data into context object + const context: PageContextObj = { + jobs: jobs, + jobSkills: jobSkills, + raids: raids, + sortedRaids: sortedRaids, + weaponKeys: weaponKeys, + } + + // Pass to the page component as props return { props: { - jobs: jobs, - jobSkills: jobSkills, - raids: raids, - sortedRaids: sortedRaids, - weaponKeys: weaponKeys, + context: context, version: version, + error: false, ...(await serverSideTranslations(locale, ['common', 'roadmap'])), - // Will be passed to the page component as props }, } } catch (error) { - printError(error, 'axios') + // Extract the underlying Axios error + const axiosError = error as AxiosError + const response = axiosError.response + + // Pass to the page component as props + return { + props: { + context: null, + error: true, + status: { + code: response?.status, + text: response?.statusText, + }, + ...(await serverSideTranslations(locale, ['common', 'roadmap'])), + }, + } } } diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index 0722556f..3a85a1fc 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -124,7 +124,8 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex .getAll() .then((response) => response.data) - let jobSkills = await api.allJobSkills().then((response) => response.data) + let jobSkills = await api.allJobSkills() + .then((response) => response.data) // Fetch and organize weapon keys let weaponKeys = await api.endpoints.weapon_keys From 308e5d07e81ccec6c0cac68eed02139c924b9293 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 28 Jan 2023 18:15:30 -0800 Subject: [PATCH 13/13] Add localizations --- public/locales/en/common.json | 15 ++++++++++++--- public/locales/ja/common.json | 9 +++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 0f7ee421..47376222 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -54,9 +54,6 @@ }, "remove": "Remove from grid" }, - "errors": { - "unauthorized": "You don't have permission to perform that action" - }, "filters": { "labels": { "element": "Element", @@ -94,6 +91,18 @@ "light": "Light" } }, + "errors": { + "internal_server_error": { + "title": "Internal Server Error", + "description": "The server reported a problem that we couldn't automatically recover from. Please try your request again." + }, + "not_found": { + "title": "Not found", + "description": "The page you're looking for couldn't be found", + "button": "Create a new party" + }, + "unauthorized": "You don't have permission to perform that action" + }, "proficiencies": { "sabre": "Sabre", "dagger": "Dagger", diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index 4bcfc3cf..015295ac 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -63,6 +63,15 @@ } }, "errors": { + "internal_server_error": { + "title": "ใ‚ตใƒผใƒใƒผใ‚จใƒฉใƒผ", + "description": "ใ‚ตใƒผใƒใƒผใ‹ใ‚‰ๅฑŠใ„ใŸใ‚จใƒฉใƒผใฏ่‡ชๅ‹•็š„ใซๅพฉใ•ใ‚Œใชใ‹ใฃใŸใŸใ‚ใ€ๅ†ใณใƒชใ‚ฏใ‚จใ‚นใƒˆใ‚’่กŒใชใฃใฆใใ ใ•ใ„" + }, + "not_found": { + "title": "่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใงใ—ใŸ", + "description": "ๆŽขใ—ใฆใ„ใ‚‹ใƒšใƒผใ‚ธใฏ่ฆ‹ใคใ‹ใ‚Šใพใ›ใ‚“ใงใ—ใŸ", + "button": "ๆ–ฐใ—ใ„็ทจๆˆใ‚’ไฝœๆˆ" + }, "unauthorized": "่กŒใฃใŸใ‚ขใ‚ฏใ‚ทใƒงใƒณใ‚’ๅฎŸ่กŒใ™ใ‚‹ๆจฉ้™ใŒใ‚ใ‚Šใพใ›ใ‚“" }, "header": {