Break collection pages into hooks (#414)

This refactors the collection pages (teams, saved and profiles) into a
bunch of hooks that handle various chunks of functionality. This way,
the actual "pages" have significantly less logic and significantly less
repeated code.

* **useFavorites** handles favoriting teams
* **useFilterState** handles the URL query filters
* **usePaginationState** simply holds data pertaining to pagination
* **useFetchTeams** handles fetching and parsing team data from the
server
* **useTeamFilter** pulls all other states together and handles some
logic that is closest to the page

Co-authored-by: Justin Edmund <383021+jedmund@users.noreply.github.com>
This commit is contained in:
Justin Edmund 2024-04-21 00:46:04 -07:00 committed by GitHub
parent 4dc2279d68
commit fc616aab01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 812 additions and 769 deletions

View file

@ -1,4 +1,5 @@
{ {
"git.ignoreLimitWarning": true, "git.ignoreLimitWarning": true,
"i18n-ally.localesPaths": ["public/locales"] "i18n-ally.localesPaths": ["public/locales"],
"i18n-ally.keystyle": "nested"
} }

View file

@ -19,7 +19,6 @@ interface Props {
defaultFilterset: FilterSet defaultFilterset: FilterSet
persistFilters?: boolean persistFilters?: boolean
children: React.ReactNode children: React.ReactNode
scrolled: boolean
element?: number element?: number
raid?: string raid?: string
raidGroups: RaidGroup[] raidGroups: RaidGroup[]
@ -32,6 +31,8 @@ const FilterBar = (props: Props) => {
// Set up translation // Set up translation
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [scrolled, setScrolled] = useState(false)
const [currentRaid, setCurrentRaid] = useState<Raid>() const [currentRaid, setCurrentRaid] = useState<Raid>()
const [recencyOpen, setRecencyOpen] = useState(false) const [recencyOpen, setRecencyOpen] = useState(false)
@ -44,7 +45,7 @@ const FilterBar = (props: Props) => {
// Set up classes object for showing shadow on scroll // Set up classes object for showing shadow on scroll
const classes = classNames({ const classes = classNames({
[styles.filterBar]: true, [styles.filterBar]: true,
[styles.shadow]: props.scrolled, [styles.shadow]: scrolled,
}) })
const filterButtonClasses = classNames({ const filterButtonClasses = classNames({
@ -52,6 +53,17 @@ const FilterBar = (props: Props) => {
filtersActive: !matchesDefaultFilters, filtersActive: !matchesDefaultFilters,
}) })
// Add scroll event listener for shadow on FilterBar on mount
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
function handleScroll() {
if (window.scrollY > 90) setScrolled(true)
else setScrolled(false)
}
// Convert raid slug to Raid object on mount // Convert raid slug to Raid object on mount
useEffect(() => { useEffect(() => {
const raid = appState.raidGroups const raid = appState.raidGroups

View file

@ -27,7 +27,7 @@ import type { SearchableObject, SearchableObjectArray } from '~types'
import styles from './index.module.scss' import styles from './index.module.scss'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import classNames from 'classnames' import classNames from 'classnames'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~hooks/useDidMountEffect'
interface Props extends DialogProps { interface Props extends DialogProps {
send: (object: SearchableObject, position: number) => any send: (object: SearchableObject, position: number) => any

50
hooks/useFavorites.tsx Normal file
View file

@ -0,0 +1,50 @@
import clonedeep from 'lodash.clonedeep'
import api from '~utils/api'
import { PageContextObj } from '~types'
export const useFavorites = (
parties: Party[],
setParties: (value: Party[]) => void
) => {
// 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[index] = party
setParties(clonedParties)
}
})
}
return {
toggleFavorite,
}
}

146
hooks/useFetchTeams.tsx Normal file
View file

@ -0,0 +1,146 @@
import { useCallback, useState } from 'react'
import api from '~utils/api'
export const useFetchTeams = (
currentPage: number,
filters: { [key: string]: any },
parties: Party[],
setParties: (value: Party[]) => void,
setTotalPages: (value: number) => void,
setRecordCount: (value: number) => void
) => {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(false)
function parseTeams(response: { [key: string]: any }, replace: boolean) {
const { parties, meta } = response
setTotalPages(meta.total_pages)
setRecordCount(meta.count)
if (replace) {
replaceResults(parties)
setIsLoading(false)
} else appendResults(parties)
}
function parseError(error: any) {
setIsLoading(false)
setError(true)
}
function processTeams(list: Party[], shouldReplace: boolean) {
if (shouldReplace) {
replaceResults(list)
} else {
appendResults(list)
}
}
function replaceResults(list: Party[]) {
if (list.length > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else {
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list])
}
function createParams() {
return {
params: Object.entries(filters).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value
}
return acc
}, {} as { [key: string]: any }),
}
}
const fetchTeams = useCallback(
({ replace } = { replace: false }) => {
if (replace) setIsLoading(true)
const params = createParams()
api.endpoints.parties
.getAll(params)
.then((response) => {
const formedResponse = {
parties: response.data.results,
meta: response.data.meta,
}
return parseTeams(formedResponse, replace)
})
.catch(parseError)
},
[filters, currentPage]
)
const fetchProfile = useCallback(
({
username,
replace,
}: {
username: string | undefined
replace: boolean
}) => {
if (replace) setIsLoading(true)
const params = createParams()
if (username && !Array.isArray(username)) {
api.endpoints.users
.getOne({
id: username,
params: params,
})
.then((response) => {
const formedResponse = {
parties: response.data.profile.parties,
meta: response.data.meta,
}
return parseTeams(formedResponse, replace)
})
.catch(parseError)
}
},
[currentPage, filters]
)
const fetchSaved = useCallback(
({ replace } = { replace: false }) => {
if (replace) setIsLoading(true)
const params = createParams()
api
.savedTeams(params)
.then((response) => {
const formedResponse = {
parties: response.data.results,
meta: response.data.meta,
}
return parseTeams(formedResponse, replace)
})
.catch(parseError)
},
[filters, currentPage]
)
return {
fetchTeams,
fetchProfile,
fetchSaved,
processTeams,
isLoading,
setIsLoading,
error,
}
}

56
hooks/useFilterState.tsx Normal file
View file

@ -0,0 +1,56 @@
import { useState, useEffect } from 'react'
import { getCookie } from 'cookies-next'
import { useQueryState } from 'nuqs'
import { defaultFilterset } from '~utils/defaultFilters'
import { parseElement, serializeElement } from '~utils/parseElement'
import type { PageContextObj } from '~types'
export const useFilterState = (context?: PageContextObj) => {
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(defaultFilterset)
useEffect(() => {
const filtersCookie = getCookie('filters')
const filters = filtersCookie
? JSON.parse(filtersCookie as string)
: defaultFilterset
setAdvancedFilters(filters)
}, [])
return {
element,
setElement,
raid,
setRaid,
recency,
setRecency,
advancedFilters,
setAdvancedFilters,
}
}

View file

@ -0,0 +1,16 @@
import { useState } from 'react'
export const usePaginationState = () => {
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [recordCount, setRecordCount] = useState(0)
return {
currentPage,
setCurrentPage,
totalPages,
setTotalPages,
recordCount,
setRecordCount,
}
}

182
hooks/useTeamFilter.tsx Normal file
View file

@ -0,0 +1,182 @@
// Libraries
import { useState, useCallback, useEffect } from 'react'
import { getCookie } from 'cookies-next'
// Hooks
import { useFetchTeams } from './useFetchTeams'
import { useFilterState } from './useFilterState'
import { usePaginationState } from './usePaginationState'
import useDidMountEffect from './useDidMountEffect'
// Utils
import { CollectionPage } from '~utils/enums'
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
import { defaultFilterset } from '~utils/defaultFilters'
// Types
import type { PageContextObj, PaginationObject } from '~types'
export const useTeamFilter = (
page: CollectionPage,
context?: PageContextObj
) => {
const [mounted, setMounted] = useState(false)
const [parties, setParties] = useState<Party[]>([])
function constructFilters({
element,
raid,
recency,
currentPage,
filterSet,
}: {
element: number
raid: string
recency: number
currentPage: number
filterSet: FilterSet
}) {
const filters: { [key: string]: any } = {
element: element !== -1 ? element : undefined,
raid: raid === 'all' ? undefined : raid,
recency: recency !== -1 ? recency : undefined,
page: currentPage,
...convertAdvancedFilters(filterSet),
}
Object.keys(filters).forEach(
(key) => filters[key] === undefined && delete filters[key]
)
return filters
}
const {
element,
setElement,
raid,
setRaid,
recency,
setRecency,
advancedFilters,
setAdvancedFilters,
} = useFilterState(context)
const {
currentPage,
setCurrentPage,
totalPages,
setTotalPages,
recordCount,
setRecordCount,
} = usePaginationState()
const {
fetchTeams,
fetchProfile,
fetchSaved,
processTeams,
isLoading: isFetching,
setIsLoading: setFetching,
error: fetchError,
} = useFetchTeams(
currentPage,
constructFilters({
element,
raid,
recency,
currentPage,
filterSet: advancedFilters,
}),
parties,
setParties,
setTotalPages,
setRecordCount
)
// Update the advancedFilters state based on cookies when the component mounts
useEffect(() => {
const filtersCookie = getCookie('filters')
const filters = filtersCookie
? JSON.parse(filtersCookie as string)
: defaultFilterset
setAdvancedFilters(filters)
}, [])
// Handle pagination object updates from fetchTeams
const setPagination = useCallback((value: PaginationObject) => {
setTotalPages(value.totalPages)
setRecordCount(value.count)
}, [])
useDidMountEffect(() => {}, [currentPage])
useEffect(() => {
if (context && context.teams && context.pagination) {
setTotalPages(context.pagination.totalPages)
setRecordCount(context.pagination.count)
// processTeams(context.teams, context.pagination.count, true)
}
setCurrentPage(1)
setFetching(false)
}, [])
// When the element, raid or recency filter changes,
// fetch all teams again.
useDidMountEffect(() => {
setCurrentPage(1)
if (mounted) fetch(true)
setMounted(true)
}, [element, raid, recency, advancedFilters])
// When the page changes, fetch all teams again.
useDidMountEffect(() => {
if (currentPage > 1) fetch(false)
else if (currentPage == 1 && mounted) fetch(true)
setMounted(true)
}, [currentPage])
function fetch(replace: boolean) {
switch (page) {
case CollectionPage.Teams:
return fetchTeams({ replace: replace })
case CollectionPage.Profile:
return fetchProfile({
username: context?.user?.username,
replace: replace,
})
case CollectionPage.Saved:
return fetchSaved({
replace: replace,
})
}
}
return {
// Expose the states and setters for the component to use
element,
setElement,
raid,
setRaid,
recency,
setRecency,
advancedFilters,
setAdvancedFilters,
parties,
setParties,
isFetching,
setFetching,
fetchError,
fetchTeams,
processTeams,
currentPage,
setCurrentPage,
totalPages,
recordCount,
setPagination,
}
}

View file

@ -1,37 +1,42 @@
import React, { useCallback, useEffect, useState } from 'react' // Libraries
import InfiniteScroll from 'react-infinite-scroll-component' import React, { useEffect, useState } from 'react'
import { useQueryState } from 'nuqs'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import InfiniteScroll from 'react-infinite-scroll-component'
import api from '~utils/api' // Hooks
import extractFilters from '~utils/extractFilters' import { useFavorites } from '~hooks/useFavorites'
import { useTeamFilter } from '~hooks/useTeamFilter'
import useDidMountEffect from '~hooks/useDidMountEffect'
// Utils
import fetchLatestVersion from '~utils/fetchLatestVersion' import fetchLatestVersion from '~utils/fetchLatestVersion'
import { setHeaders } from '~utils/userToken'
import useDidMountEffect from '~utils/useDidMountEffect'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
import { CollectionPage } from '~utils/enums'
import { permissiveFilterset } from '~utils/defaultFilters' import { permissiveFilterset } from '~utils/defaultFilters'
import { elements, allElement } from '~data/elements' import { setHeaders } from '~utils/userToken'
import { emptyPaginationObject } from '~utils/emptyStates' import {
fetchRaidGroupsAndFilters,
fetchUserProfile,
parseAdvancedFilters,
} from '~utils/serverSideUtils'
// Types
import type { AxiosError } from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { PageContextObj, ResponseStatus } from '~types'
// Components
import ErrorSection from '~components/ErrorSection'
import FilterBar from '~components/filters/FilterBar'
import GridRep from '~components/reps/GridRep' import GridRep from '~components/reps/GridRep'
import GridRepCollection from '~components/reps/GridRepCollection' import GridRepCollection from '~components/reps/GridRepCollection'
import LoadingRep from '~components/reps/LoadingRep' import LoadingRep from '~components/reps/LoadingRep'
import ErrorSection from '~components/ErrorSection'
import FilterBar from '~components/filters/FilterBar'
import ProfileHead from '~components/head/ProfileHead' import ProfileHead from '~components/head/ProfileHead'
import UserInfo from '~components/filters/UserInfo' import UserInfo from '~components/filters/UserInfo'
import type { AxiosError } from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
import type {
FilterObject,
PageContextObj,
PaginationObject,
ResponseStatus,
} from '~types'
interface Props { interface Props {
context?: PageContextObj context?: PageContextObj
version: AppUpdate version: AppUpdate
@ -52,144 +57,48 @@ const ProfileRoute: React.FC<Props> = ({
// Import translations // Import translations
const { t } = useTranslation('common') const { t } = useTranslation('common')
// Set up app-specific states
const [mounted, setMounted] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [isLoading, setIsLoading] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>()
// Set up infinite scrolling-related states const {
const [recordCount, setRecordCount] = useState(0) element,
const [currentPage, setCurrentPage] = useState(1) setElement,
const [totalPages, setTotalPages] = useState(1) raid,
setRaid,
recency,
setRecency,
advancedFilters,
setAdvancedFilters,
currentPage,
setCurrentPage,
totalPages,
recordCount,
parties,
setParties,
isFetching,
setFetching,
fetchError,
fetchTeams,
processTeams,
setPagination,
} = useTeamFilter(CollectionPage.Profile, context)
// Set up filter-specific query states const { toggleFavorite } = useFavorites(parties, setParties)
// 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 // Set the initial parties from props
useEffect(() => { useDidMountEffect(() => {
if (context && context.teams && context.pagination) { if (context) {
setTotalPages(context.pagination.totalPages) if (context.teams && context.pagination) {
setRecordCount(context.pagination.count) processTeams(context.teams, true)
replaceResults(context.pagination.count, context.teams) setPagination(context.pagination)
appState.raidGroups = context.raidGroups
appState.version = version appState.raidGroups = context.raidGroups
appState.version = version
}
} }
setCurrentPage(1) setCurrentPage(1)
}, []) setFetching(false)
}, [context])
// 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 {
// TODO: Put an alert here
console.error('There was an error.')
}
}, [])
const fetchProfile = useCallback(
({ replace }: { replace: boolean }) => {
if (replace) setIsLoading(true)
const filters = {
params: {
element: element != -1 ? element : undefined,
raid: raid === 'all' ? undefined : raid,
recency: recency != -1 ? recency : undefined,
page: currentPage,
...advancedFilters,
},
}
if (username && !Array.isArray(username)) {
api.endpoints.users
.getOne({
id: username,
params: { ...filters },
})
.then((response) => {
const results = response.data.profile.parties
const meta = response.data.meta
setTotalPages(meta.total_pages)
setRecordCount(meta.count)
if (replace) {
setIsLoading(false)
replaceResults(meta.count, results)
} else appendResults(results)
})
.catch((error) => handleError(error))
}
},
[currentPage, username, parties, element, raid, recency, advancedFilters]
)
function replaceResults(count: number, list: Party[]) {
if (count > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else {
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list])
}
// Fetch all raids on mount, then find the raid in the URL if present // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
@ -197,26 +106,6 @@ const ProfileRoute: React.FC<Props> = ({
setRaids(raids) setRaids(raids)
}, [setRaids]) }, [setRaids])
// When the element, raid or recency filter changes,
// fetch all teams again.
useDidMountEffect(() => {
setCurrentPage(1)
if (mounted) {
fetchProfile({ replace: true })
}
setMounted(true)
}, [username, element, raid, recency, advancedFilters])
// When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed
if (currentPage > 1) fetchProfile({ replace: false })
else if (currentPage == 1) fetchProfile({ replace: true })
setMounted(true)
}, [currentPage])
// Receive filters from the filter bar // Receive filters from the filter bar
function receiveFilters(filters: FilterSet) { function receiveFilters(filters: FilterSet) {
if (filters.element == 0) setElement(0, { shallow: true }) if (filters.element == 0) setElement(0, { shallow: true })
@ -230,10 +119,6 @@ const ProfileRoute: React.FC<Props> = ({
} }
// Methods: Navigation // Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) { function goTo(shortcode: string) {
router.push(`/p/${shortcode}`) router.push(`/p/${shortcode}`)
@ -249,7 +134,7 @@ const ProfileRoute: React.FC<Props> = ({
else return <div /> else return <div />
} }
// TODO: Add save functions // Page component rendering methods
function renderParties() { function renderParties() {
return parties.map((party, i) => { return parties.map((party, i) => {
@ -257,12 +142,14 @@ const ProfileRoute: React.FC<Props> = ({
<GridRep <GridRep
party={party} party={party}
key={`party-${i}`} key={`party-${i}`}
loading={isLoading} loading={isFetching}
onClick={goTo} onClick={goTo}
onSave={(teamId, favorited) => toggleFavorite(teamId, favorited)}
/> />
) )
}) })
} }
function renderLoading(number: number) { function renderLoading(number: number) {
return ( return (
<GridRepCollection> <GridRepCollection>
@ -293,7 +180,6 @@ const ProfileRoute: React.FC<Props> = ({
onAdvancedFilter={receiveAdvancedFilters} onAdvancedFilter={receiveAdvancedFilters}
onFilter={receiveFilters} onFilter={receiveFilters}
persistFilters={false} persistFilters={false}
scrolled={scrolled}
element={element} element={element}
raid={raid} raid={raid}
raidGroups={context.raidGroups} raidGroups={context.raidGroups}
@ -337,46 +223,24 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
const version = await fetchLatestVersion() const version = await fetchLatestVersion()
try { try {
// Fetch and organize raids // We don't pre-load advanced filters here
let raidGroups: RaidGroup[] = await api const { raidGroups, filters } = await fetchRaidGroupsAndFilters(query)
.raidGroups()
.then((response) => response.data)
// Create filter object let context: PageContextObj | undefined = undefined
const filters: FilterObject = extractFilters(query, raidGroups)
const params = {
params: { ...filters, ...permissiveFilterset },
}
// Set up empty variables
let user: User | undefined = undefined
let teams: Party[] | undefined = undefined
let pagination: PaginationObject = emptyPaginationObject
// Perform a request only if we received a username // Perform a request only if we received a username
if (query.username) { if (query.username) {
const response = await api.endpoints.users.getOne({ const { user, teams, pagination } = await fetchUserProfile(
id: query.username, query.username,
params, filters
}) )
// Assign values to pass to props context = {
user = response.data.profile user: user,
teams: teams,
if (response.data.profile.parties) teams = response.data.profile.parties raidGroups: raidGroups,
else teams = [] pagination: pagination,
}
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,
raidGroups: raidGroups,
pagination: pagination,
} }
// Pass to the page component as props // Pass to the page component as props

View file

@ -1,37 +1,41 @@
import React, { useCallback, useEffect, useState } from 'react' // Libraries
import InfiniteScroll from 'react-infinite-scroll-component' import React, { useEffect, useState } from 'react'
import { useQueryState } from 'nuqs'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep' import InfiniteScroll from 'react-infinite-scroll-component'
import api from '~utils/api' // Hooks
import { setHeaders } from '~utils/userToken' import { useFavorites } from '~hooks/useFavorites'
import extractFilters from '~utils/extractFilters' import { useTeamFilter } from '~hooks/useTeamFilter'
import useDidMountEffect from '~hooks/useDidMountEffect'
// Utils
import fetchLatestVersion from '~utils/fetchLatestVersion' import fetchLatestVersion from '~utils/fetchLatestVersion'
import useDidMountEffect from '~utils/useDidMountEffect'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
import { CollectionPage } from '~utils/enums'
import { permissiveFilterset } from '~utils/defaultFilters' import { permissiveFilterset } from '~utils/defaultFilters'
import { elements, allElement } from '~data/elements' import { setHeaders } from '~utils/userToken'
import { emptyPaginationObject } from '~utils/emptyStates' import {
fetchRaidGroupsAndFilters,
fetchSaved,
parseAdvancedFilters,
} from '~utils/serverSideUtils'
// Types
import type { AxiosError } from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { PageContextObj, ResponseStatus } from '~types'
// Components
import ErrorSection from '~components/ErrorSection' import ErrorSection from '~components/ErrorSection'
import FilterBar from '~components/filters/FilterBar'
import GridRep from '~components/reps/GridRep' import GridRep from '~components/reps/GridRep'
import GridRepCollection from '~components/reps/GridRepCollection' import GridRepCollection from '~components/reps/GridRepCollection'
import LoadingRep from '~components/reps/LoadingRep' import LoadingRep from '~components/reps/LoadingRep'
import FilterBar from '~components/filters/FilterBar'
import SavedHead from '~components/head/SavedHead' 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 { interface Props {
context?: PageContextObj context?: PageContextObj
version: AppUpdate version: AppUpdate
@ -51,148 +55,48 @@ const SavedRoute: React.FC<Props> = ({
// Import translations // Import translations
const { t } = useTranslation('common') const { t } = useTranslation('common')
// Set up app-specific states
const [mounted, setMounted] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [isLoading, setIsLoading] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>()
// Set up infinite scrolling-related states const {
const [recordCount, setRecordCount] = useState(0) element,
const [currentPage, setCurrentPage] = useState(1) setElement,
const [totalPages, setTotalPages] = useState(1) raid,
setRaid,
recency,
setRecency,
advancedFilters,
setAdvancedFilters,
currentPage,
setCurrentPage,
totalPages,
recordCount,
parties,
setParties,
isFetching,
setFetching,
fetchError,
fetchTeams,
processTeams,
setPagination,
} = useTeamFilter(CollectionPage.Saved, context)
// Set up filter-specific query states const { toggleFavorite } = useFavorites(parties, setParties)
// 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 // Set the initial parties from props
useEffect(() => { useDidMountEffect(() => {
if (context && context.teams && context.pagination) { if (context) {
setTotalPages(context.pagination.totalPages) if (context.teams && context.pagination) {
setRecordCount(context.pagination.count) processTeams(context.teams, true)
replaceResults(context.pagination.count, context.teams) setPagination(context.pagination)
appState.raidGroups = context.raidGroups
appState.version = version appState.raidGroups = context.raidGroups
appState.version = version
}
} }
setCurrentPage(1) setCurrentPage(1)
}, []) setFetching(false)
}, [context])
// 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 }) => {
if (replace) setIsLoading(true)
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) {
setIsLoading(false)
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 // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
@ -200,26 +104,6 @@ const SavedRoute: React.FC<Props> = ({
setRaids(raids) setRaids(raids)
}, [setRaids]) }, [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 // Receive filters from the filter bar
function receiveFilters(filters: FilterSet) { function receiveFilters(filters: FilterSet) {
if (filters.element == 0) setElement(0, { shallow: true }) if (filters.element == 0) setElement(0, { shallow: true })
@ -232,49 +116,7 @@ const SavedRoute: React.FC<Props> = ({
setAdvancedFilters(filters) 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 // Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) { function goTo(shortcode: string) {
router.push(`/p/${shortcode}`) router.push(`/p/${shortcode}`)
@ -290,13 +132,15 @@ const SavedRoute: React.FC<Props> = ({
else return <div /> else return <div />
} }
// Page component rendering methods
function renderParties() { function renderParties() {
return parties.map((party, i) => { return parties.map((party, i) => {
return ( return (
<GridRep <GridRep
party={party} party={party}
key={`party-${i}`} key={`party-${i}`}
loading={isLoading} loading={isFetching}
onClick={goTo} onClick={goTo}
onSave={toggleFavorite} onSave={toggleFavorite}
/> />
@ -334,7 +178,6 @@ const SavedRoute: React.FC<Props> = ({
onFilter={receiveFilters} onFilter={receiveFilters}
onAdvancedFilter={receiveAdvancedFilters} onAdvancedFilter={receiveAdvancedFilters}
persistFilters={false} persistFilters={false}
scrolled={scrolled}
element={element} element={element}
raid={raid} raid={raid}
raidGroups={context.raidGroups} raidGroups={context.raidGroups}
@ -378,42 +221,14 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
const version = await fetchLatestVersion() const version = await fetchLatestVersion()
try { try {
// Fetch and organize raids // We don't pre-load advanced filters here
let raidGroups: RaidGroup[] = await api const { raidGroups, filters } = await fetchRaidGroupsAndFilters(query)
.raidGroups() const { teams, pagination } = await fetchSaved(filters)
.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 { return {
props: { props: {
context: context, context: { teams, raidGroups, pagination },
version: version, version,
error: false, error: false,
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common'])),
}, },

View file

@ -1,38 +1,40 @@
import React, { useCallback, useEffect, useState } from 'react' // Libraries
import InfiniteScroll from 'react-infinite-scroll-component' import React, { useEffect, useState } from 'react'
import { getCookie } from 'cookies-next'
import { useQueryState } from 'nuqs'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep' import axios, { AxiosResponse } from 'axios'
import InfiniteScroll from 'react-infinite-scroll-component'
import api from '~utils/api' // Hooks
import { setHeaders } from '~utils/userToken' import { useFavorites } from '~hooks/useFavorites'
import extractFilters from '~utils/extractFilters' import { useTeamFilter } from '~hooks/useTeamFilter'
import useDidMountEffect from '~hooks/useDidMountEffect'
// Utils
import fetchLatestVersion from '~utils/fetchLatestVersion' import fetchLatestVersion from '~utils/fetchLatestVersion'
import useDidMountEffect from '~utils/useDidMountEffect'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { defaultFilterset } from '~utils/defaultFilters'
import { elements, allElement } from '~data/elements'
import { emptyPaginationObject } from '~utils/emptyStates'
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters' import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
import { defaultFilterset } from '~utils/defaultFilters'
import { setHeaders } from '~utils/userToken'
import {
fetchParties,
fetchRaidGroupsAndFilters,
parseAdvancedFilters,
} from '~utils/serverSideUtils'
// Types
import type { NextApiRequest, NextApiResponse } from 'next'
import type { PageContextObj, ResponseStatus } from '~types'
// Components
import ErrorSection from '~components/ErrorSection' import ErrorSection from '~components/ErrorSection'
import FilterBar from '~components/filters/FilterBar'
import GridRep from '~components/reps/GridRep' import GridRep from '~components/reps/GridRep'
import GridRepCollection from '~components/reps/GridRepCollection' import GridRepCollection from '~components/reps/GridRepCollection'
import LoadingRep from '~components/reps/LoadingRep' import LoadingRep from '~components/reps/LoadingRep'
import FilterBar from '~components/filters/FilterBar'
import TeamsHead from '~components/head/TeamsHead' import TeamsHead from '~components/head/TeamsHead'
import { CollectionPage } from '~utils/enums'
import type { AxiosError } from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
import type {
FilterObject,
PageContextObj,
PaginationObject,
ResponseStatus,
} from '~types'
interface Props { interface Props {
context?: PageContextObj context?: PageContextObj
@ -47,166 +49,51 @@ const TeamsRoute: React.FC<Props> = ({
error, error,
status, status,
}: Props) => { }: Props) => {
// Set up router
const router = useRouter() const router = useRouter()
// Import translations
const { t } = useTranslation('common') const { t } = useTranslation('common')
// Set up app-specific states
const [mounted, setMounted] = useState(false)
const [scrolled, setScrolled] = useState(false)
const [isLoading, setIsLoading] = useState(false)
// Set up page-specific states
const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>()
// Set up infinite scrolling-related states const {
const [recordCount, setRecordCount] = useState(0) element,
const [currentPage, setCurrentPage] = useState(1) setElement,
const [totalPages, setTotalPages] = useState(1) raid,
setRaid,
recency,
setRecency,
advancedFilters,
setAdvancedFilters,
currentPage,
setCurrentPage,
totalPages,
recordCount,
parties,
setParties,
isFetching,
setFetching,
fetchError,
fetchTeams,
processTeams,
setPagination,
} = useTeamFilter(CollectionPage.Teams, context)
// Set up filter-specific query states const { toggleFavorite } = useFavorites(parties, setParties)
// 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>(defaultFilterset)
// 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 // Set the initial parties from props
useEffect(() => { useDidMountEffect(() => {
if (context && context.teams && context.pagination) { if (context) {
setTotalPages(context.pagination.totalPages) if (context.teams && context.pagination) {
setRecordCount(context.pagination.count) processTeams(context.teams, true)
replaceResults(context.pagination.count, context.teams) setPagination(context.pagination)
appState.raidGroups = context.raidGroups
appState.version = version appState.raidGroups = context.raidGroups
appState.version = version
}
} }
setCurrentPage(1) setCurrentPage(1)
setFetching(false)
setIsLoading(false) }, [context])
}, [])
// Add scroll event listener for shadow on FilterBar on mount
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Fetch the user's advanced filters
useEffect(() => {
const filtersCookie = getCookie('filters')
const filters = filtersCookie
? JSON.parse(filtersCookie as string)
: defaultFilterset
setAdvancedFilters(filters)
}, [])
// 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 }) => {
if (replace) setIsLoading(true)
const filters: {
[key: string]: any
} = {
element: element !== -1 ? element : undefined,
raid: raid === 'all' ? undefined : raid,
recency: recency !== -1 ? recency : undefined,
page: currentPage,
...convertAdvancedFilters(advancedFilters),
}
Object.keys(filters).forEach(
(key) => filters[key] === undefined && delete filters[key]
)
const params = {
params: {
...filters,
},
}
api.endpoints.parties
.getAll(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)
setIsLoading(false)
} else appendResults(results)
})
.catch((error) => handleError(error))
},
[currentPage, parties, element, raid, recency, advancedFilters]
)
function replaceResults(count: number, list: Party[]) {
if (count > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else {
setParties([])
}
}
function appendResults(list: Party[]) {
setParties([...parties, ...list])
}
// Fetch all raids on mount, then find the raid in the URL if present // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
@ -214,26 +101,6 @@ const TeamsRoute: React.FC<Props> = ({
setRaids(raids) setRaids(raids)
}, [setRaids]) }, [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 && mounted) fetchTeams({ replace: true })
setMounted(true)
}, [currentPage])
// Receive filters from the filter bar // Receive filters from the filter bar
function receiveFilters(filters: FilterSet) { function receiveFilters(filters: FilterSet) {
if (filters.element == 0) setElement(0, { shallow: true }) if (filters.element == 0) setElement(0, { shallow: true })
@ -246,49 +113,7 @@ const TeamsRoute: React.FC<Props> = ({
setAdvancedFilters(filters) 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[index] = party
setParties(clonedParties)
}
})
}
// Methods: Navigation // Methods: Navigation
function handleScroll() {
if (window.pageYOffset > 90) setScrolled(true)
else setScrolled(false)
}
function goTo(shortcode: string) { function goTo(shortcode: string) {
router.push(`/p/${shortcode}`) router.push(`/p/${shortcode}`)
@ -304,24 +129,23 @@ const TeamsRoute: React.FC<Props> = ({
else return <div /> else return <div />
} }
// Page component rendering methods
function renderParties() { function renderParties() {
return parties.map((party, i) => { return parties.map((party, i) => (
return ( <GridRep
<GridRep party={party}
party={party} key={`party-${i}`}
key={`party-${i}`} loading={isFetching}
loading={isLoading} onClick={() => goTo(party.shortcode)}
onClick={goTo} onSave={(teamId, favorited) => toggleFavorite(teamId, favorited)}
onSave={toggleFavorite} />
/> ))
)
})
} }
function renderLoading(number: number) { function renderLoading(number: number) {
return ( return (
<GridRepCollection> <GridRepCollection>
{Array.from(Array(number)).map((x, i) => ( {Array.from({ length: number }, (_, i) => (
<LoadingRep key={`loading-${i}`} /> <LoadingRep key={`loading-${i}`} />
))} ))}
</GridRepCollection> </GridRepCollection>
@ -348,7 +172,6 @@ const TeamsRoute: React.FC<Props> = ({
onFilter={receiveFilters} onFilter={receiveFilters}
onAdvancedFilter={receiveAdvancedFilters} onAdvancedFilter={receiveAdvancedFilters}
persistFilters={true} persistFilters={true}
scrolled={scrolled}
element={element} element={element}
raid={raid} raid={raid}
raidGroups={context.raidGroups} raidGroups={context.raidGroups}
@ -381,65 +204,35 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
// Fetch latest version // Fetch latest version
const version = await fetchLatestVersion() const version = await fetchLatestVersion()
// Fetch user's advanced filters
const filtersCookie = getCookie('filters', { req: req, res: res })
const advancedFilters = filtersCookie ? JSON.parse(filtersCookie as string) : undefined
const convertedFilters = advancedFilters ? convertAdvancedFilters(advancedFilters) : undefined
try { try {
// Fetch and organize raids const advancedFilters = parseAdvancedFilters(req, res)
let raidGroups: RaidGroup[] = await api const convertedFilters = advancedFilters
.raidGroups() ? convertAdvancedFilters(advancedFilters)
.then((response) => response.data) : undefined
const { raidGroups, filters } = await fetchRaidGroupsAndFilters(query)
const { teams, pagination } = await fetchParties(filters, convertedFilters)
// Create filter object
const filters: FilterObject = extractFilters(query, raidGroups)
const params = {
params: { ...filters, ...convertedFilters },
}
// Set up empty variables
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
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 { return {
props: { props: {
context: context, context: { teams, raidGroups, pagination },
version: version, version,
error: false, error: false,
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common'])),
}, },
} }
} catch (error) { } catch (error) {
// Extract the underlying Axios error // If error is of type AxiosError, extract the response into a variable
const axiosError = error as AxiosError let response: AxiosResponse<any, any> | undefined = axios.isAxiosError(error)
const response = axiosError.response ? error.response
: undefined
// Pass to the page component as props
return { return {
props: { props: {
context: null, context: null,
error: true, error: true,
status: { status: {
code: response ? response.status : -999, code: response?.status ?? -999,
text: response ? response.statusText : "unspecified_error", text: response?.statusText ?? 'unspecified_error',
}, },
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common'])),
}, },

View file

@ -16,3 +16,8 @@ interface FilterSet {
includes?: MentionItem[] includes?: MentionItem[]
excludes?: MentionItem[] excludes?: MentionItem[]
} }
interface ConvertedFilters extends Omit<FilterSet, 'includes' | 'excludes'> {
includes: string
excludes: string
}

View file

@ -1,21 +1,21 @@
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
export function convertAdvancedFilters(filters: FilterSet) { export function convertAdvancedFilters(filters: FilterSet): ConvertedFilters {
let copy = cloneDeep(filters) let copy: FilterSet = cloneDeep(filters)
const includes = filterString(filters.includes || []) const includes: string = filterString(filters.includes || [])
const excludes = filterString(filters.excludes || []) const excludes: string = filterString(filters.excludes || [])
delete copy.includes delete (copy as any).includes
delete copy.excludes delete (copy as any).excludes
return { return {
...copy, ...copy,
includes, includes,
excludes, excludes,
} } as ConvertedFilters
} }
export function filterString(list: MentionItem[]) { export function filterString(list: MentionItem[]): string {
return list.map((item) => item.granblue_id).join(',') return list.map((item) => item.granblue_id).join(',')
} }

View file

@ -24,3 +24,9 @@ export enum AboutTabs {
Updates, Updates,
Roadmap, Roadmap,
} }
export enum CollectionPage {
Teams,
Profile,
Saved,
}

20
utils/parseElement.tsx Normal file
View file

@ -0,0 +1,20 @@
import { elements, allElement } from '~data/elements'
export function parseElement(query: string) {
let element: TeamElement | undefined =
query === 'all'
? allElement
: elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
}
export 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
}

77
utils/serverSideUtils.tsx Normal file
View file

@ -0,0 +1,77 @@
import { getCookie } from 'cookies-next'
import { NextApiRequest, NextApiResponse } from 'next'
import api from './api'
import extractFilters from './extractFilters'
import { FilterObject } from '~types'
import { permissiveFilterset } from './defaultFilters'
// Parse advanced filters from cookies
export function parseAdvancedFilters(
req: NextApiRequest,
res: NextApiResponse
) {
const filtersCookie = getCookie('filters', { req, res })
return filtersCookie ? JSON.parse(filtersCookie as string) : undefined
}
// Fetch raid groups and create filter object
export async function fetchRaidGroupsAndFilters(query: {
[index: string]: string
}) {
const raidGroups = await api.raidGroups().then((response) => response.data)
const filters = extractFilters(query, raidGroups)
return { raidGroups, filters }
}
// Fetch initial set of parties
export async function fetchParties(
filters: FilterObject,
convertedFilters: ConvertedFilters | undefined
) {
const params = { params: { ...filters, ...convertedFilters } }
const response = await api.endpoints.parties.getAll(params)
return {
teams: response.data.results,
pagination: {
count: response.data.meta.count,
totalPages: response.data.meta.total_pages,
perPage: response.data.meta.per_page,
},
}
}
export async function fetchUserProfile(
username: string,
filters: FilterObject
) {
const params = { params: { ...filters, ...permissiveFilterset } }
const response = await api.endpoints.users.getOne({
id: username,
params,
})
return {
user: response.data.profile,
teams: response.data.profile.parties,
pagination: {
count: response.data.meta.count,
totalPages: response.data.meta.total_pages,
perPage: response.data.meta.per_page,
},
}
}
export async function fetchSaved(filters: FilterObject) {
const params = { params: { ...filters, ...permissiveFilterset } }
const response = await api.savedTeams(params)
return {
teams: response.data.results,
pagination: {
count: response.data.meta.count,
totalPages: response.data.meta.total_pages,
perPage: response.data.meta.per_page,
},
}
}