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:
parent
4dc2279d68
commit
fc616aab01
17 changed files with 812 additions and 769 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"git.ignoreLimitWarning": true,
|
||||
"i18n-ally.localesPaths": ["public/locales"]
|
||||
"i18n-ally.localesPaths": ["public/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ interface Props {
|
|||
defaultFilterset: FilterSet
|
||||
persistFilters?: boolean
|
||||
children: React.ReactNode
|
||||
scrolled: boolean
|
||||
element?: number
|
||||
raid?: string
|
||||
raidGroups: RaidGroup[]
|
||||
|
|
@ -32,6 +31,8 @@ const FilterBar = (props: Props) => {
|
|||
// Set up translation
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
const [currentRaid, setCurrentRaid] = useState<Raid>()
|
||||
|
||||
const [recencyOpen, setRecencyOpen] = useState(false)
|
||||
|
|
@ -44,7 +45,7 @@ const FilterBar = (props: Props) => {
|
|||
// Set up classes object for showing shadow on scroll
|
||||
const classes = classNames({
|
||||
[styles.filterBar]: true,
|
||||
[styles.shadow]: props.scrolled,
|
||||
[styles.shadow]: scrolled,
|
||||
})
|
||||
|
||||
const filterButtonClasses = classNames({
|
||||
|
|
@ -52,6 +53,17 @@ const FilterBar = (props: Props) => {
|
|||
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
|
||||
useEffect(() => {
|
||||
const raid = appState.raidGroups
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import type { SearchableObject, SearchableObjectArray } from '~types'
|
|||
import styles from './index.module.scss'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import classNames from 'classnames'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import useDidMountEffect from '~hooks/useDidMountEffect'
|
||||
|
||||
interface Props extends DialogProps {
|
||||
send: (object: SearchableObject, position: number) => any
|
||||
|
|
|
|||
50
hooks/useFavorites.tsx
Normal file
50
hooks/useFavorites.tsx
Normal 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
146
hooks/useFetchTeams.tsx
Normal 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
56
hooks/useFilterState.tsx
Normal 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,
|
||||
}
|
||||
}
|
||||
16
hooks/usePaginationState.tsx
Normal file
16
hooks/usePaginationState.tsx
Normal 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
182
hooks/useTeamFilter.tsx
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,42 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { useQueryState } from 'nuqs'
|
||||
// Libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
|
||||
import api from '~utils/api'
|
||||
import extractFilters from '~utils/extractFilters'
|
||||
// Hooks
|
||||
import { useFavorites } from '~hooks/useFavorites'
|
||||
import { useTeamFilter } from '~hooks/useTeamFilter'
|
||||
import useDidMountEffect from '~hooks/useDidMountEffect'
|
||||
|
||||
// Utils
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import { setHeaders } from '~utils/userToken'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
|
||||
import { CollectionPage } from '~utils/enums'
|
||||
import { permissiveFilterset } from '~utils/defaultFilters'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
import { setHeaders } from '~utils/userToken'
|
||||
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 GridRepCollection from '~components/reps/GridRepCollection'
|
||||
import LoadingRep from '~components/reps/LoadingRep'
|
||||
import ErrorSection from '~components/ErrorSection'
|
||||
import FilterBar from '~components/filters/FilterBar'
|
||||
import ProfileHead from '~components/head/ProfileHead'
|
||||
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 {
|
||||
context?: PageContextObj
|
||||
version: AppUpdate
|
||||
|
|
@ -52,144 +57,48 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
// Import translations
|
||||
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[]>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const {
|
||||
element,
|
||||
setElement,
|
||||
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
|
||||
// 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
|
||||
}
|
||||
const { toggleFavorite } = useFavorites(parties, setParties)
|
||||
|
||||
// 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
|
||||
useDidMountEffect(() => {
|
||||
if (context) {
|
||||
if (context.teams && context.pagination) {
|
||||
processTeams(context.teams, true)
|
||||
setPagination(context.pagination)
|
||||
|
||||
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 {
|
||||
// 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])
|
||||
}
|
||||
setFetching(false)
|
||||
}, [context])
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
|
|
@ -197,26 +106,6 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
setRaids(raids)
|
||||
}, [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
|
||||
function receiveFilters(filters: FilterSet) {
|
||||
if (filters.element == 0) setElement(0, { shallow: true })
|
||||
|
|
@ -230,10 +119,6 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90) setScrolled(true)
|
||||
else setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
|
|
@ -249,7 +134,7 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
else return <div />
|
||||
}
|
||||
|
||||
// TODO: Add save functions
|
||||
// Page component rendering methods
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
|
|
@ -257,12 +142,14 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
<GridRep
|
||||
party={party}
|
||||
key={`party-${i}`}
|
||||
loading={isLoading}
|
||||
loading={isFetching}
|
||||
onClick={goTo}
|
||||
onSave={(teamId, favorited) => toggleFavorite(teamId, favorited)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function renderLoading(number: number) {
|
||||
return (
|
||||
<GridRepCollection>
|
||||
|
|
@ -293,7 +180,6 @@ const ProfileRoute: React.FC<Props> = ({
|
|||
onAdvancedFilter={receiveAdvancedFilters}
|
||||
onFilter={receiveFilters}
|
||||
persistFilters={false}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raid={raid}
|
||||
raidGroups={context.raidGroups}
|
||||
|
|
@ -337,46 +223,24 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
const version = await fetchLatestVersion()
|
||||
|
||||
try {
|
||||
// Fetch and organize raids
|
||||
let raidGroups: RaidGroup[] = await api
|
||||
.raidGroups()
|
||||
.then((response) => response.data)
|
||||
// We don't pre-load advanced filters here
|
||||
const { raidGroups, filters } = await fetchRaidGroupsAndFilters(query)
|
||||
|
||||
// Create filter object
|
||||
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
|
||||
let context: PageContextObj | undefined = undefined
|
||||
|
||||
// Perform a request only if we received a username
|
||||
if (query.username) {
|
||||
const response = await api.endpoints.users.getOne({
|
||||
id: query.username,
|
||||
params,
|
||||
})
|
||||
const { user, teams, pagination } = await fetchUserProfile(
|
||||
query.username,
|
||||
filters
|
||||
)
|
||||
|
||||
// Assign values to pass to props
|
||||
user = response.data.profile
|
||||
|
||||
if (response.data.profile.parties) teams = response.data.profile.parties
|
||||
else teams = []
|
||||
|
||||
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,
|
||||
context = {
|
||||
user: user,
|
||||
teams: teams,
|
||||
raidGroups: raidGroups,
|
||||
pagination: pagination,
|
||||
}
|
||||
}
|
||||
|
||||
// Pass to the page component as props
|
||||
|
|
|
|||
319
pages/saved.tsx
319
pages/saved.tsx
|
|
@ -1,37 +1,41 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { useQueryState } from 'nuqs'
|
||||
// Libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { setHeaders } from '~utils/userToken'
|
||||
import extractFilters from '~utils/extractFilters'
|
||||
// Hooks
|
||||
import { useFavorites } from '~hooks/useFavorites'
|
||||
import { useTeamFilter } from '~hooks/useTeamFilter'
|
||||
import useDidMountEffect from '~hooks/useDidMountEffect'
|
||||
|
||||
// Utils
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { convertAdvancedFilters } from '~utils/convertAdvancedFilters'
|
||||
import { CollectionPage } from '~utils/enums'
|
||||
import { permissiveFilterset } from '~utils/defaultFilters'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
import { setHeaders } from '~utils/userToken'
|
||||
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 FilterBar from '~components/filters/FilterBar'
|
||||
import GridRep from '~components/reps/GridRep'
|
||||
import GridRepCollection from '~components/reps/GridRepCollection'
|
||||
import LoadingRep from '~components/reps/LoadingRep'
|
||||
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
|
||||
|
|
@ -51,148 +55,48 @@ const SavedRoute: React.FC<Props> = ({
|
|||
// Import translations
|
||||
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[]>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const {
|
||||
element,
|
||||
setElement,
|
||||
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
|
||||
// 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
|
||||
}
|
||||
const { toggleFavorite } = useFavorites(parties, setParties)
|
||||
|
||||
// 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
|
||||
useDidMountEffect(() => {
|
||||
if (context) {
|
||||
if (context.teams && context.pagination) {
|
||||
processTeams(context.teams, true)
|
||||
setPagination(context.pagination)
|
||||
|
||||
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 }) => {
|
||||
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])
|
||||
}
|
||||
setFetching(false)
|
||||
}, [context])
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
|
|
@ -200,26 +104,6 @@ const SavedRoute: React.FC<Props> = ({
|
|||
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 })
|
||||
|
|
@ -232,49 +116,7 @@ const SavedRoute: React.FC<Props> = ({
|
|||
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}`)
|
||||
|
|
@ -290,13 +132,15 @@ const SavedRoute: React.FC<Props> = ({
|
|||
else return <div />
|
||||
}
|
||||
|
||||
// Page component rendering methods
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return (
|
||||
<GridRep
|
||||
party={party}
|
||||
key={`party-${i}`}
|
||||
loading={isLoading}
|
||||
loading={isFetching}
|
||||
onClick={goTo}
|
||||
onSave={toggleFavorite}
|
||||
/>
|
||||
|
|
@ -334,7 +178,6 @@ const SavedRoute: React.FC<Props> = ({
|
|||
onFilter={receiveFilters}
|
||||
onAdvancedFilter={receiveAdvancedFilters}
|
||||
persistFilters={false}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raid={raid}
|
||||
raidGroups={context.raidGroups}
|
||||
|
|
@ -378,42 +221,14 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
const version = await fetchLatestVersion()
|
||||
|
||||
try {
|
||||
// Fetch and organize raids
|
||||
let raidGroups: RaidGroup[] = await api
|
||||
.raidGroups()
|
||||
.then((response) => response.data)
|
||||
// We don't pre-load advanced filters here
|
||||
const { raidGroups, filters } = await fetchRaidGroupsAndFilters(query)
|
||||
const { teams, pagination } = await fetchSaved(filters)
|
||||
|
||||
// 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,
|
||||
context: { teams, raidGroups, pagination },
|
||||
version,
|
||||
error: false,
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
},
|
||||
|
|
|
|||
377
pages/teams.tsx
377
pages/teams.tsx
|
|
@ -1,38 +1,40 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useQueryState } from 'nuqs'
|
||||
// Libraries
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
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'
|
||||
import { setHeaders } from '~utils/userToken'
|
||||
import extractFilters from '~utils/extractFilters'
|
||||
// Hooks
|
||||
import { useFavorites } from '~hooks/useFavorites'
|
||||
import { useTeamFilter } from '~hooks/useTeamFilter'
|
||||
import useDidMountEffect from '~hooks/useDidMountEffect'
|
||||
|
||||
// Utils
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
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 { 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 FilterBar from '~components/filters/FilterBar'
|
||||
import GridRep from '~components/reps/GridRep'
|
||||
import GridRepCollection from '~components/reps/GridRepCollection'
|
||||
import LoadingRep from '~components/reps/LoadingRep'
|
||||
import FilterBar from '~components/filters/FilterBar'
|
||||
import TeamsHead from '~components/head/TeamsHead'
|
||||
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type {
|
||||
FilterObject,
|
||||
PageContextObj,
|
||||
PaginationObject,
|
||||
ResponseStatus,
|
||||
} from '~types'
|
||||
import { CollectionPage } from '~utils/enums'
|
||||
|
||||
interface Props {
|
||||
context?: PageContextObj
|
||||
|
|
@ -47,166 +49,51 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
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)
|
||||
const [isLoading, setIsLoading] = 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)
|
||||
const {
|
||||
element,
|
||||
setElement,
|
||||
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
|
||||
// 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
|
||||
}
|
||||
const { toggleFavorite } = useFavorites(parties, setParties)
|
||||
|
||||
// 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
|
||||
useDidMountEffect(() => {
|
||||
if (context) {
|
||||
if (context.teams && context.pagination) {
|
||||
processTeams(context.teams, true)
|
||||
setPagination(context.pagination)
|
||||
|
||||
appState.raidGroups = context.raidGroups
|
||||
appState.version = version
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentPage(1)
|
||||
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
// 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])
|
||||
}
|
||||
setFetching(false)
|
||||
}, [context])
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
|
|
@ -214,26 +101,6 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
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 && mounted) fetchTeams({ replace: true })
|
||||
setMounted(true)
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters(filters: FilterSet) {
|
||||
if (filters.element == 0) setElement(0, { shallow: true })
|
||||
|
|
@ -246,49 +113,7 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
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
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90) setScrolled(true)
|
||||
else setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
|
|
@ -304,24 +129,23 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
else return <div />
|
||||
}
|
||||
|
||||
// Page component rendering methods
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return (
|
||||
<GridRep
|
||||
party={party}
|
||||
key={`party-${i}`}
|
||||
loading={isLoading}
|
||||
onClick={goTo}
|
||||
onSave={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return parties.map((party, i) => (
|
||||
<GridRep
|
||||
party={party}
|
||||
key={`party-${i}`}
|
||||
loading={isFetching}
|
||||
onClick={() => goTo(party.shortcode)}
|
||||
onSave={(teamId, favorited) => toggleFavorite(teamId, favorited)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
function renderLoading(number: number) {
|
||||
return (
|
||||
<GridRepCollection>
|
||||
{Array.from(Array(number)).map((x, i) => (
|
||||
{Array.from({ length: number }, (_, i) => (
|
||||
<LoadingRep key={`loading-${i}`} />
|
||||
))}
|
||||
</GridRepCollection>
|
||||
|
|
@ -348,7 +172,6 @@ const TeamsRoute: React.FC<Props> = ({
|
|||
onFilter={receiveFilters}
|
||||
onAdvancedFilter={receiveAdvancedFilters}
|
||||
persistFilters={true}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raid={raid}
|
||||
raidGroups={context.raidGroups}
|
||||
|
|
@ -381,65 +204,35 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Fetch latest version
|
||||
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 {
|
||||
// Fetch and organize raids
|
||||
let raidGroups: RaidGroup[] = await api
|
||||
.raidGroups()
|
||||
.then((response) => response.data)
|
||||
const advancedFilters = parseAdvancedFilters(req, res)
|
||||
const convertedFilters = advancedFilters
|
||||
? convertAdvancedFilters(advancedFilters)
|
||||
: 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 {
|
||||
props: {
|
||||
context: context,
|
||||
version: version,
|
||||
context: { teams, raidGroups, pagination },
|
||||
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
|
||||
// If error is of type AxiosError, extract the response into a variable
|
||||
let response: AxiosResponse<any, any> | undefined = axios.isAxiosError(error)
|
||||
? error.response
|
||||
: undefined
|
||||
|
||||
return {
|
||||
props: {
|
||||
context: null,
|
||||
error: true,
|
||||
status: {
|
||||
code: response ? response.status : -999,
|
||||
text: response ? response.statusText : "unspecified_error",
|
||||
code: response?.status ?? -999,
|
||||
text: response?.statusText ?? 'unspecified_error',
|
||||
},
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
},
|
||||
|
|
|
|||
5
types/FilterSet.d.ts
vendored
5
types/FilterSet.d.ts
vendored
|
|
@ -16,3 +16,8 @@ interface FilterSet {
|
|||
includes?: MentionItem[]
|
||||
excludes?: MentionItem[]
|
||||
}
|
||||
|
||||
interface ConvertedFilters extends Omit<FilterSet, 'includes' | 'excludes'> {
|
||||
includes: string
|
||||
excludes: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
export function convertAdvancedFilters(filters: FilterSet) {
|
||||
let copy = cloneDeep(filters)
|
||||
export function convertAdvancedFilters(filters: FilterSet): ConvertedFilters {
|
||||
let copy: FilterSet = cloneDeep(filters)
|
||||
|
||||
const includes = filterString(filters.includes || [])
|
||||
const excludes = filterString(filters.excludes || [])
|
||||
const includes: string = filterString(filters.includes || [])
|
||||
const excludes: string = filterString(filters.excludes || [])
|
||||
|
||||
delete copy.includes
|
||||
delete copy.excludes
|
||||
delete (copy as any).includes
|
||||
delete (copy as any).excludes
|
||||
|
||||
return {
|
||||
...copy,
|
||||
includes,
|
||||
excludes,
|
||||
}
|
||||
} as ConvertedFilters
|
||||
}
|
||||
|
||||
export function filterString(list: MentionItem[]) {
|
||||
export function filterString(list: MentionItem[]): string {
|
||||
return list.map((item) => item.granblue_id).join(',')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,3 +24,9 @@ export enum AboutTabs {
|
|||
Updates,
|
||||
Roadmap,
|
||||
}
|
||||
|
||||
export enum CollectionPage {
|
||||
Teams,
|
||||
Profile,
|
||||
Saved,
|
||||
}
|
||||
|
|
|
|||
20
utils/parseElement.tsx
Normal file
20
utils/parseElement.tsx
Normal 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
77
utils/serverSideUtils.tsx
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue