* Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Added World Series to weapon series empty state (#293) * Push 2023/03 updates to main (#292) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Add World series to empty state * Added items from 2023/03 Legfest and 2023/03/30 update (#290) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Missed items (#291) * Added avatars (#286) * Deploy #287 (#288) * Added avatars * Added content from the 2023/03/22 update (#287) * Added avatars (#286) * Added localizations * Added update, changed CSS * Add logic for showing Lucifer uncap and 250 art * Added new weapon series * Added updates * Add more items * Enables advanced filters in collections (#289) * Add skeleton of FilterModal * Install react-slider from Radix * Move AccountModal styles to more generic place * Make generic TableField and move styles This is so we have a base for other table rows that use different interactive elements * Implement custom Slider component This inherits from Radix's Slider * Implement SliderTableField * Implemented SwitchTableField * Change enabled switch color * Implement InputTableField * Update modal skeleton * Added localizations for Advanced filters * Update styles for various components Added some new colors and fixed spacing * Added value reporting and fixed a cycle error * Added default values, clearing filters, etc * Default values * Ability to clear filters * Receiving values from components * Fix maximum cycle depth exceeded error * Update TableFields to not error Also optional value is required * Create FilterSet.d.ts * Send filtersets to FilterModal This sends the default filterset and the user's filterset to the filter modal. The default filterset is used when resetting all filters. The users filterset is used so that it is populated with the user's values when they open the modal * Add new localizations * Change types and add default filterset object * Add fast-deep-equal package * Change value in table fields * Input table fields need to be able to be empty * Slider table fields should default to 0 if value isn't provided * Set width of Select in table field in Filter dialog * Add style for filter button with filters active * Swap to using selects for some boolean fields Charge Attack, Full Auto, and Auto Guard are not boolean values since the user can select (and the default should be) to show both on and off values. We swap to using a SelectTableField here to represent this difference. We also added logic for Full Auto and Auto Guard fields since they are tied together in some cases (you can't show Auto Guard teams that have Full Auto disabled) * Populate values from defaultFilterSet * Update how we save and propagate filters We save filterset in a local state, because the FilterBar will send it down to us from cookies. We then set each individual property from that filter set. We set inputs to have a placeholder, as max buttons and max turns could not be set (null). Then, we only send those fields when they have a value provided by the user. * Remove default filterset This was moved to a utils/ file * Propagate filters from modal This updates how we handle filter propagation to accommodate the advanced ones. The icon lights up when filters are active. * Implement advanced filters on Teams page * Add skeleton of FilterModal * Make generic TableField and move styles This is so we have a base for other table rows that use different interactive elements * Implement custom Slider component This inherits from Radix's Slider * Implement SliderTableField * Implemented SwitchTableField * Implement InputTableField * Update modal skeleton * Added localizations for Advanced filters * Update styles for various components Added some new colors and fixed spacing * Added value reporting and fixed a cycle error * Added default values, clearing filters, etc * Default values * Ability to clear filters * Receiving values from components * Fix maximum cycle depth exceeded error * Update TableFields to not error Also optional value is required * Create FilterSet.d.ts * Send filtersets to FilterModal This sends the default filterset and the user's filterset to the filter modal. The default filterset is used when resetting all filters. The users filterset is used so that it is populated with the user's values when they open the modal * Add new localizations * Change types and add default filterset object * Change value in table fields * Input table fields need to be able to be empty * Slider table fields should default to 0 if value isn't provided * Set width of Select in table field in Filter dialog * Add style for filter button with filters active * Swap to using selects for some boolean fields Charge Attack, Full Auto, and Auto Guard are not boolean values since the user can select (and the default should be) to show both on and off values. We swap to using a SelectTableField here to represent this difference. We also added logic for Full Auto and Auto Guard fields since they are tied together in some cases (you can't show Auto Guard teams that have Full Auto disabled) * Populate values from defaultFilterSet * Update how we save and propagate filters We save filterset in a local state, because the FilterBar will send it down to us from cookies. We then set each individual property from that filter set. We set inputs to have a placeholder, as max buttons and max turns could not be set (null). Then, we only send those fields when they have a value provided by the user. * Remove default filterset This was moved to a utils/ file * Propagate filters from modal This updates how we handle filter propagation to accommodate the advanced ones. The icon lights up when filters are active. * GridRep adjustments * Properly unset mainhand when cells get reused and the new team doesnt have one * Slightly better styling to make the grid more correct * Fix bad merge * Add advanced filter support to saved and profile pages * Fix auto guard text * Ensure fetchTeams callback is updated with filters * Add auto guard icon to GridRep * Disable max buttons and turns * Fix build errors
451 lines
12 KiB
TypeScript
451 lines
12 KiB
TypeScript
import React, { useCallback, useEffect, useState } from 'react'
|
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
|
import { getCookie } from 'cookies-next'
|
|
import { queryTypes, useQueryState } from 'next-usequerystate'
|
|
import { useRouter } from 'next/router'
|
|
import { useTranslation } from 'next-i18next'
|
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
|
import clonedeep from 'lodash.clonedeep'
|
|
|
|
import api from '~utils/api'
|
|
import { setHeaders } from '~utils/userToken'
|
|
import extractFilters from '~utils/extractFilters'
|
|
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
|
import organizeRaids from '~utils/organizeRaids'
|
|
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 ErrorSection from '~components/ErrorSection'
|
|
import GridRep from '~components/GridRep'
|
|
import GridRepCollection from '~components/GridRepCollection'
|
|
import FilterBar from '~components/FilterBar'
|
|
import TeamsHead from '~components/TeamsHead'
|
|
|
|
import type { AxiosError } from 'axios'
|
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
import type {
|
|
FilterObject,
|
|
PageContextObj,
|
|
PaginationObject,
|
|
ResponseStatus,
|
|
} from '~types'
|
|
|
|
interface Props {
|
|
context?: PageContextObj
|
|
version: AppUpdate
|
|
error: boolean
|
|
status?: ResponseStatus
|
|
}
|
|
|
|
const TeamsRoute: React.FC<Props> = ({
|
|
context,
|
|
version,
|
|
error,
|
|
status,
|
|
}: Props) => {
|
|
// Set up router
|
|
const router = useRouter()
|
|
|
|
// Import translations
|
|
const { t } = useTranslation('common')
|
|
|
|
// Set up app-specific states
|
|
const [raidsLoading, setRaidsLoading] = useState(true)
|
|
const [scrolled, setScrolled] = useState(false)
|
|
|
|
// Set up page-specific states
|
|
const [parties, setParties] = useState<Party[]>([])
|
|
const [raids, setRaids] = useState<Raid[]>()
|
|
const [raid, setRaid] = useState<Raid>()
|
|
|
|
// Set up infinite scrolling-related states
|
|
const [recordCount, setRecordCount] = useState(0)
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
const [totalPages, setTotalPages] = useState(1)
|
|
|
|
// Set up filter-specific query states
|
|
// Recency is in seconds
|
|
const [element, setElement] = useQueryState('element', {
|
|
defaultValue: -1,
|
|
history: 'push',
|
|
parse: (query: string) => parseElement(query),
|
|
serialize: (value) => serializeElement(value),
|
|
})
|
|
const [raidSlug, setRaidSlug] = useQueryState('raid', {
|
|
defaultValue: 'all',
|
|
history: 'push',
|
|
})
|
|
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
|
|
useEffect(() => {
|
|
if (context && context.teams && context.pagination) {
|
|
setTotalPages(context.pagination.totalPages)
|
|
setRecordCount(context.pagination.count)
|
|
replaceResults(context.pagination.count, context.teams)
|
|
appState.version = version
|
|
}
|
|
setCurrentPage(1)
|
|
}, [])
|
|
|
|
// 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 }) => {
|
|
const filters: {
|
|
[key: string]: any
|
|
} = {
|
|
element: element !== -1 ? element : undefined,
|
|
raid: raid ? raid.id : undefined,
|
|
recency: recency !== -1 ? recency : undefined,
|
|
page: currentPage,
|
|
...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)
|
|
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
|
|
useEffect(() => {
|
|
api.endpoints.raids.getAll().then((response) => {
|
|
setRaids(response.data)
|
|
|
|
setRaidsLoading(false)
|
|
|
|
const raid = response.data.find((r: Raid) => r.slug === raidSlug)
|
|
setRaid(raid)
|
|
|
|
return raid
|
|
})
|
|
}, [setRaids])
|
|
|
|
// When the element, raid or recency filter changes,
|
|
// fetch all teams again.
|
|
useDidMountEffect(() => {
|
|
setCurrentPage(1)
|
|
fetchTeams({ replace: 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 })
|
|
}, [currentPage])
|
|
|
|
// Receive filters from the filter bar
|
|
function receiveFilters(filters: FilterSet) {
|
|
if (filters.element == 0) setElement(0, { shallow: true })
|
|
else if (filters.element) setElement(filters.element, { shallow: true })
|
|
|
|
if (raids && filters.raidSlug) {
|
|
const raid = raids.find((raid) => raid.slug === filters.raidSlug)
|
|
setRaid(raid)
|
|
setRaidSlug(filters.raidSlug, { shallow: true })
|
|
}
|
|
|
|
if (filters.recency) setRecency(filters.recency, { shallow: true })
|
|
|
|
delete filters.element
|
|
delete filters.raidSlug
|
|
delete filters.recency
|
|
|
|
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}`)
|
|
}
|
|
|
|
// Methods: Page component rendering
|
|
function pageHead() {
|
|
return <TeamsHead />
|
|
}
|
|
|
|
function pageError() {
|
|
if (status) return <ErrorSection status={status} />
|
|
else return <div />
|
|
}
|
|
|
|
function renderParties() {
|
|
return parties.map((party, i) => {
|
|
return (
|
|
<GridRep
|
|
id={party.id}
|
|
shortcode={party.shortcode}
|
|
name={party.name}
|
|
createdAt={new Date(party.created_at)}
|
|
raid={party.raid}
|
|
grid={party.weapons}
|
|
user={party.user}
|
|
favorited={party.favorited}
|
|
fullAuto={party.full_auto}
|
|
autoGuard={party.auto_guard}
|
|
key={`party-${i}`}
|
|
displayUser={true}
|
|
onClick={goTo}
|
|
onSave={toggleFavorite}
|
|
/>
|
|
)
|
|
})
|
|
}
|
|
|
|
if (context) {
|
|
return (
|
|
<div id="Teams">
|
|
{pageHead()}
|
|
<FilterBar
|
|
onFilter={receiveFilters}
|
|
scrolled={scrolled}
|
|
element={element}
|
|
raidSlug={raidSlug ? raidSlug : undefined}
|
|
recency={recency}
|
|
>
|
|
<h1>{t('teams.title')}</h1>
|
|
</FilterBar>
|
|
|
|
<section>
|
|
<InfiniteScroll
|
|
dataLength={parties && parties.length > 0 ? parties.length : 0}
|
|
next={() => setCurrentPage(currentPage + 1)}
|
|
hasMore={totalPages > currentPage}
|
|
loader={
|
|
<div id="NotFound">
|
|
<h2>Loading...</h2>
|
|
</div>
|
|
}
|
|
>
|
|
<GridRepCollection>{renderParties()}</GridRepCollection>
|
|
</InfiniteScroll>
|
|
|
|
{parties.length == 0 ? (
|
|
<div id="NotFound">
|
|
<h2>{t('teams.not_found')}</h2>
|
|
</div>
|
|
) : (
|
|
''
|
|
)}
|
|
</section>
|
|
</div>
|
|
)
|
|
} else return pageError()
|
|
}
|
|
|
|
export const getServerSidePaths = async () => {
|
|
return {
|
|
paths: [
|
|
// Object variant:
|
|
{ params: { party: 'string' } },
|
|
],
|
|
fallback: true,
|
|
}
|
|
}
|
|
|
|
// prettier-ignore
|
|
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
|
|
// Set headers for server-side requests
|
|
setHeaders(req, res)
|
|
|
|
// Fetch latest version
|
|
const version = await fetchLatestVersion()
|
|
|
|
// Fetch user's advanced filters
|
|
const filtersCookie = getCookie('filters', { req: req, res: res })
|
|
const advancedFilters = filtersCookie ? JSON.parse(filtersCookie as string) : undefined
|
|
|
|
try {
|
|
// Fetch and organize raids
|
|
let { raids, sortedRaids } = await api.endpoints.raids
|
|
.getAll()
|
|
.then((response) => organizeRaids(response.data))
|
|
|
|
// Create filter object
|
|
const filters: FilterObject = extractFilters(query, raids)
|
|
const params = {
|
|
params: { ...filters, ...advancedFilters },
|
|
}
|
|
|
|
// 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,
|
|
raids: raids,
|
|
sortedRaids: sortedRaids,
|
|
pagination: pagination,
|
|
}
|
|
|
|
// Pass to the page component as props
|
|
return {
|
|
props: {
|
|
context: context,
|
|
version: version,
|
|
error: false,
|
|
...(await serverSideTranslations(locale, ['common'])),
|
|
},
|
|
}
|
|
} catch (error) {
|
|
// Extract the underlying Axios error
|
|
const axiosError = error as AxiosError
|
|
const response = axiosError.response
|
|
|
|
// Pass to the page component as props
|
|
return {
|
|
props: {
|
|
context: null,
|
|
error: true,
|
|
status: {
|
|
code: response?.status,
|
|
text: response?.statusText,
|
|
},
|
|
...(await serverSideTranslations(locale, ['common'])),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
export default TeamsRoute
|