Merge pull request #26 from jedmund/query-params
Implement query params on Collection views
This commit is contained in:
commit
b0643d73b0
17 changed files with 672 additions and 235 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -76,3 +76,4 @@ typings/
|
||||||
|
|
||||||
# DS_Store
|
# DS_Store
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ const AccountModal = () => {
|
||||||
const privateSelect = React.createRef<HTMLInputElement>()
|
const privateSelect = React.createRef<HTMLInputElement>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(cookies.user)
|
|
||||||
if (cookies.user) setPicture(cookies.user.picture)
|
if (cookies.user) setPicture(cookies.user.picture)
|
||||||
if (cookies.user) setLanguage(cookies.user.language)
|
if (cookies.user) setLanguage(cookies.user.language)
|
||||||
}, [cookies])
|
}, [cookies])
|
||||||
|
|
|
||||||
|
|
@ -9,48 +9,61 @@ import './index.scss'
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
scrolled: boolean
|
scrolled: boolean
|
||||||
onFilter: (element?: number, raid?: string, recency?: number) => void
|
element?: number
|
||||||
|
raidSlug?: string
|
||||||
|
recency?: number
|
||||||
|
onFilter: ({element, raidSlug, recency} : { element?: number, raidSlug?: string, recency?: number}) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterBar = (props: Props) => {
|
const FilterBar = (props: Props) => {
|
||||||
|
// Set up translation
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// Set up refs for filter dropdowns
|
||||||
const elementSelect = React.createRef<HTMLSelectElement>()
|
const elementSelect = React.createRef<HTMLSelectElement>()
|
||||||
const raidSelect = React.createRef<HTMLSelectElement>()
|
const raidSelect = React.createRef<HTMLSelectElement>()
|
||||||
const recencySelect = React.createRef<HTMLSelectElement>()
|
const recencySelect = React.createRef<HTMLSelectElement>()
|
||||||
|
|
||||||
|
// Set up classes object for showing shadow on scroll
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'FilterBar': true,
|
'FilterBar': true,
|
||||||
'shadow': props.scrolled
|
'shadow': props.scrolled
|
||||||
})
|
})
|
||||||
|
|
||||||
function selectChanged(event: React.ChangeEvent<HTMLSelectElement>) {
|
function elementSelectChanged() {
|
||||||
const elementValue = (elementSelect.current) ? parseInt(elementSelect.current.value) : -1
|
const elementValue = (elementSelect.current) ? parseInt(elementSelect.current.value) : -1
|
||||||
const raidValue = (raidSelect.current) ? raidSelect.current.value : ''
|
props.onFilter({ element: elementValue })
|
||||||
const recencyValue = (recencySelect.current) ? parseInt(recencySelect.current.value) : -1
|
}
|
||||||
|
|
||||||
props.onFilter(elementValue, raidValue, recencyValue)
|
function recencySelectChanged() {
|
||||||
|
const recencyValue = (recencySelect.current) ? parseInt(recencySelect.current.value) : -1
|
||||||
|
props.onFilter({ recency: recencyValue })
|
||||||
|
}
|
||||||
|
|
||||||
|
function raidSelectChanged(slug?: string) {
|
||||||
|
props.onFilter({ raidSlug: slug })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<select onChange={selectChanged} ref={elementSelect}>
|
<select onChange={elementSelectChanged} ref={elementSelect} value={props.element}>
|
||||||
<option key={-1} value={-1}>{t('elements.full.all')}</option>
|
<option data-element="all" key={-1} value={-1}>{t('elements.full.all')}</option>
|
||||||
<option key={-0} value={0}>{t('elements.full.null')}</option>
|
<option data-element="null" key={0} value={0}>{t('elements.full.null')}</option>
|
||||||
<option key={1}value={1}>{t('elements.full.wind')}</option>
|
<option data-element="wind" key={1} value={1}>{t('elements.full.wind')}</option>
|
||||||
<option key={2}value={2}>{t('elements.full.fire')}</option>
|
<option data-element="fire" key={2} value={2}>{t('elements.full.fire')}</option>
|
||||||
<option key={3}value={3}>{t('elements.full.water')}</option>
|
<option data-element="water" key={3} value={3}>{t('elements.full.water')}</option>
|
||||||
<option key={4}value={4}>{t('elements.full.earth')}</option>
|
<option data-element="earth" key={4} value={4}>{t('elements.full.earth')}</option>
|
||||||
<option key={5}value={5}>{t('elements.full.dark')}</option>
|
<option data-element="dark" key={5} value={5}>{t('elements.full.dark')}</option>
|
||||||
<option key={6}value={6}>{t('elements.full.light')}</option>
|
<option data-element="light" key={6} value={6}>{t('elements.full.light')}</option>
|
||||||
</select>
|
</select>
|
||||||
<RaidDropdown
|
<RaidDropdown
|
||||||
allOption={true}
|
currentRaid={props.raidSlug}
|
||||||
onChange={selectChanged}
|
showAllRaidsOption={true}
|
||||||
|
onChange={raidSelectChanged}
|
||||||
ref={raidSelect}
|
ref={raidSelect}
|
||||||
/>
|
/>
|
||||||
<select onChange={selectChanged} ref={recencySelect}>
|
<select onChange={recencySelectChanged} ref={recencySelect}>
|
||||||
<option key={-1} value={-1}>{t('recency.all_time')}</option>
|
<option key={-1} value={-1}>{t('recency.all_time')}</option>
|
||||||
<option key={86400} value={86400}>{t('recency.last_day')}</option>
|
<option key={86400} value={86400}>{t('recency.last_day')}</option>
|
||||||
<option key={604800} value={604800}>{t('recency.last_week')}</option>
|
<option key={604800} value={604800}>{t('recency.last_week')}</option>
|
||||||
|
|
|
||||||
|
|
@ -117,13 +117,18 @@ const GridRep = (props: Props) => {
|
||||||
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
|
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
|
||||||
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div>
|
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div>
|
||||||
</div>
|
</div>
|
||||||
{ (account.authorized && (props.user && account.user && account.user.id !== props.user.id)) ?
|
{
|
||||||
<Button
|
(account.authorized && (
|
||||||
active={props.favorited}
|
(props.user && account.user && account.user.id !== props.user.id)
|
||||||
icon="save"
|
|| (!props.user)
|
||||||
type={ButtonType.IconOnly}
|
)) ?
|
||||||
onClick={sendSaveData}
|
<Button
|
||||||
/> : ''}
|
active={props.favorited}
|
||||||
|
icon="save"
|
||||||
|
type={ButtonType.IconOnly}
|
||||||
|
onClick={sendSaveData} />
|
||||||
|
: ''
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="bottom">
|
<div className="bottom">
|
||||||
<div className={userClass}>
|
<div className={userClass}>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const HeaderMenu = (props: Props) => {
|
||||||
<ul className="Menu auth">
|
<ul className="Menu auth">
|
||||||
<div className="MenuGroup">
|
<div className="MenuGroup">
|
||||||
<li className="MenuItem profile">
|
<li className="MenuItem profile">
|
||||||
<Link href={`/${accountCookies.account.username}` || ''}>
|
<Link href={`/${accountCookies.account.username}` || ''} passHref>
|
||||||
<div>
|
<div>
|
||||||
<span>{accountCookies.account.username}</span>
|
<span>{accountCookies.account.username}</span>
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ const PartyDetails = (props: Props) => {
|
||||||
function updateDetails(event: React.ChangeEvent) {
|
function updateDetails(event: React.ChangeEvent) {
|
||||||
const nameValue = nameInput.current?.value
|
const nameValue = nameInput.current?.value
|
||||||
const descriptionValue = descriptionInput.current?.value
|
const descriptionValue = descriptionInput.current?.value
|
||||||
const raid = raids.find(raid => raid.id == raidSelect.current?.value)
|
const raid = raids.find(raid => raid.slug === raidSelect.current?.value)
|
||||||
|
|
||||||
props.updateCallback(nameValue, descriptionValue, raid)
|
props.updateCallback(nameValue, descriptionValue, raid)
|
||||||
}
|
}
|
||||||
|
|
@ -83,8 +83,8 @@ const PartyDetails = (props: Props) => {
|
||||||
ref={nameInput}
|
ref={nameInput}
|
||||||
/>
|
/>
|
||||||
<RaidDropdown
|
<RaidDropdown
|
||||||
allOption={false}
|
showAllRaidsOption={false}
|
||||||
selected={party.raid?.id || ''}
|
currentRaid={party.raid?.slug || ''}
|
||||||
onBlur={updateDetails}
|
onBlur={updateDetails}
|
||||||
ref={raidSelect}
|
ref={raidSelect}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,45 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
|
||||||
|
|
||||||
import { appState } from '~utils/appState'
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import { appState } from '~utils/appState'
|
||||||
|
import { raidGroups } from '~utils/raidGroups'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
allOption: boolean
|
showAllRaidsOption: boolean
|
||||||
selected?: string
|
currentRaid?: string
|
||||||
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
onChange?: (slug?: string) => void
|
||||||
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
|
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
|
||||||
|
// Set up router for locale
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation('common')
|
const locale = router.locale || 'en'
|
||||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
|
||||||
|
|
||||||
const [raids, setRaids] = useState<Raid[][]>()
|
// Set up local states for storing raids
|
||||||
|
const [currentRaid, setCurrentRaid] = useState<Raid>()
|
||||||
const raidGroups = [
|
const [raids, setRaids] = useState<Raid[]>()
|
||||||
'Assorted', 'Guild Wars', 'Omega', 'T1 Summons', 'T2 Summons',
|
const [sortedRaids, setSortedRaids] = useState<Raid[][]>()
|
||||||
'Primarchs', 'Nightmare', 'Omega (Impossible)', 'Omega II',
|
|
||||||
'Tier 1 Summons (Impossible)', 'Tier 3 Summons', 'Ennead', 'Malice',
|
|
||||||
'6-Star Raids', 'Six-Dragons', 'Nightmare (Impossible)', 'Arcarum: Replicard Sandbox',
|
|
||||||
'Astral', 'Super Ultimate'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
// Organize raids into groups on mount
|
||||||
const organizeRaids = useCallback((raids: Raid[]) => {
|
const organizeRaids = useCallback((raids: Raid[]) => {
|
||||||
|
// Set up empty raid for "All raids"
|
||||||
|
const all = {
|
||||||
|
id: '0',
|
||||||
|
name: {
|
||||||
|
en: 'All raids',
|
||||||
|
ja: '全て'
|
||||||
|
},
|
||||||
|
slug: 'all',
|
||||||
|
level: 0,
|
||||||
|
group: 0,
|
||||||
|
element: 0
|
||||||
|
}
|
||||||
|
|
||||||
const numGroups = Math.max.apply(Math, raids.map(raid => raid.group))
|
const numGroups = Math.max.apply(Math, raids.map(raid => raid.group))
|
||||||
let groupedRaids = []
|
let groupedRaids = []
|
||||||
|
|
||||||
|
|
@ -38,55 +47,64 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
|
||||||
groupedRaids[i] = raids.filter(raid => raid.group == i)
|
groupedRaids[i] = raids.filter(raid => raid.group == i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.allOption)
|
if (props.showAllRaidsOption) {
|
||||||
groupedRaids[0].unshift({
|
raids.unshift(all)
|
||||||
id: '0',
|
groupedRaids[0].unshift(all)
|
||||||
name: {
|
|
||||||
en: 'All raids',
|
|
||||||
ja: '全てのマルチ'
|
|
||||||
},
|
|
||||||
level: 0,
|
|
||||||
group: 0,
|
|
||||||
element: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
setRaids(groupedRaids)
|
|
||||||
}, [props.allOption])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function fetchRaids() {
|
|
||||||
api.endpoints.raids.getAll()
|
|
||||||
.then((response) => {
|
|
||||||
const raids = response.data.map((r: any) => r.raid)
|
|
||||||
|
|
||||||
appState.raids = raids
|
|
||||||
organizeRaids(raids)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRaids()
|
setRaids(raids)
|
||||||
|
setSortedRaids(groupedRaids)
|
||||||
|
appState.raids = raids
|
||||||
|
}, [props.showAllRaidsOption])
|
||||||
|
|
||||||
|
// Fetch all raids on mount
|
||||||
|
useEffect(() => {
|
||||||
|
api.endpoints.raids.getAll()
|
||||||
|
.then(response => organizeRaids(response.data.map((r: any) => r.raid)))
|
||||||
}, [organizeRaids])
|
}, [organizeRaids])
|
||||||
|
|
||||||
function raidGroup(index: number) {
|
// Set current raid on mount
|
||||||
const options = raids && raids.length > 0 && raids[index].length > 0 &&
|
useEffect(() => {
|
||||||
raids[index].sort((a, b) => a.element - b.element).map((item, i) => {
|
if (raids && props.currentRaid) {
|
||||||
|
const raid = raids.find(raid => raid.slug === props.currentRaid)
|
||||||
|
setCurrentRaid(raid)
|
||||||
|
}
|
||||||
|
}, [raids, props.currentRaid])
|
||||||
|
|
||||||
|
// Enable changing select value
|
||||||
|
function handleChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
|
if (props.onChange) props.onChange(event.target.value)
|
||||||
|
|
||||||
|
if (raids) {
|
||||||
|
const raid = raids.find(raid => raid.slug === event.target.value)
|
||||||
|
setCurrentRaid(raid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render JSX for each raid option, sorted into optgroups
|
||||||
|
function renderRaidGroup(index: number) {
|
||||||
|
const options = sortedRaids && sortedRaids.length > 0 && sortedRaids[index].length > 0 &&
|
||||||
|
sortedRaids[index].sort((a, b) => a.element - b.element).map((item, i) => {
|
||||||
return (
|
return (
|
||||||
<option key={i} value={item.id}>{item.name[locale]}</option>
|
<option key={i} value={item.slug}>{item.name[locale]}</option>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<optgroup key={index} label={raidGroups[index]}>
|
<optgroup key={index} label={raidGroups[index].name[locale]}>
|
||||||
{options}
|
{options}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select key={props.selected} defaultValue={props.selected} onBlur={props.onBlur} onChange={props.onChange} ref={ref}>
|
<select
|
||||||
{ Array.from(Array(raids?.length)).map((x, i) => {
|
key={currentRaid?.slug}
|
||||||
return raidGroup(i)
|
value={currentRaid?.slug}
|
||||||
})}
|
onBlur={props.onBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
ref={ref}>
|
||||||
|
{ Array.from(Array(sortedRaids?.length)).map((x, i) => renderRaidGroup(i)) }
|
||||||
</select>
|
</select>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,6 @@ interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyNames {
|
|
||||||
[key: string]: {
|
|
||||||
en: string,
|
|
||||||
jp: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SummonHovercard = (props: Props) => {
|
const SummonHovercard = (props: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
|
||||||
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -26,6 +26,7 @@
|
||||||
"next": "12.0.8",
|
"next": "12.0.8",
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^10.5.0",
|
||||||
"next-remote-watch": "^1.0.0",
|
"next-remote-watch": "^1.0.0",
|
||||||
|
"next-usequerystate": "^1.7.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|
@ -46,7 +47,7 @@
|
||||||
"eslint": "8.7.0",
|
"eslint": "8.7.0",
|
||||||
"eslint-config-next": "12.0.8",
|
"eslint-config-next": "12.0.8",
|
||||||
"eslint-plugin-valtio": "^0.4.1",
|
"eslint-plugin-valtio": "^0.4.1",
|
||||||
"typescript": "4.5.5"
|
"typescript": "^4.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
|
|
@ -6271,6 +6272,16 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-usequerystate": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-usequerystate/-/next-usequerystate-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-OAR+4zgWCz1P5XnrbCrhTBf/UfJIoj17mXRDB5daUq5yYZyYETWQnJFuqEFB9qEd8KrpjBNF2aSmrQpqVXdLUA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-dom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||||
|
|
@ -12334,6 +12345,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"next-usequerystate": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-usequerystate/-/next-usequerystate-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-OAR+4zgWCz1P5XnrbCrhTBf/UfJIoj17mXRDB5daUq5yYZyYETWQnJFuqEFB9qEd8KrpjBNF2aSmrQpqVXdLUA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"next": "12.0.8",
|
"next": "12.0.8",
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^10.5.0",
|
||||||
"next-remote-watch": "^1.0.0",
|
"next-remote-watch": "^1.0.0",
|
||||||
|
"next-usequerystate": "^1.7.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|
@ -51,6 +52,6 @@
|
||||||
"eslint": "8.7.0",
|
"eslint": "8.7.0",
|
||||||
"eslint-config-next": "12.0.8",
|
"eslint-config-next": "12.0.8",
|
||||||
"eslint-plugin-valtio": "^0.4.1",
|
"eslint-plugin-valtio": "^0.4.1",
|
||||||
"typescript": "4.5.5"
|
"typescript": "^4.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,97 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
|
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||||
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 api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import { elements, allElement } from '~utils/Element'
|
||||||
|
|
||||||
import GridRep from '~components/GridRep'
|
import GridRep from '~components/GridRep'
|
||||||
import GridRepCollection from '~components/GridRepCollection'
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
import FilterBar from '~components/FilterBar'
|
import FilterBar from '~components/FilterBar'
|
||||||
|
|
||||||
|
const emptyUser = {
|
||||||
|
id: '',
|
||||||
|
username: '',
|
||||||
|
granblueId: 0,
|
||||||
|
picture: {
|
||||||
|
picture: '',
|
||||||
|
element: ''
|
||||||
|
},
|
||||||
|
private: false
|
||||||
|
}
|
||||||
|
|
||||||
const ProfileRoute: React.FC = () => {
|
const ProfileRoute: React.FC = () => {
|
||||||
|
// Set up cookies
|
||||||
|
const [cookies] = useCookies(['account'])
|
||||||
|
const headers = (cookies.account) ? {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||||
|
}
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
// Set up router
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { username } = router.query
|
const { username } = router.query
|
||||||
|
|
||||||
|
// Import translations
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
const [cookies] = useCookies(['account'])
|
|
||||||
|
|
||||||
|
// Set up app-specific states
|
||||||
const [found, setFound] = useState(false)
|
const [found, setFound] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
|
// Set up page-specific states
|
||||||
const [parties, setParties] = useState<Party[]>([])
|
const [parties, setParties] = useState<Party[]>([])
|
||||||
const [user, setUser] = useState<User>({
|
const [raids, setRaids] = useState<Raid[]>()
|
||||||
id: '',
|
const [raid, setRaid] = useState<Raid>()
|
||||||
username: '',
|
const [user, setUser] = useState<User>(emptyUser)
|
||||||
granblueId: 0,
|
|
||||||
picture: {
|
// Set up filter-specific query states
|
||||||
picture: '',
|
// Recency is in seconds
|
||||||
element: ''
|
const [element, setElement] = useQueryState("element", {
|
||||||
},
|
defaultValue: -1,
|
||||||
private: false
|
parse: (query: string) => parseElement(query),
|
||||||
|
serialize: value => serializeElement(value)
|
||||||
})
|
})
|
||||||
|
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||||
|
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1))
|
||||||
|
|
||||||
// Filter states
|
// Define transformers for element
|
||||||
const [element, setElement] = useState<number | null>(null)
|
function parseElement(query: string) {
|
||||||
const [raidId, setRaidId] = useState<string | null>(null)
|
let element: TeamElement | undefined =
|
||||||
const [recencyInSeconds, setRecencyInSeconds] = useState<number | null>(null)
|
(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scroll event listener for shadow on FilterBar on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("scroll", handleScroll)
|
window.addEventListener("scroll", handleScroll)
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
const handleError = useCallback((error: any) => {
|
const handleError = useCallback((error: any) => {
|
||||||
if (error.response != null) {
|
if (error.response != null) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
@ -56,24 +103,14 @@ const ProfileRoute: React.FC = () => {
|
||||||
const fetchProfile = useCallback(() => {
|
const fetchProfile = useCallback(() => {
|
||||||
const filters = {
|
const filters = {
|
||||||
params: {
|
params: {
|
||||||
element: element,
|
element: (element != -1) ? element : undefined,
|
||||||
raid: raidId,
|
raid: (raid) ? raid.id : undefined,
|
||||||
recency: recencyInSeconds
|
recency: (recency != -1) ? recency : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = (cookies.account) ? {
|
if (username && !Array.isArray(username))
|
||||||
headers: {
|
api.endpoints.users.getOne({ id: username , params: {...filters, ...headers} })
|
||||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
|
||||||
}
|
|
||||||
} : {}
|
|
||||||
|
|
||||||
const params = {...filters, ...headers}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
if (username)
|
|
||||||
api.endpoints.users.getOne({ id: username as string, params: params })
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
setUser({
|
setUser({
|
||||||
id: response.data.user.id,
|
id: response.data.user.id,
|
||||||
|
|
@ -91,29 +128,55 @@ const ProfileRoute: React.FC = () => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
.catch(error => handleError(error))
|
.catch(error => handleError(error))
|
||||||
}, [username, element, raidId, recencyInSeconds, cookies.account, handleError])
|
}, [element, raid, recency])
|
||||||
|
|
||||||
|
// Fetch all raids on mount, then find the raid in the URL if present
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProfile()
|
api.endpoints.raids.getAll()
|
||||||
}, [fetchProfile])
|
.then(response => {
|
||||||
|
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||||
|
setRaids(cleanRaids)
|
||||||
|
|
||||||
function receiveFilters(element?: number, raid?: string, recency?: number) {
|
setRaidsLoading(false)
|
||||||
if (element != null && element >= 0)
|
|
||||||
|
const raid = cleanRaids.find(r => r.slug === raidSlug)
|
||||||
|
setRaid(raid)
|
||||||
|
|
||||||
|
return raid
|
||||||
|
})
|
||||||
|
}, [setRaids])
|
||||||
|
|
||||||
|
// When the element, raid or recency filter changes,
|
||||||
|
// fetch all teams again.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!raidsLoading) fetchProfile()
|
||||||
|
}, [element, raid, recency])
|
||||||
|
|
||||||
|
// On first mount only, disable loading if we are fetching all teams
|
||||||
|
useEffect(() => {
|
||||||
|
if (raidSlug === 'all') {
|
||||||
|
setRaidsLoading(false)
|
||||||
|
fetchProfile()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Receive filters from the filter bar
|
||||||
|
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) {
|
||||||
|
if (element == 0)
|
||||||
|
setElement(0)
|
||||||
|
else if (element)
|
||||||
setElement(element)
|
setElement(element)
|
||||||
else
|
|
||||||
setElement(null)
|
|
||||||
|
|
||||||
if (raid && raid != '0')
|
if (raids && raidSlug) {
|
||||||
setRaidId(raid)
|
const raid = raids.find(raid => raid.slug === raidSlug)
|
||||||
else
|
setRaid(raid)
|
||||||
setRaidId(null)
|
setRaidSlug(raidSlug)
|
||||||
|
}
|
||||||
|
|
||||||
if (recency && recency > 0)
|
if (recency) setRecency(recency)
|
||||||
setRecencyInSeconds(recency)
|
|
||||||
else
|
|
||||||
setRecencyInSeconds(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Navigation
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (window.pageYOffset > 90)
|
if (window.pageYOffset > 90)
|
||||||
setScrolled(true)
|
setScrolled(true)
|
||||||
|
|
@ -125,6 +188,8 @@ const ProfileRoute: React.FC = () => {
|
||||||
router.push(`/p/${shortcode}`)
|
router.push(`/p/${shortcode}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add save functions
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="Profile">
|
<div id="Profile">
|
||||||
<Head>
|
<Head>
|
||||||
|
|
@ -140,17 +205,22 @@ const ProfileRoute: React.FC = () => {
|
||||||
<meta name="twitter:title" content={`@${user.username}\'s Teams`} />
|
<meta name="twitter:title" content={`@${user.username}\'s Teams`} />
|
||||||
<meta name="twitter:description" content={`Browse @${user.username}\''s Teams and filter raid, element or recency`} />
|
<meta name="twitter:description" content={`Browse @${user.username}\''s Teams and filter raid, element or recency`} />
|
||||||
</Head>
|
</Head>
|
||||||
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
|
<FilterBar
|
||||||
<div className="UserInfo">
|
onFilter={receiveFilters}
|
||||||
<img
|
scrolled={scrolled}
|
||||||
alt={user.picture.picture}
|
element={element}
|
||||||
className={`profile ${user.picture.element}`}
|
raidSlug={ (raidSlug) ? raidSlug : undefined }
|
||||||
srcSet={`/profile/${user.picture.picture}.png,
|
recency={recency}>
|
||||||
/profile/${user.picture.picture}@2x.png 2x`}
|
<div className="UserInfo">
|
||||||
src={`/profile/${user.picture.picture}.png`}
|
<img
|
||||||
/>
|
alt={user.picture.picture}
|
||||||
<h1>{user.username}</h1>
|
className={`profile ${user.picture.element}`}
|
||||||
</div>
|
srcSet={`/profile/${user.picture.picture}.png,
|
||||||
|
/profile/${user.picture.picture}@2x.png 2x`}
|
||||||
|
src={`/profile/${user.picture.picture}.png`}
|
||||||
|
/>
|
||||||
|
<h1>{user.username}</h1>
|
||||||
|
</div>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
||||||
150
pages/saved.tsx
150
pages/saved.tsx
|
|
@ -1,43 +1,84 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
|
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import clonedeep from 'lodash.clonedeep'
|
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||||
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import { elements, allElement } from '~utils/Element'
|
||||||
|
|
||||||
import GridRep from '~components/GridRep'
|
import GridRep from '~components/GridRep'
|
||||||
import GridRepCollection from '~components/GridRepCollection'
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
import FilterBar from '~components/FilterBar'
|
import FilterBar from '~components/FilterBar'
|
||||||
|
|
||||||
const SavedRoute: React.FC = () => {
|
const SavedRoute: React.FC = () => {
|
||||||
const router = useRouter()
|
// Set up cookies
|
||||||
const { t } = useTranslation('common')
|
|
||||||
|
|
||||||
// Cookies
|
|
||||||
const [cookies] = useCookies(['account'])
|
const [cookies] = useCookies(['account'])
|
||||||
const headers = (cookies.account != null) ? {
|
const headers = (cookies.account) ? {
|
||||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
headers: {
|
||||||
|
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||||
|
}
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
|
// Set up router
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Import translations
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// Set up app-specific states
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
|
// Set up page-specific states
|
||||||
const [parties, setParties] = useState<Party[]>([])
|
const [parties, setParties] = useState<Party[]>([])
|
||||||
|
const [raids, setRaids] = useState<Raid[]>()
|
||||||
|
const [raid, setRaid] = useState<Raid>()
|
||||||
|
|
||||||
// Filter states
|
// Set up filter-specific query states
|
||||||
const [element, setElement] = useState<number | null>(null)
|
// Recency is in seconds
|
||||||
const [raidId, setRaidId] = useState<string | null>(null)
|
const [element, setElement] = useQueryState("element", {
|
||||||
const [recencyInSeconds, setRecencyInSeconds] = useState<number | null>(null)
|
defaultValue: -1,
|
||||||
|
parse: (query: string) => parseElement(query),
|
||||||
|
serialize: value => serializeElement(value)
|
||||||
|
})
|
||||||
|
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||||
|
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1))
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scroll event listener for shadow on FilterBar on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("scroll", handleScroll)
|
window.addEventListener("scroll", handleScroll)
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
const handleError = useCallback((error: any) => {
|
const handleError = useCallback((error: any) => {
|
||||||
if (error.response != null) {
|
if (error.response != null) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
@ -47,51 +88,73 @@ const SavedRoute: React.FC = () => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchTeams = useCallback(() => {
|
const fetchTeams = useCallback(() => {
|
||||||
const filterParams = {
|
const filters = {
|
||||||
params: {
|
params: {
|
||||||
element: element,
|
element: (element != -1) ? element : undefined,
|
||||||
raid: raidId,
|
raid: (raid) ? raid?.id : undefined,
|
||||||
recency: recencyInSeconds
|
recency: (recency != -1) ? recency : undefined
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
api.savedTeams({...filters, ...headers})
|
||||||
|
|
||||||
api.savedTeams(filterParams)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const parties: Party[] = response.data
|
const parties: Party[] = response.data
|
||||||
setParties(parties.map((p: any) => p.party).sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
setParties(parties.map((p: any) => p.party)
|
||||||
|
.sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
.catch(error => handleError(error))
|
.catch(error => handleError(error))
|
||||||
}, [element, raidId, recencyInSeconds, cookies.account, handleError])
|
}, [element, raid, recency])
|
||||||
|
|
||||||
|
// Fetch all raids on mount, then find the raid in the URL if present
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTeams()
|
api.endpoints.raids.getAll()
|
||||||
}, [fetchTeams])
|
.then(response => {
|
||||||
|
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||||
|
setRaids(cleanRaids)
|
||||||
|
|
||||||
function receiveFilters(element?: number, raid?: string, recency?: number) {
|
setRaidsLoading(false)
|
||||||
if (element != null && element >= 0)
|
|
||||||
|
const raid = cleanRaids.find(r => r.slug === raidSlug)
|
||||||
|
setRaid(raid)
|
||||||
|
|
||||||
|
return raid
|
||||||
|
})
|
||||||
|
}, [setRaids])
|
||||||
|
|
||||||
|
// When the element, raid or recency filter changes,
|
||||||
|
// fetch all teams again.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!raidsLoading) fetchTeams()
|
||||||
|
}, [element, raid, recency])
|
||||||
|
|
||||||
|
// On first mount only, disable loading if we are fetching all teams
|
||||||
|
useEffect(() => {
|
||||||
|
if (raidSlug === 'all') {
|
||||||
|
setRaidsLoading(false)
|
||||||
|
fetchTeams()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Receive filters from the filter bar
|
||||||
|
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) {
|
||||||
|
if (element == 0)
|
||||||
|
setElement(0)
|
||||||
|
else if (element)
|
||||||
setElement(element)
|
setElement(element)
|
||||||
else
|
|
||||||
setElement(null)
|
|
||||||
|
|
||||||
if (raid && raid != '0')
|
if (raids && raidSlug) {
|
||||||
setRaidId(raid)
|
const raid = raids.find(raid => raid.slug === raidSlug)
|
||||||
else
|
setRaid(raid)
|
||||||
setRaidId(null)
|
setRaidSlug(raidSlug)
|
||||||
|
}
|
||||||
|
|
||||||
if (recency && recency > 0)
|
if (recency) setRecency(recency)
|
||||||
setRecencyInSeconds(recency)
|
|
||||||
else
|
|
||||||
setRecencyInSeconds(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Favorites
|
||||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||||
if (favorited)
|
if (favorited)
|
||||||
unsaveFavorite(teamId)
|
unsaveFavorite(teamId)
|
||||||
|
|
@ -133,6 +196,7 @@ const SavedRoute: React.FC = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Navigation
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (window.pageYOffset > 90)
|
if (window.pageYOffset > 90)
|
||||||
setScrolled(true)
|
setScrolled(true)
|
||||||
|
|
@ -158,8 +222,13 @@ const SavedRoute: React.FC = () => {
|
||||||
<meta name="twitter:title" content="Your saved Teams" />
|
<meta name="twitter:title" content="Your saved Teams" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
|
<FilterBar
|
||||||
<h1>{t('saved.title')}</h1>
|
onFilter={receiveFilters}
|
||||||
|
scrolled={scrolled}
|
||||||
|
element={element}
|
||||||
|
raidSlug={ (raidSlug) ? raidSlug : undefined }
|
||||||
|
recency={recency}>
|
||||||
|
<h1>{t('saved.title')}</h1>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -178,8 +247,7 @@ const SavedRoute: React.FC = () => {
|
||||||
key={`party-${i}`}
|
key={`party-${i}`}
|
||||||
displayUser={true}
|
displayUser={true}
|
||||||
onClick={goTo}
|
onClick={goTo}
|
||||||
onSave={toggleFavorite}
|
onSave={toggleFavorite} />
|
||||||
/>
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</GridRepCollection>
|
</GridRepCollection>
|
||||||
|
|
|
||||||
155
pages/teams.tsx
155
pages/teams.tsx
|
|
@ -1,46 +1,84 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
|
import { useQueryState, queryTypes } from 'next-usequerystate'
|
||||||
|
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 clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import { elements, allElement } from '~utils/Element'
|
||||||
|
|
||||||
import GridRep from '~components/GridRep'
|
import GridRep from '~components/GridRep'
|
||||||
import GridRepCollection from '~components/GridRepCollection'
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
import FilterBar from '~components/FilterBar'
|
import FilterBar from '~components/FilterBar'
|
||||||
|
|
||||||
const TeamsRoute: React.FC = () => {
|
const TeamsRoute: React.FC = () => {
|
||||||
const router = useRouter()
|
// Set up cookies
|
||||||
const { t } = useTranslation('common')
|
|
||||||
|
|
||||||
// Cookies
|
|
||||||
const [cookies] = useCookies(['account'])
|
const [cookies] = useCookies(['account'])
|
||||||
const headers = (cookies.account != null) ? {
|
const headers = (cookies.account) ? {
|
||||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
headers: {
|
||||||
|
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||||
|
}
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
|
// Set up router
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Import translations
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// Set up app-specific states
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
|
// Set up page-specific states
|
||||||
const [parties, setParties] = useState<Party[]>([])
|
const [parties, setParties] = useState<Party[]>([])
|
||||||
|
const [raids, setRaids] = useState<Raid[]>()
|
||||||
|
const [raid, setRaid] = useState<Raid>()
|
||||||
|
|
||||||
// Filter states
|
// Set up filter-specific query states
|
||||||
const [element, setElement] = useState<number | null>(null)
|
// Recency is in seconds
|
||||||
const [raidId, setRaidId] = useState<string | null>(null)
|
const [element, setElement] = useQueryState("element", {
|
||||||
const [recencyInSeconds, setRecencyInSeconds] = useState<number | null>(null)
|
defaultValue: -1,
|
||||||
|
parse: (query: string) => parseElement(query),
|
||||||
|
serialize: value => serializeElement(value)
|
||||||
|
})
|
||||||
|
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||||
|
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1))
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scroll event listener for shadow on FilterBar on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("scroll", handleScroll)
|
window.addEventListener("scroll", handleScroll)
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
const handleError = useCallback((error: any) => {
|
const handleError = useCallback((error: any) => {
|
||||||
if (error.response != null) {
|
if (error.response != null) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
@ -52,54 +90,71 @@ const TeamsRoute: React.FC = () => {
|
||||||
const fetchTeams = useCallback(() => {
|
const fetchTeams = useCallback(() => {
|
||||||
const filters = {
|
const filters = {
|
||||||
params: {
|
params: {
|
||||||
element: element,
|
element: (element != -1) ? element : undefined,
|
||||||
raid: raidId,
|
raid: (raid) ? raid.id : undefined,
|
||||||
recency: recencyInSeconds
|
recency: (recency != -1) ? recency : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = (cookies.account) ? {
|
api.endpoints.parties.getAll({...filters, ...headers})
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
|
||||||
}
|
|
||||||
} : {}
|
|
||||||
|
|
||||||
const params = {...filters, ...headers}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
api.endpoints.parties.getAll(params)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const parties: Party[] = response.data
|
const parties: Party[] = response.data
|
||||||
setParties(parties.map((p: any) => p.party).sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
setParties(parties.map((p: any) => p.party)
|
||||||
|
.sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
.catch(error => handleError(error))
|
.catch(error => handleError(error))
|
||||||
}, [element, raidId, recencyInSeconds, cookies.account, handleError])
|
}, [element, raid, recency])
|
||||||
|
|
||||||
|
// Fetch all raids on mount, then find the raid in the URL if present
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTeams()
|
api.endpoints.raids.getAll()
|
||||||
}, [fetchTeams])
|
.then(response => {
|
||||||
|
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||||
|
setRaids(cleanRaids)
|
||||||
|
|
||||||
function receiveFilters(element?: number, raid?: string, recency?: number) {
|
setRaidsLoading(false)
|
||||||
if (element != null && element >= 0)
|
|
||||||
|
const raid = cleanRaids.find(r => r.slug === raidSlug)
|
||||||
|
setRaid(raid)
|
||||||
|
|
||||||
|
return raid
|
||||||
|
})
|
||||||
|
}, [setRaids])
|
||||||
|
|
||||||
|
// When the element, raid or recency filter changes,
|
||||||
|
// fetch all teams again.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!raidsLoading) fetchTeams()
|
||||||
|
}, [element, raid, recency])
|
||||||
|
|
||||||
|
// On first mount only, disable loading if we are fetching all teams
|
||||||
|
useEffect(() => {
|
||||||
|
if (raidSlug === 'all') {
|
||||||
|
setRaidsLoading(false)
|
||||||
|
fetchTeams()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Receive filters from the filter bar
|
||||||
|
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) {
|
||||||
|
if (element == 0)
|
||||||
|
setElement(0)
|
||||||
|
else if (element)
|
||||||
setElement(element)
|
setElement(element)
|
||||||
else
|
|
||||||
setElement(null)
|
|
||||||
|
|
||||||
if (raid && raid != '0')
|
if (raids && raidSlug) {
|
||||||
setRaidId(raid)
|
const raid = raids.find(raid => raid.slug === raidSlug)
|
||||||
else
|
setRaid(raid)
|
||||||
setRaidId(null)
|
setRaidSlug(raidSlug)
|
||||||
|
}
|
||||||
|
|
||||||
if (recency && recency > 0)
|
if (recency) setRecency(recency)
|
||||||
setRecencyInSeconds(recency)
|
|
||||||
else
|
|
||||||
setRecencyInSeconds(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Favorites
|
||||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||||
if (favorited)
|
if (favorited)
|
||||||
unsaveFavorite(teamId)
|
unsaveFavorite(teamId)
|
||||||
|
|
@ -141,6 +196,7 @@ const TeamsRoute: React.FC = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Navigation
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (window.pageYOffset > 90)
|
if (window.pageYOffset > 90)
|
||||||
setScrolled(true)
|
setScrolled(true)
|
||||||
|
|
@ -167,8 +223,14 @@ const TeamsRoute: React.FC = () => {
|
||||||
<meta name="twitter:title" content="Discover Teams" />
|
<meta name="twitter:title" content="Discover Teams" />
|
||||||
<meta name="twitter:description" content="Find different Granblue Fantasy teams by raid, element or recency" />
|
<meta name="twitter:description" content="Find different Granblue Fantasy teams by raid, element or recency" />
|
||||||
</Head>
|
</Head>
|
||||||
<FilterBar onFilter={receiveFilters} scrolled={scrolled}>
|
|
||||||
<h1>{t('teams.title')}</h1>
|
<FilterBar
|
||||||
|
onFilter={receiveFilters}
|
||||||
|
scrolled={scrolled}
|
||||||
|
element={element}
|
||||||
|
raidSlug={ (raidSlug) ? raidSlug : undefined }
|
||||||
|
recency={recency}>
|
||||||
|
<h1>{t('teams.title')}</h1>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -187,8 +249,7 @@ const TeamsRoute: React.FC = () => {
|
||||||
key={`party-${i}`}
|
key={`party-${i}`}
|
||||||
displayUser={true}
|
displayUser={true}
|
||||||
onClick={goTo}
|
onClick={goTo}
|
||||||
onSave={toggleFavorite}
|
onSave={toggleFavorite} />
|
||||||
/>
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</GridRepCollection>
|
</GridRepCollection>
|
||||||
|
|
|
||||||
3
types/Raid.d.ts
vendored
3
types/Raid.d.ts
vendored
|
|
@ -5,7 +5,8 @@ interface Raid {
|
||||||
en: string
|
en: string
|
||||||
ja: string
|
ja: string
|
||||||
}
|
}
|
||||||
|
slug: string
|
||||||
level: number
|
level: number
|
||||||
group: number
|
group: number
|
||||||
element: TeamElement
|
element: number
|
||||||
}
|
}
|
||||||
7
types/TeamElement.d.ts
vendored
Normal file
7
types/TeamElement.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
interface TeamElement {
|
||||||
|
id: number,
|
||||||
|
name: {
|
||||||
|
en: string,
|
||||||
|
ja: string
|
||||||
|
}
|
||||||
|
}
|
||||||
59
utils/Element.tsx
Normal file
59
utils/Element.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
export const allElement: TeamElement = {
|
||||||
|
id: -1,
|
||||||
|
name: {
|
||||||
|
en: "All",
|
||||||
|
ja: "全s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const elements: TeamElement[] = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: {
|
||||||
|
en: "Null",
|
||||||
|
ja: "無"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: {
|
||||||
|
en: "Wind",
|
||||||
|
ja: "風"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: {
|
||||||
|
en: "Fire",
|
||||||
|
ja: "火"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: {
|
||||||
|
en: "Water",
|
||||||
|
ja: "水"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: {
|
||||||
|
en: "Earth",
|
||||||
|
ja: "土"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: {
|
||||||
|
en: "Dark",
|
||||||
|
ja: "闇"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: {
|
||||||
|
en: "Light",
|
||||||
|
ja: "光"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
124
utils/raidGroups.tsx
Normal file
124
utils/raidGroups.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
interface RaidGroup {
|
||||||
|
name: {
|
||||||
|
[key: string]: string
|
||||||
|
en: string
|
||||||
|
ja: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const raidGroups: RaidGroup[] = [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Assorted',
|
||||||
|
ja: 'その他'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Guild Wars',
|
||||||
|
ja: '星の古戦場'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Omega',
|
||||||
|
ja: 'マグナ'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'T1 Summons',
|
||||||
|
ja: '召喚石マルチ1(旧)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'T2 Summons',
|
||||||
|
ja: '召喚石マルチ2(新)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Primarchs',
|
||||||
|
ja: '四大天使'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Nightmare',
|
||||||
|
ja: 'HELL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Omega (Impossible)',
|
||||||
|
ja: 'マグナHL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Omega II',
|
||||||
|
ja: 'マグナII'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Tier 1 Summons (Impossible)',
|
||||||
|
ja: '旧召喚石HL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Tier 3 Summons',
|
||||||
|
ja: 'エピックHL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Ennead',
|
||||||
|
ja: 'エニアド'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Malice',
|
||||||
|
ja: 'マリス'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: '6-Star Raids',
|
||||||
|
ja: '★★★★★★'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Six-Dragons',
|
||||||
|
ja: '六竜HL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Nightmare (Impossible)',
|
||||||
|
ja: '高級HELL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Arcarum: Replicard Sandbox',
|
||||||
|
ja: 'アーカルム レプリカルド・サンドボックス'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Astrals',
|
||||||
|
ja: '星の民'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
en: 'Super Ultimate',
|
||||||
|
ja: 'スーパーアルティメット'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
Reference in a new issue