hensei-web/pages/saved.tsx
Justin Edmund 702566e2ed
(Hotfix) (Temporary) nuclear option for raid population (#339)
* Another attempt to fix RaidCombobox loading

* Final nuclear option of getting raids to populate

No matter what I do, raids won't populate from state specifically in production. I will have to investigate this more, but for now we are going with the nuclear option of passing raids down from the context object we get from SSR through all components into RaidCombobox
2023-07-04 02:20:48 -07:00

432 lines
12 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { queryTypes, useQueryState } from 'next-usequerystate'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep'
import api from '~utils/api'
import { setHeaders } from '~utils/userToken'
import extractFilters from '~utils/extractFilters'
import fetchLatestVersion from '~utils/fetchLatestVersion'
import useDidMountEffect from '~utils/useDidMountEffect'
import { appState } from '~utils/appState'
import { permissiveFilterset } from '~utils/defaultFilters'
import { elements, allElement } from '~data/elements'
import { emptyPaginationObject } from '~utils/emptyStates'
import ErrorSection from '~components/ErrorSection'
import GridRep from '~components/GridRep'
import GridRepCollection from '~components/GridRepCollection'
import FilterBar from '~components/filters/FilterBar'
import SavedHead from '~components/head/SavedHead'
import type { AxiosError } from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
import type {
FilterObject,
PageContextObj,
PaginationObject,
ResponseStatus,
} from '~types'
interface Props {
context?: PageContextObj
version: AppUpdate
error: boolean
status?: ResponseStatus
}
const SavedRoute: React.FC<Props> = ({
context,
version,
error,
status,
}: Props) => {
// Set up router
const router = useRouter()
// Import translations
const { t } = useTranslation('common')
// Set up app-specific states
const [mounted, setMounted] = useState(false)
const [scrolled, setScrolled] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>()
// Set up infinite scrolling-related states
const [recordCount, setRecordCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
// Set up filter-specific query states
// Recency is in seconds
const [element, setElement] = useQueryState('element', {
defaultValue: -1,
history: 'push',
parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value),
})
const [raid, setRaid] = useQueryState('raid', {
defaultValue: 'all',
history: 'push',
parse: (query: string) => {
const raids = context?.raidGroups.flatMap((group) => group.raids)
const raid = raids?.find((r: Raid) => r.slug === query)
return raid ? raid.id : 'all'
},
serialize: (value) => value,
})
const [recency, setRecency] = useQueryState('recency', {
defaultValue: -1,
history: 'push',
parse: (query: string) => parseInt(query),
serialize: (value) => `${value}`,
})
const [advancedFilters, setAdvancedFilters] =
useState<FilterSet>(permissiveFilterset)
// Define transformers for element
function parseElement(query: string) {
let element: TeamElement | undefined =
query === 'all'
? allElement
: elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
}
function serializeElement(value: number | undefined) {
let name = ''
if (value != undefined) {
if (value == -1) name = allElement.name.en.toLowerCase()
else name = elements[value].name.en.toLowerCase()
}
return name
}
// Set the initial parties from props
useEffect(() => {
if (context && context.teams && context.pagination) {
setTotalPages(context.pagination.totalPages)
setRecordCount(context.pagination.count)
replaceResults(context.pagination.count, context.teams)
appState.raidGroups = context.raidGroups
appState.version = version
}
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Handle errors
const handleError = useCallback((error: any) => {
if (error.response != null) {
console.error(error)
} else {
console.error('There was an error.')
}
}, [])
const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => {
const filters: {
[key: string]: any
} = {
element: element !== -1 ? element : undefined,
raid: raid === 'all' ? undefined : raid,
recency: recency !== -1 ? recency : undefined,
page: currentPage,
...advancedFilters,
}
Object.keys(filters).forEach(
(key) => filters[key] === undefined && delete filters[key]
)
const params = {
params: {
...filters,
},
}
api
.savedTeams(params)
.then((response) => {
const results = response.data.results
const meta = response.data.meta
setTotalPages(meta.total_pages)
setRecordCount(meta.count)
if (replace) replaceResults(meta.count, results)
else appendResults(results)
})
.catch((error) => handleError(error))
},
[currentPage, parties, element, raid, recency, advancedFilters]
)
function replaceResults(count: number, list: Party[]) {
if (count > 0) {
setParties(list)
} else {
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list])
}
// Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => {
const raids = appState.raidGroups.flatMap((group) => group.raids)
setRaids(raids)
}, [setRaids])
// When the element, raid or recency filter changes,
// fetch all teams again.
useDidMountEffect(() => {
setCurrentPage(1)
if (mounted) {
fetchTeams({ replace: true })
}
setMounted(true)
}, [element, raid, recency, advancedFilters])
// When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed
if (currentPage > 1) fetchTeams({ replace: false })
else if (currentPage == 1) fetchTeams({ replace: true })
setMounted(true)
}, [currentPage])
// Receive filters from the filter bar
function receiveFilters(filters: FilterSet) {
if (filters.element == 0) setElement(0, { shallow: true })
else if (filters.element) setElement(filters.element, { shallow: true })
if (filters.recency) setRecency(filters.recency, { shallow: true })
if (filters.raid) setRaid(filters.raid, { shallow: true })
}
function receiveAdvancedFilters(filters: FilterSet) {
setAdvancedFilters(filters)
}
// Methods: Favorites
function toggleFavorite(teamId: string, favorited: boolean) {
if (favorited) unsaveFavorite(teamId)
else saveFavorite(teamId)
}
function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId }).then((response) => {
if (response.status == 201) {
const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index]
party.favorited = true
let clonedParties = clonedeep(parties)
clonedParties[index] = party
setParties(clonedParties)
}
})
}
function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId }).then((response) => {
if (response.status == 200) {
const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index]
party.favorited = false
let clonedParties = clonedeep(parties)
clonedParties.splice(index, 1)
setParties(clonedParties)
}
})
}
// Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) {
router.push(`/p/${shortcode}`)
}
// Methods: Page component rendering
function pageHead() {
return <SavedHead />
}
function pageError() {
if (status) return <ErrorSection status={status} />
else return <div />
}
function renderParties() {
return parties.map((party, i) => {
return (
<GridRep
id={party.id}
shortcode={party.shortcode}
name={party.name}
createdAt={new Date(party.created_at)}
raid={party.raid}
grid={party.weapons}
user={party.user}
favorited={party.favorited}
fullAuto={party.full_auto}
autoGuard={party.auto_guard}
key={`party-${i}`}
onClick={goTo}
onSave={toggleFavorite}
/>
)
})
}
if (context) {
return (
<div className="teams">
{pageHead()}
<FilterBar
defaultFilterset={permissiveFilterset}
onFilter={receiveFilters}
onAdvancedFilter={receiveAdvancedFilters}
persistFilters={false}
scrolled={scrolled}
element={element}
raid={raid}
raidGroups={context.raidGroups}
recency={recency}
>
<h1>{t('saved.title')}</h1>
</FilterBar>
<section>
<InfiniteScroll
dataLength={parties && parties.length > 0 ? parties.length : 0}
next={() => setCurrentPage(currentPage + 1)}
hasMore={totalPages > currentPage}
loader={
<div className="notFound">
<h2>{t('loading')}</h2>
</div>
}
>
<GridRepCollection>{renderParties()}</GridRepCollection>
</InfiniteScroll>
{parties.length == 0 ? (
<div className="notFound">
<h2>{t('saved.not_found')}</h2>
</div>
) : (
''
)}
</section>
</div>
)
} else return pageError()
}
export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: 'string' } },
],
fallback: true,
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// Set headers for server-side requests
setHeaders(req, res)
// Fetch latest version
const version = await fetchLatestVersion()
try {
// Fetch and organize raids
let raidGroups: RaidGroup[] = await api
.raidGroups()
.then((response) => response.data)
// Create filter object
const filters: FilterObject = extractFilters(query, raidGroups)
const params = {
params: { ...filters, ...permissiveFilterset },
}
// Set up empty variables
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
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,
raidGroups: raidGroups,
pagination: pagination,
}
// Pass to the page component as props
return {
props: {
context: context,
version: version,
error: false,
...(await serverSideTranslations(locale, ['common'])),
},
}
} catch (error) {
// 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'])),
},
}
}
}
export default SavedRoute