Merge branch 'main' into weapon-mods

This commit is contained in:
Justin Edmund 2022-12-23 16:42:05 -08:00
commit 28f6d8fd73
41 changed files with 663 additions and 812 deletions

View file

@ -24,18 +24,6 @@ const AccountModal = () => {
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// Cookies
const cookie = getCookie('account')
const headers = {}
// cookies.account != null
// ? {
// headers: {
// Authorization: `Bearer ${cookies.account.access_token}`,
// },
// }
// : {}
// State // State
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [picture, setPicture] = useState('') const [picture, setPicture] = useState('')
@ -171,12 +159,16 @@ const AccountModal = () => {
pictureData.find((i) => i.filename === picture)?.element pictureData.find((i) => i.filename === picture)?.element
}`} }`}
> >
<img {picture ? (
alt="Profile preview" <img
srcSet={`/profile/${picture}.png, alt="Profile preview"
srcSet={`/profile/${picture}.png,
/profile/${picture}@2x.png 2x`} /profile/${picture}@2x.png 2x`}
src={`/profile/${picture}.png`} src={`/profile/${picture}.png`}
/> />
) : (
''
)}
</div> </div>
<select <select

View file

@ -29,9 +29,10 @@ const Alert = (props: Props) => {
</AlertDialog.Description> </AlertDialog.Description>
<div className="buttons"> <div className="buttons">
<AlertDialog.Cancel asChild> <AlertDialog.Cancel asChild>
<Button onClick={props.cancelAction}> <Button
{props.cancelActionText} onClick={props.cancelAction}
</Button> text={props.cancelActionText}
/>
</AlertDialog.Cancel> </AlertDialog.Cancel>
{props.primaryAction ? ( {props.primaryAction ? (
<AlertDialog.Action onClick={props.primaryAction}> <AlertDialog.Action onClick={props.primaryAction}>

View file

@ -1,6 +1,7 @@
.Limited { .Limited {
$offset: 2px; $offset: 2px;
align-items: center;
background: var(--input-bg); background: var(--input-bg);
border-radius: $input-corner; border-radius: $input-corner;
border: $offset solid transparent; border: $offset solid transparent;

View file

@ -100,8 +100,8 @@ const CharacterConflictModal = (props: Props) => {
</div> </div>
</div> </div>
<footer> <footer>
<Button onClick={close}>Nevermind</Button> <Button onClick={close} text="Nevermind" />
<Button onClick={props.resolveConflict}>Confirm</Button> <Button onClick={props.resolveConflict} text="Confirm" />
</footer> </footer>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -35,9 +35,6 @@ const CharacterGrid = (props: Props) => {
const accountData: AccountCookie = cookie const accountData: AccountCookie = cookie
? JSON.parse(cookie as string) ? JSON.parse(cookie as string)
: null : null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
// Set up state for view management // Set up state for view management
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState)
@ -104,7 +101,7 @@ const CharacterGrid = (props: Props) => {
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveCharacter(party.id, character, position) saveCharacter(party.id, character, position)
.then((response) => storeGridCharacter(response.data.grid_character)) .then((response) => storeGridCharacter(response.data))
.catch((error) => console.error(error)) .catch((error) => console.error(error))
}) })
} else { } else {
@ -122,7 +119,7 @@ const CharacterGrid = (props: Props) => {
setPosition(data.position) setPosition(data.position)
setModalOpen(true) setModalOpen(true)
} else { } else {
storeGridCharacter(data.grid_character) storeGridCharacter(data)
} }
} }
@ -131,17 +128,14 @@ const CharacterGrid = (props: Props) => {
character: Character, character: Character,
position: number position: number
) { ) {
return await api.endpoints.characters.create( return await api.endpoints.characters.create({
{ character: {
character: { party_id: partyId,
party_id: partyId, character_id: character.id,
character_id: character.id, position: position,
position: position, uncap_level: characterUncapLevel(character),
uncap_level: characterUncapLevel(character),
},
}, },
headers })
)
} }
function storeGridCharacter(gridCharacter: GridCharacter) { function storeGridCharacter(gridCharacter: GridCharacter) {
@ -155,11 +149,10 @@ const CharacterGrid = (props: Props) => {
incoming: incoming.id, incoming: incoming.id,
conflicting: conflicts.map((c) => c.id), conflicting: conflicts.map((c) => c.id),
position: position, position: position,
params: headers,
}) })
.then((response) => { .then((response) => {
// Store new character in state // Store new character in state
storeGridCharacter(response.data.grid_character) storeGridCharacter(response.data)
// Remove conflicting characters from state // Remove conflicting characters from state
conflicts.forEach( conflicts.forEach(
@ -182,24 +175,26 @@ const CharacterGrid = (props: Props) => {
} }
// Methods: Saving job and job skills // Methods: Saving job and job skills
const saveJob = function (job: Job) { const saveJob = async function (job?: Job) {
const payload = { const payload = {
party: { party: {
job_id: job ? job.id : '', job_id: job ? job.id : -1,
}, },
...headers,
} }
if (party.id && appState.party.editable) { if (party.id && appState.party.editable) {
api.updateJob({ partyId: party.id, params: payload }).then((response) => { const response = await api.updateJob({
const newParty = response.data.party partyId: party.id,
params: payload,
setJob(newParty.job)
appState.party.job = newParty.job
setJobSkills(newParty.job_skills)
appState.party.jobSkills = newParty.job_skills
}) })
const newParty = response.data
setJob(newParty.job)
appState.party.job = newParty.job
setJobSkills(newParty.job_skills)
appState.party.jobSkills = newParty.job_skills
} }
} }
@ -217,7 +212,6 @@ const CharacterGrid = (props: Props) => {
const payload = { const payload = {
party: skillObject, party: skillObject,
...headers,
} }
skillObject[positionedKey] = skill.id skillObject[positionedKey] = skill.id
@ -225,7 +219,7 @@ const CharacterGrid = (props: Props) => {
.updateJobSkills({ partyId: party.id, params: payload }) .updateJobSkills({ partyId: party.id, params: payload })
.then((response) => { .then((response) => {
// Update the current skills // Update the current skills
const newSkills = response.data.party.job_skills const newSkills = response.data.job_skills
setJobSkills(newSkills) setJobSkills(newSkills)
appState.party.jobSkills = newSkills appState.party.jobSkills = newSkills
}) })
@ -269,7 +263,7 @@ const CharacterGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) if (uncapLevel != previousUncapValues[position])
await api.updateUncap('character', id, uncapLevel).then((response) => { await api.updateUncap('character', id, uncapLevel).then((response) => {
storeGridCharacter(response.data.grid_character) storeGridCharacter(response.data)
}) })
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View file

@ -28,7 +28,7 @@
gap: $unit-half; gap: $unit-half;
h5 { h5 {
color: var(--text-secondary); color: var(--text-tertiary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;

View file

@ -22,6 +22,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: $unit; gap: $unit;
justify-content: space-between;
.left { .left {
display: flex; display: flex;
@ -38,6 +39,7 @@
.DialogClose { .DialogClose {
background: transparent; background: transparent;
border: none;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
@ -58,7 +60,13 @@
.DialogTitle { .DialogTitle {
color: var(--text-primary); color: var(--text-primary);
font-size: $font-xlarge; font-size: $font-xlarge;
flex-grow: 1;
h1 {
color: var(--text-primary);
font-size: $font-xlarge;
font-weight: $medium;
text-align: left;
}
} }
.DialogTop { .DialogTop {

View file

@ -60,7 +60,7 @@
img { img {
$diameter: $unit * 6; $diameter: $unit * 6;
border-radius: $diameter / 2; border-radius: calc($diameter / 2);
height: $diameter; height: $diameter;
width: $diameter; width: $diameter;

View file

@ -32,11 +32,6 @@ const FilterBar = (props: Props) => {
const [recencyOpen, setRecencyOpen] = useState(false) const [recencyOpen, setRecencyOpen] = useState(false)
const [elementOpen, setElementOpen] = useState(false) const [elementOpen, setElementOpen] = useState(false)
// Set up refs for filter dropdowns
const elementSelect = React.createRef<HTMLSelectElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const recencySelect = React.createRef<HTMLSelectElement>()
// Set up classes object for showing shadow on scroll // Set up classes object for showing shadow on scroll
const classes = classNames({ const classes = classNames({
FilterBar: true, FilterBar: true,
@ -69,9 +64,9 @@ const FilterBar = (props: Props) => {
<div className={classes}> <div className={classes}>
{props.children} {props.children}
<Select <Select
defaultValue={-1} value={`${props.element}`}
open={elementOpen} open={elementOpen}
onChange={elementSelectChanged} onValueChange={elementSelectChanged}
onClick={openElementSelect} onClick={openElementSelect}
> >
<SelectItem data-element="all" key={-1} value={-1}> <SelectItem data-element="all" key={-1} value={-1}>
@ -105,13 +100,13 @@ const FilterBar = (props: Props) => {
defaultRaid="all" defaultRaid="all"
showAllRaidsOption={true} showAllRaidsOption={true}
onChange={raidSelectChanged} onChange={raidSelectChanged}
ref={raidSelect}
/> />
<Select <Select
defaultValue={-1} value={`${props.recency}`}
trigger={'All time'}
open={recencyOpen} open={recencyOpen}
onChange={recencySelectChanged} onValueChange={recencySelectChanged}
onClick={openRecencySelect} onClick={openRecencySelect}
> >
<SelectItem key={-1} value={-1}> <SelectItem key={-1} value={-1}>

View file

@ -79,7 +79,7 @@ const GridRep = (props: Props) => {
let url = '' let url = ''
if (mainhand) { if (mainhand) {
if (mainhand.element == 0 && props.grid[0].element) { if (mainhand.element == 0 && props.grid[0] && props.grid[0].element) {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${props.grid[0].element}.jpg` url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${props.grid[0].element}.jpg`
} else { } else {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg` url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`
@ -119,14 +119,14 @@ const GridRep = (props: Props) => {
} }
const userImage = () => { const userImage = () => {
if (props.user && props.user.picture) { if (props.user && props.user.avatar) {
return ( return (
<img <img
alt={props.user.picture.picture} alt={props.user.avatar.picture}
className={`profile ${props.user.picture.element}`} className={`profile ${props.user.avatar.element}`}
srcSet={`/profile/${props.user.picture.picture}.png, srcSet={`/profile/${props.user.avatar.picture}.png,
/profile/${props.user.picture.picture}@2x.png 2x`} /profile/${props.user.avatar.picture}@2x.png 2x`}
src={`/profile/${props.user.picture.picture}.png`} src={`/profile/${props.user.avatar.picture}.png`}
/> />
) )
} else return <div className="no-user" /> } else return <div className="no-user" />
@ -164,7 +164,7 @@ const GridRep = (props: Props) => {
!props.user) ? ( !props.user) ? (
<Button <Button
className="Save" className="Save"
accessoryIcon={<SaveIcon class="stroke" />} accessoryIcon={<SaveIcon className="stroke" />}
active={props.favorited} active={props.favorited}
contained={true} contained={true}
buttonSize="small" buttonSize="small"

View file

@ -5,10 +5,10 @@
border-radius: 6px; border-radius: 6px;
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
padding: $unit-2x; padding: ($unit * 1.5) $unit-2x;
width: 100%; width: 100%;
&.Contained { &.Bound {
background-color: var(--input-bound-bg); background-color: var(--input-bound-bg);
&:hover { &:hover {

View file

@ -7,43 +7,33 @@ interface Props
React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement HTMLInputElement
> { > {
contained?: boolean
error?: string error?: string
label?: string label?: string
} }
const defaultProps = {
contained: false,
}
const Input = React.forwardRef<HTMLInputElement, Props>(function input( const Input = React.forwardRef<HTMLInputElement, Props>(function input(
{ contained, error, label, ...props }, props: Props,
forwardedRef forwardedRef
) { ) {
const classes = classNames( const classes = classNames({ Input: true }, props.className)
{ const { value, ...inputProps } = props
Input: true,
Contained: contained,
},
props.className
)
return ( return (
<label className="Label" htmlFor={props.name}> <label className="Label" htmlFor={props.name}>
<input <input
{...props} {...inputProps}
autoComplete="off" autoComplete="off"
className={classes} className={classes}
defaultValue={props.value || ''} defaultValue={props.value || ''}
ref={forwardedRef} ref={forwardedRef}
formNoValidate formNoValidate
/> />
{label} {props.label}
{error && error.length > 0 && <p className="InputError">{error}</p>} {props.error && props.error.length > 0 && (
<p className="InputError">{props.error}</p>
)}
</label> </label>
) )
}) })
Input.defaultProps = defaultProps
export default Input export default Input

View file

@ -102,17 +102,20 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
return ( return (
<Select <Select
value={currentJob ? currentJob.id : 'no-job'}
placeholder={'Select a class...'} placeholder={'Select a class...'}
open={open} open={open}
onClick={openJobSelect} onClick={openJobSelect}
onChange={handleChange} onValueChange={handleChange}
triggerClass="Job" triggerClass="Job"
> >
<SelectItem key={-1} value="no-job"> <SelectItem key={-1} value="no-job">
No class No class
</SelectItem> </SelectItem>
{sortedJobs {sortedJobs
? Object.keys(sortedJobs).map((x) => renderJobGroup(x)) ? Object.keys(sortedJobs)
.sort((a, b) => ('' + a).localeCompare(b))
.map((x) => renderJobGroup(x))
: ''} : ''}
</Select> </Select>
) )

View file

@ -18,7 +18,7 @@ interface Props {
job?: Job job?: Job
jobSkills: JobSkillObject jobSkills: JobSkillObject
editable: boolean editable: boolean
saveJob: (job: Job) => void saveJob: (job?: Job) => void
saveSkill: (skill: JobSkill, position: number) => void saveSkill: (skill: JobSkill, position: number) => void
} }
@ -41,17 +41,15 @@ const JobSection = (props: Props) => {
useEffect(() => { useEffect(() => {
// Set current job based on ID // Set current job based on ID
if (props.job) { setJob(props.job)
setJob(props.job) setSkills({
setSkills({ 0: props.jobSkills[0],
0: props.jobSkills[0], 1: props.jobSkills[1],
1: props.jobSkills[1], 2: props.jobSkills[2],
2: props.jobSkills[2], 3: props.jobSkills[3],
3: props.jobSkills[3], })
})
if (selectRef.current) selectRef.current.value = props.job.id if (selectRef.current && props.job) selectRef.current.value = props.job.id
}
}, [props]) }, [props])
useEffect(() => { useEffect(() => {
@ -68,10 +66,8 @@ const JobSection = (props: Props) => {
}, [job]) }, [job])
function receiveJob(job?: Job) { function receiveJob(job?: Job) {
if (job) { setJob(job)
setJob(job) props.saveJob(job)
props.saveJob(job)
}
} }
function generateImageUrl() { function generateImageUrl() {
@ -88,11 +84,13 @@ const JobSection = (props: Props) => {
} }
const canEditSkill = (skill?: JobSkill) => { const canEditSkill = (skill?: JobSkill) => {
if (job && skill) { // If there is a job and a skill present in the slot
if (skill.job.id === job.id && skill.main && !skill.sub) return false if (job) {
} // If the skill's job is one of the job's main skill
if (skill && skill.job.id === job.id && skill.main) return false
return props.editable return props.editable
} else return false
} }
const skillItem = (index: number, editable: boolean) => { const skillItem = (index: number, editable: boolean) => {

View file

@ -56,7 +56,7 @@
} }
h5 { h5 {
color: var(--text-secondary); color: var(--text-tertiary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;

View file

@ -40,9 +40,11 @@ const JobSkillSearchFilterBar = (props: Props) => {
return ( return (
<div className="SearchFilterBar"> <div className="SearchFilterBar">
<Select <Select
defaultValue={-1} value={-1}
triggerClass="Bound"
trigger={'All elements'}
open={open} open={open}
onChange={onChange} onValueChange={onChange}
onClick={openSelect} onClick={openSelect}
> >
<SelectItem key="all" value={-1}> <SelectItem key="all" value={-1}>

View file

@ -4,13 +4,17 @@ import Router, { useRouter } from 'next/router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog'
import api from '~utils/api' import api from '~utils/api'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import Button from '~components/Button' import Button from '~components/Button'
import Input from '~components/Input' import Input from '~components/Input'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogClose,
} from '~components/Dialog'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
@ -111,23 +115,23 @@ const LoginModal = (props: Props) => {
} }
function storeCookieInfo(response: AxiosResponse) { function storeCookieInfo(response: AxiosResponse) {
const user = response.data.user const resp = response.data
const cookieObj: AccountCookie = { const cookieObj: AccountCookie = {
userId: user.id, userId: resp.user.id,
username: user.username, username: resp.user.username,
token: response.data.access_token, token: resp.access_token,
} }
setCookie('account', cookieObj, { path: '/' }) setCookie('account', cookieObj, { path: '/' })
} }
function storeUserInfo(response: AxiosResponse) { function storeUserInfo(response: AxiosResponse) {
const user = response.data.user const user = response.data
const cookieObj: UserCookie = { const cookieObj: UserCookie = {
picture: user.picture.picture, picture: user.avatar.picture,
element: user.picture.element, element: user.avatar.element,
language: user.language, language: user.language,
gender: user.gender, gender: user.gender,
} }
@ -137,8 +141,8 @@ const LoginModal = (props: Props) => {
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.avatar.picture,
element: user.picture.element, element: user.avatar.element,
gender: user.gender, gender: user.gender,
} }
@ -165,51 +169,44 @@ const LoginModal = (props: Props) => {
} }
return ( return (
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <DialogTrigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('menu.login')}</span> <span>{t('menu.login')}</span>
</li> </li>
</Dialog.Trigger> </DialogTrigger>
<Dialog.Portal> <DialogContent className="Login Dialog">
<Dialog.Content <div className="DialogHeader">
className="Login Dialog" <div className="DialogTitle">
onOpenAutoFocus={(event) => event.preventDefault()} <h1>{t('modals.login.title')}</h1>
>
<div className="DialogHeader">
<Dialog.Title className="DialogTitle">
{t('modals.login.title')}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</Dialog.Close>
</div> </div>
<DialogClose className="DialogClose">
<CrossIcon />
</DialogClose>
</div>
<form className="form" onSubmit={login}> <form className="form" onSubmit={login}>
<Input <Input
name="email" name="email"
placeholder={t('modals.login.placeholders.email')} placeholder={t('modals.login.placeholders.email')}
onChange={handleChange} onChange={handleChange}
error={errors.email} error={errors.email}
ref={emailInput} ref={emailInput}
/> />
<Input <Input
name="password" name="password"
placeholder={t('modals.login.placeholders.password')} placeholder={t('modals.login.placeholders.password')}
onChange={handleChange} type="password"
error={errors.password} onChange={handleChange}
ref={passwordInput} error={errors.password}
/> ref={passwordInput}
/>
<Button>{t('modals.login.buttons.confirm')}</Button> <Button text={t('modals.login.buttons.confirm')} />
</form> </form>
</Dialog.Content> </DialogContent>
<Dialog.Overlay className="Overlay" /> </Dialog>
</Dialog.Portal>
</Dialog.Root>
) )
} }

View file

@ -25,18 +25,6 @@ interface Props {
} }
const Party = (props: Props) => { const Party = (props: Props) => {
// Cookies
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = useMemo(() => {
return accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
}, [accountData])
// Set up router // Set up router
const router = useRouter() const router = useRouter()
@ -55,12 +43,13 @@ const Party = (props: Props) => {
async function createParty(extra: boolean = false) { async function createParty(extra: boolean = false) {
let body = { let body = {
party: { party: {
...(accountData && { user_id: accountData.userId }),
extra: extra, extra: extra,
}, },
} }
return await api.endpoints.parties.create(body, headers) console.log(body)
return await api.endpoints.parties.create(body)
} }
// Methods: Updating the party's details // Methods: Updating the party's details
@ -68,13 +57,9 @@ const Party = (props: Props) => {
appState.party.extra = event.target.checked appState.party.extra = event.target.checked
if (party.id) { if (party.id) {
api.endpoints.parties.update( api.endpoints.parties.update(party.id, {
party.id, party: { extra: event.target.checked },
{ })
party: { extra: event.target.checked },
},
headers
)
} }
} }
@ -86,17 +71,13 @@ const Party = (props: Props) => {
) { ) {
if (appState.party.id) if (appState.party.id)
api.endpoints.parties api.endpoints.parties
.update( .update(appState.party.id, {
appState.party.id, party: {
{ name: name,
party: { description: description,
name: name, raid_id: raid?.id,
description: description,
raid_id: raid?.id,
},
}, },
headers })
)
.then(() => { .then(() => {
appState.party.name = name appState.party.name = name
appState.party.description = description appState.party.description = description
@ -110,7 +91,7 @@ const Party = (props: Props) => {
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (appState.party.editable && appState.party.id) { if (appState.party.editable && appState.party.id) {
api.endpoints.parties api.endpoints.parties
.destroy({ id: appState.party.id, params: headers }) .destroy({ id: appState.party.id })
.then(() => { .then(() => {
// Push to route // Push to route
router.push('/') router.push('/')
@ -131,26 +112,28 @@ const Party = (props: Props) => {
} }
// Methods: Storing party data // Methods: Storing party data
const storeParty = function (party: Party) { const storeParty = function (team: Party) {
// Store the important party and state-keeping values // Store the important party and state-keeping values
appState.party.name = party.name appState.party.name = team.name
appState.party.description = party.description appState.party.description = team.description
appState.party.raid = party.raid appState.party.raid = team.raid
appState.party.updated_at = party.updated_at appState.party.updated_at = team.updated_at
appState.party.job = party.job appState.party.job = team.job
appState.party.jobSkills = party.job_skills appState.party.jobSkills = team.job_skills
appState.party.id = party.id appState.party.id = team.id
appState.party.extra = party.extra appState.party.extra = team.extra
appState.party.user = party.user appState.party.user = team.user
appState.party.favorited = party.favorited appState.party.favorited = team.favorited
appState.party.created_at = party.created_at appState.party.created_at = team.created_at
appState.party.updated_at = party.updated_at appState.party.updated_at = team.updated_at
appState.party.detailsVisible = false
// Populate state // Populate state
storeCharacters(party.characters) storeCharacters(team.characters)
storeWeapons(party.weapons) storeWeapons(team.weapons)
storeSummons(party.summons) storeSummons(team.summons)
} }
const storeCharacters = (list: Array<GridCharacter>) => { const storeCharacters = (list: Array<GridCharacter>) => {
@ -256,13 +239,11 @@ const Party = (props: Props) => {
<React.Fragment> <React.Fragment>
{navigation} {navigation}
<section id="Party">{currentGrid()}</section> <section id="Party">{currentGrid()}</section>
{ <PartyDetails
<PartyDetails editable={party.editable}
editable={party.editable} updateCallback={updateDetails}
updateCallback={updateDetails} deleteCallback={deleteTeam}
deleteCallback={deleteTeam} />
/>
}
</React.Fragment> </React.Fragment>
) )
} }

View file

@ -1,5 +1,4 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
@ -14,7 +13,6 @@ import CharLimitedFieldset from '~components/CharLimitedFieldset'
import RaidDropdown from '~components/RaidDropdown' import RaidDropdown from '~components/RaidDropdown'
import TextFieldset from '~components/TextFieldset' import TextFieldset from '~components/TextFieldset'
import { accountState } from '~utils/accountState'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import CheckIcon from '~public/icons/Check.svg' import CheckIcon from '~public/icons/Check.svg'
@ -25,18 +23,6 @@ import './index.scss'
import Link from 'next/link' import Link from 'next/link'
import { formatTimeAgo } from '~utils/timeAgo' import { formatTimeAgo } from '~utils/timeAgo'
const emptyRaid: Raid = {
id: '',
name: {
en: '',
ja: '',
},
slug: '',
level: 0,
group: 0,
element: 0,
}
// Props // Props
interface Props { interface Props {
editable: boolean editable: boolean
@ -48,7 +34,6 @@ interface Props {
const PartyDetails = (props: Props) => { const PartyDetails = (props: Props) => {
const { party, raids } = useSnapshot(appState) const { party, raids } = useSnapshot(appState)
const { account } = useSnapshot(accountState)
const { t } = useTranslation('common') const { t } = useTranslation('common')
const router = useRouter() const router = useRouter()
@ -56,7 +41,8 @@ const PartyDetails = (props: Props) => {
const nameInput = React.createRef<HTMLInputElement>() const nameInput = React.createRef<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>() const descriptionInput = React.createRef<HTMLTextAreaElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const [raidSlug, setRaidSlug] = useState('')
const readOnlyClasses = classNames({ const readOnlyClasses = classNames({
PartyDetails: true, PartyDetails: true,
@ -116,10 +102,14 @@ const PartyDetails = (props: Props) => {
appState.party.detailsVisible = !appState.party.detailsVisible appState.party.detailsVisible = !appState.party.detailsVisible
} }
function receiveRaid(slug?: string) {
if (slug) setRaidSlug(slug)
}
function updateDetails(event: React.MouseEvent) { function updateDetails(event: React.MouseEvent) {
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.slug === raidSelect.current?.value) const raid = raids.find((raid) => raid.slug === raidSlug)
props.updateCallback(nameValue, descriptionValue, raid) props.updateCallback(nameValue, descriptionValue, raid)
toggleDetails() toggleDetails()
@ -129,11 +119,11 @@ const PartyDetails = (props: Props) => {
if (party.user) if (party.user)
return ( return (
<img <img
alt={party.user.picture.picture} alt={party.user.avatar.picture}
className={`profile ${party.user.picture.element}`} className={`profile ${party.user.avatar.element}`}
srcSet={`/profile/${party.user.picture.picture}.png, srcSet={`/profile/${party.user.avatar.picture}.png,
/profile/${party.user.picture.picture}@2x.png 2x`} /profile/${party.user.avatar.picture}@2x.png 2x`}
src={`/profile/${party.user.picture.picture}.png`} src={`/profile/${party.user.avatar.picture}.png`}
/> />
) )
else return <div className="no-user" /> else return <div className="no-user" />
@ -220,8 +210,8 @@ const PartyDetails = (props: Props) => {
/> />
<RaidDropdown <RaidDropdown
showAllRaidsOption={false} showAllRaidsOption={false}
currentRaid={party.raid?.slug || ''} currentRaid={party.raid ? party.raid.slug : undefined}
ref={raidSelect} onChange={receiveRaid}
/> />
<TextFieldset <TextFieldset
fieldName="name" fieldName="name"

View file

@ -6,6 +6,7 @@ import SelectItem from '~components/SelectItem'
import SelectGroup from '~components/SelectGroup' import SelectGroup from '~components/SelectGroup'
import api from '~utils/api' import api from '~utils/api'
import organizeRaids from '~utils/organizeRaids'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { raidGroups } from '~utils/raidGroups' import { raidGroups } from '~utils/raidGroups'
@ -20,6 +21,19 @@ interface Props {
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
} }
// Set up empty raid for "All raids"
const allRaidsOption = {
id: '0',
name: {
en: 'All raids',
ja: '全て',
},
slug: 'all',
level: 0,
group: 0,
element: 0,
}
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>( const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
function useFieldSet(props, ref) { function useFieldSet(props, ref) {
// Set up router for locale // Set up router for locale
@ -28,7 +42,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
// Set up local states for storing raids // Set up local states for storing raids
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [currentRaid, setCurrentRaid] = useState<Raid>() const [currentRaid, setCurrentRaid] = useState<Raid | undefined>(undefined)
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>()
const [sortedRaids, setSortedRaids] = useState<Raid[][]>() const [sortedRaids, setSortedRaids] = useState<Raid[][]>()
@ -37,38 +51,17 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
} }
// Organize raids into groups on mount // Organize raids into groups on mount
const organizeRaids = useCallback( const organizeAllRaids = useCallback(
(raids: Raid[]) => { (raids: Raid[]) => {
// Set up empty raid for "All raids" let { sortedRaids } = organizeRaids(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)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
if (props.showAllRaidsOption) { if (props.showAllRaidsOption) {
raids.unshift(all) raids.unshift(allRaidsOption)
groupedRaids[0].unshift(all) sortedRaids[0].unshift(allRaidsOption)
} }
setRaids(raids) setRaids(raids)
setSortedRaids(groupedRaids) setSortedRaids(sortedRaids)
appState.raids = raids appState.raids = raids
}, },
[props.showAllRaidsOption] [props.showAllRaidsOption]
@ -78,22 +71,19 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
useEffect(() => { useEffect(() => {
api.endpoints.raids api.endpoints.raids
.getAll() .getAll()
.then((response) => .then((response) => organizeAllRaids(response.data))
organizeRaids(response.data.map((r: any) => r.raid))
)
}, [organizeRaids]) }, [organizeRaids])
// Set current raid on mount // Set current raid on mount
useEffect(() => { useEffect(() => {
if (raids && props.currentRaid) { if (raids && props.currentRaid) {
const raid = raids.find((raid) => raid.slug === props.currentRaid) const raid = raids.find((raid) => raid.slug === props.currentRaid)
setCurrentRaid(raid) if (raid) setCurrentRaid(raid)
} }
}, [raids, props.currentRaid]) }, [raids, props.currentRaid])
// Enable changing select value // Enable changing select value
function handleChange(value: string) { function handleChange(value: string) {
console.log(value)
if (props.onChange) props.onChange(value) if (props.onChange) props.onChange(value)
if (raids) { if (raids) {
@ -110,13 +100,12 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
sortedRaids[index].length > 0 && sortedRaids[index].length > 0 &&
sortedRaids[index] sortedRaids[index]
.sort((a, b) => a.element - b.element) .sort((a, b) => a.element - b.element)
.map((item, i) => { .map((item, i) => (
return ( <SelectItem key={i} value={item.slug}>
<SelectItem key={i} value={item.slug}> {item.name[locale]}
{item.name[locale]} </SelectItem>
</SelectItem> ))
)
})
return ( return (
<SelectGroup <SelectGroup
key={index} key={index}
@ -129,17 +118,19 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(
} }
return ( return (
<Select <React.Fragment>
defaultValue={props.defaultRaid} <Select
placeholder={'Select a raid...'} value={props.currentRaid}
open={open} placeholder={'Select a raid...'}
onClick={openRaidSelect} open={open}
onChange={handleChange} onClick={openRaidSelect}
> onValueChange={handleChange}
{Array.from(Array(sortedRaids?.length)).map((x, i) => >
renderRaidGroup(i) {Array.from(Array(sortedRaids?.length)).map((x, i) =>
)} renderRaidGroup(i)
</Select> )}
</Select>
</React.Fragment>
) )
} }
) )

View file

@ -85,11 +85,11 @@ const SearchModal = (props: Props) => {
page: currentPage, page: currentPage,
}) })
.then((response) => { .then((response) => {
setTotalPages(response.data.total_pages) setTotalPages(response.data.meta.total_pages)
setRecordCount(response.data.count) setRecordCount(response.data.meta.count)
if (replace) { if (replace) {
replaceResults(response.data.count, response.data.results) replaceResults(response.data.meta.count, response.data.results)
} else { } else {
appendResults(response.data.results) appendResults(response.data.results)
} }
@ -330,7 +330,7 @@ const SearchModal = (props: Props) => {
<div id="Bar"> <div id="Bar">
<Input <Input
autoComplete="off" autoComplete="off"
className="Search" className="Search Bound"
name="query" name="query"
placeholder={props.placeholderText} placeholder={props.placeholderText}
ref={searchInput} ref={searchInput}

View file

@ -35,6 +35,14 @@
text-align: left; text-align: left;
} }
&.Bound {
background-color: var(--select-contained-bg);
&:hover {
background-color: var(--select-contained-bg-hover);
}
}
.SelectIcon { .SelectIcon {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -1,4 +1,4 @@
import React from 'react' import React, { useEffect, useState } from 'react'
import * as RadixSelect from '@radix-ui/react-select' import * as RadixSelect from '@radix-ui/react-select'
import classNames from 'classnames' import classNames from 'classnames'
@ -7,31 +7,41 @@ import ArrowIcon from '~public/icons/Arrow.svg'
import './index.scss' import './index.scss'
// Props // Props
interface Props { interface Props
extends React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> {
open: boolean open: boolean
defaultValue?: string | number trigger?: React.ReactNode
placeholder?: string
name?: string
children?: React.ReactNode children?: React.ReactNode
onClick?: () => void onClick?: () => void
onChange?: (value: string) => void onValueChange?: (value: string) => void
triggerClass?: string triggerClass?: string
} }
const Select = React.forwardRef<HTMLButtonElement, Props>(function useFieldSet( const Select = (props: Props) => {
props, const [value, setValue] = useState('')
ref
) { useEffect(() => {
if (props.value && props.value !== '') setValue(`${props.value}`)
else setValue('')
}, [props.value])
function onValueChange(newValue: string) {
setValue(`${newValue}`)
if (props.onValueChange) props.onValueChange(newValue)
}
console.log(value)
return ( return (
<RadixSelect.Root <RadixSelect.Root
defaultValue={props.defaultValue as string} value={value !== '' ? value : undefined}
name={props.name} onValueChange={onValueChange}
onValueChange={props.onChange}
> >
<RadixSelect.Trigger <RadixSelect.Trigger
className={classNames('SelectTrigger', props.triggerClass)} className={classNames('SelectTrigger', props.triggerClass)}
placeholder={props.placeholder} placeholder={props.placeholder}
ref={ref}
> >
<RadixSelect.Value placeholder={props.placeholder} /> <RadixSelect.Value placeholder={props.placeholder} />
<RadixSelect.Icon className="SelectIcon"> <RadixSelect.Icon className="SelectIcon">
@ -52,6 +62,6 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function useFieldSet(
</RadixSelect.Portal> </RadixSelect.Portal>
</RadixSelect.Root> </RadixSelect.Root>
) )
}) }
export default Select export default Select

View file

@ -5,13 +5,17 @@ import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios' import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog'
import api from '~utils/api' import api from '~utils/api'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import Button from '~components/Button' import Button from '~components/Button'
import Input from '~components/Input' import Input from '~components/Input'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogClose,
} from '~components/Dialog'
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from '~public/icons/Cross.svg'
import './index.scss' import './index.scss'
@ -75,19 +79,19 @@ const SignupModal = (props: Props) => {
.create(body) .create(body)
.then((response) => { .then((response) => {
storeCookieInfo(response) storeCookieInfo(response)
return response.data.user.user_id return response.data.id
}) })
.then((id) => fetchUserInfo(id)) .then((id) => fetchUserInfo(id))
.then((infoResponse) => storeUserInfo(infoResponse)) .then((infoResponse) => storeUserInfo(infoResponse))
} }
function storeCookieInfo(response: AxiosResponse) { function storeCookieInfo(response: AxiosResponse) {
const user = response.data.user const resp = response.data
const cookieObj: AccountCookie = { const cookieObj: AccountCookie = {
userId: user.user_id, userId: resp.id,
username: user.username, username: resp.username,
token: user.token, token: resp.token,
} }
setCookie('account', cookieObj, { path: '/' }) setCookie('account', cookieObj, { path: '/' })
@ -98,11 +102,11 @@ const SignupModal = (props: Props) => {
} }
function storeUserInfo(response: AxiosResponse) { function storeUserInfo(response: AxiosResponse) {
const user = response.data.user const user = response.data
const cookieObj: UserCookie = { const cookieObj: UserCookie = {
picture: user.picture.picture, picture: user.avatar.picture,
element: user.picture.element, element: user.avatar.element,
language: user.language, language: user.language,
gender: user.gender, gender: user.gender,
} }
@ -113,8 +117,8 @@ const SignupModal = (props: Props) => {
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.avatar.picture,
element: user.picture.element, element: user.avatar.element,
gender: user.gender, gender: user.gender,
} }
@ -251,73 +255,67 @@ const SignupModal = (props: Props) => {
} }
return ( return (
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <DialogTrigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('menu.signup')}</span> <span>{t('menu.signup')}</span>
</li> </li>
</Dialog.Trigger> </DialogTrigger>
<Dialog.Portal> <DialogContent className="Signup Dialog">
<Dialog.Content <div className="DialogHeader">
className="Signup Dialog" <div className="DialogTitle">
onOpenAutoFocus={(event) => event.preventDefault()} <h1>{t('modals.signup.title')}</h1>
>
<div className="DialogHeader">
<Dialog.Title className="DialogTitle">
{t('modals.signup.title')}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</Dialog.Close>
</div> </div>
<DialogClose className="DialogClose">
<CrossIcon />
</DialogClose>
</div>
<form className="form" onSubmit={register}> <form className="form" onSubmit={register}>
<Input <Input
name="username" name="username"
placeholder={t('modals.signup.placeholders.username')} placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange} onChange={handleNameChange}
error={errors.username} error={errors.username}
ref={usernameInput} ref={usernameInput}
/> />
<Input <Input
name="email" name="email"
placeholder={t('modals.signup.placeholders.email')} placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange} onChange={handleNameChange}
error={errors.email} error={errors.email}
ref={emailInput} ref={emailInput}
/> />
<Input <Input
name="password" name="password"
placeholder={t('modals.signup.placeholders.password')} placeholder={t('modals.signup.placeholders.password')}
onChange={handlePasswordChange} type="password"
error={errors.password} onChange={handlePasswordChange}
ref={passwordInput} error={errors.password}
/> ref={passwordInput}
/>
<Input <Input
name="confirm_password" name="confirm_password"
placeholder={t('modals.signup.placeholders.password_confirm')} placeholder={t('modals.signup.placeholders.password_confirm')}
onChange={handlePasswordChange} type="password"
error={errors.passwordConfirmation} onChange={handlePasswordChange}
ref={passwordConfirmationInput} error={errors.passwordConfirmation}
/> ref={passwordConfirmationInput}
/>
<Button>{t('modals.signup.buttons.confirm')}</Button> <Button text={t('modals.signup.buttons.confirm')} />
<Dialog.Description className="terms"> <p className="terms">
{/* <Trans i18nKey="modals.signup.agreement"> {/* <Trans i18nKey="modals.signup.agreement">
By signing up, I agree to the <Link href="/privacy"><span>Privacy Policy</span></Link><Link href="/usage"><span>Usage Guidelines</span></Link>. By signing up, I agree to the <Link href="/privacy"><span>Privacy Policy</span></Link><Link href="/usage"><span>Usage Guidelines</span></Link>.
</Trans> */} </Trans> */}
</Dialog.Description> </p>
</form> </form>
</Dialog.Content> </DialogContent>
<Dialog.Overlay className="Overlay" /> </Dialog>
</Dialog.Portal>
</Dialog.Root>
) )
} }

View file

@ -96,13 +96,13 @@ const SummonGrid = (props: Props) => {
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveSummon(party.id, summon, position).then((response) => saveSummon(party.id, summon, position).then((response) =>
storeGridSummon(response.data.grid_summon) storeGridSummon(response.data)
) )
}) })
} else { } else {
if (party.editable) if (party.editable)
saveSummon(party.id, summon, position).then((response) => saveSummon(party.id, summon, position).then((response) =>
storeGridSummon(response.data.grid_summon) storeGridSummon(response.data)
) )
} }
} }

View file

@ -28,7 +28,7 @@
gap: $unit-half; gap: $unit-half;
h5 { h5 {
color: var(--text-secondary); color: var(--text-tertiary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;

View file

@ -86,12 +86,12 @@ const WeaponGrid = (props: Props) => {
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveWeapon(party.id, weapon, position).then((response) => saveWeapon(party.id, weapon, position).then((response) =>
storeGridWeapon(response.data.grid_weapon) storeGridWeapon(response.data)
) )
}) })
} else { } else {
saveWeapon(party.id, weapon, position).then((response) => saveWeapon(party.id, weapon, position).then((response) =>
storeGridWeapon(response.data.grid_weapon) storeGridWeapon(response.data)
) )
} }
} }

View file

@ -28,7 +28,7 @@
gap: $unit-half; gap: $unit-half;
h5 { h5 {
color: var(--text-secondary); color: var(--text-tertiary);
display: inline-block; display: inline-block;
font-size: $font-medium; font-size: $font-medium;
font-weight: $medium; font-weight: $medium;

View file

@ -1,7 +1,6 @@
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 { getCookie } from 'cookies-next'
import { queryTypes, useQueryState } from 'next-usequerystate' 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'
@ -10,32 +9,29 @@ import InfiniteScroll from 'react-infinite-scroll-component'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import api from '~utils/api' import api from '~utils/api'
import setUserToken from '~utils/setUserToken'
import extractFilters from '~utils/extractFilters'
import organizeRaids from '~utils/organizeRaids'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~utils/Element'
import { emptyPaginationObject } from '~utils/emptyStates'
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'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import type { FilterObject, PaginationObject } from '~types'
interface Props { interface Props {
user?: User user?: User
teams?: { count: number; total_pages: number; results: Party[] } teams?: Party[]
meta: PaginationObject
raids: Raid[] raids: Raid[]
sortedRaids: Raid[][] sortedRaids: Raid[][]
} }
const ProfileRoute: React.FC<Props> = (props: Props) => { const ProfileRoute: React.FC<Props> = (props: Props) => {
// Set up cookies
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {}
// Set up router // Set up router
const router = useRouter() const router = useRouter()
const { username } = router.query const { username } = router.query
@ -61,16 +57,20 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
// Recency is in seconds // Recency is in seconds
const [element, setElement] = useQueryState('element', { const [element, setElement] = useQueryState('element', {
defaultValue: -1, defaultValue: -1,
history: 'push',
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value), serialize: (value) => serializeElement(value),
}) })
const [raidSlug, setRaidSlug] = useQueryState('raid', { const [raidSlug, setRaidSlug] = useQueryState('raid', {
defaultValue: 'all', defaultValue: 'all',
history: 'push',
})
const [recency, setRecency] = useQueryState('recency', {
defaultValue: -1,
history: 'push',
parse: (query: string) => parseInt(query),
serialize: (value) => `${value}`,
}) })
const [recency, setRecency] = useQueryState(
'recency',
queryTypes.integer.withDefault(-1)
)
// Define transformers for element // Define transformers for element
function parseElement(query: string) { function parseElement(query: string) {
@ -95,9 +95,9 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
// Set the initial parties from props // Set the initial parties from props
useEffect(() => { useEffect(() => {
if (props.teams) { if (props.teams) {
setTotalPages(props.teams.total_pages) setTotalPages(props.meta.totalPages)
setRecordCount(props.teams.count) setRecordCount(props.meta.count)
replaceResults(props.teams.count, props.teams.results) replaceResults(props.meta.count, props.teams)
} }
setCurrentPage(1) setCurrentPage(1)
}, []) }, [])
@ -113,6 +113,7 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
if (error.response != null) { if (error.response != null) {
console.error(error) console.error(error)
} else { } else {
// TODO: Put an alert here
console.error('There was an error.') console.error('There was an error.')
} }
}, []) }, [])
@ -132,18 +133,17 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
api.endpoints.users api.endpoints.users
.getOne({ .getOne({
id: username, id: username,
params: { ...filters, ...{ headers: headers } }, params: { ...filters },
}) })
.then((response) => { .then((response) => {
setTotalPages(response.data.parties.total_pages) const results = response.data.profile.parties
setRecordCount(response.data.parties.count) const meta = response.data.meta
if (replace) setTotalPages(meta.total_pages)
replaceResults( setRecordCount(meta.count)
response.data.parties.count,
response.data.parties.results if (replace) replaceResults(meta.count, results)
) else appendResults(results)
else appendResults(response.data.parties.results)
}) })
.catch((error) => handleError(error)) .catch((error) => handleError(error))
} }
@ -166,12 +166,11 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
// Fetch all raids on mount, then find the raid in the URL if present // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
api.endpoints.raids.getAll().then((response) => { api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) setRaids(response.data)
setRaids(cleanRaids)
setRaidsLoading(false) setRaidsLoading(false)
const raid = cleanRaids.find((r) => r.slug === raidSlug) const raid = response.data.find((r: Raid) => r.slug === raidSlug)
setRaid(raid) setRaid(raid)
return raid return raid
@ -202,16 +201,16 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
raidSlug?: string raidSlug?: string
recency?: number recency?: number
}) { }) {
if (element == 0) setElement(0) if (element == 0) setElement(0, { shallow: true })
else if (element) setElement(element) else if (element) setElement(element, { shallow: true })
if (raids && raidSlug) { if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug) const raid = raids.find((raid) => raid.slug === raidSlug)
setRaid(raid) setRaid(raid)
setRaidSlug(raidSlug) setRaidSlug(raidSlug, { shallow: true })
} }
if (recency) setRecency(recency) if (recency) setRecency(recency, { shallow: true })
} }
// Methods: Navigation // Methods: Navigation
@ -283,11 +282,11 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
> >
<div className="UserInfo"> <div className="UserInfo">
<img <img
alt={props.user?.picture.picture} alt={props.user?.avatar.picture}
className={`profile ${props.user?.picture.element}`} className={`profile ${props.user?.avatar.element}`}
srcSet={`/profile/${props.user?.picture.picture}.png, srcSet={`/profile/${props.user?.avatar.picture}.png,
/profile/${props.user?.picture.picture}@2x.png 2x`} /profile/${props.user?.avatar.picture}@2x.png 2x`}
src={`/profile/${props.user?.picture.picture}.png`} src={`/profile/${props.user?.avatar.picture}.png`}
/> />
<h1>{props.user?.username}</h1> <h1>{props.user?.username}</h1>
</div> </div>
@ -331,103 +330,54 @@ export const getServerSidePaths = async () => {
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// Cookies // Set headers for server-side requests
const cookie = getCookie("account", { req, res }) setUserToken(req, res)
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
// Fetch and organize raids
let { raids, sortedRaids } = await api.endpoints.raids let { raids, sortedRaids } = await api.endpoints.raids
.getAll({ params: headers }) .getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid))) .then((response) => organizeRaids(response.data))
// Extract recency filter
const recencyParam: number = parseInt(query.recency)
// Extract element filter
const elementParam: string = query.element
const teamElement: TeamElement | undefined =
elementParam === "all"
? allElement
: elements.find(
(element) => element.name.en.toLowerCase() === elementParam
)
// Extract raid filter
const raidParam: string = query.raid
const raid: Raid | undefined = raids.find((r) => r.slug === raidParam)
// Create filter object // Create filter object
const filters: { const filters: FilterObject = extractFilters(query, raids)
raid?: string const params = {
element?: number params: { ...filters },
recency?: number }
} = {}
if (recencyParam) filters.recency = recencyParam // Set up empty variables
if (teamElement && teamElement.id > -1) filters.element = teamElement.id
if (raid) filters.raid = raid.id
// Fetch initial set of parties here
let user: User | null = null let user: User | null = null
let teams: Party[] | null = null let teams: Party[] | null = null
let meta: PaginationObject = emptyPaginationObject
// Perform a request only if we received a username
if (query.username) { if (query.username) {
const response = await api.endpoints.users.getOne({ const response = await api.endpoints.users.getOne({
id: query.username, id: query.username,
params: { params,
...filters,
},
...headers,
}) })
user = response.data.user // Assign values to pass to props
teams = response.data.parties user = response.data.profile
if (response.data.profile.parties) teams = response.data.profile.parties
else teams = []
meta.count = response.data.meta.count
meta.totalPages = response.data.meta.total_pages
meta.perPage = response.data.meta.per_page
} }
return { return {
props: { props: {
user: user, user: user,
teams: teams, teams: teams,
meta: meta,
raids: raids, raids: raids,
sortedRaids: sortedRaids, sortedRaids: sortedRaids,
...(await serverSideTranslations(locale, ["common"])), ...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }
} }
const organizeRaids = (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)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
return {
raids: raids,
sortedRaids: groupedRaids,
}
}
export default ProfileRoute export default ProfileRoute

View file

@ -7,6 +7,7 @@ import type { AppProps } from 'next/app'
import Layout from '~components/Layout' import Layout from '~components/Layout'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import setUserToken from '~utils/setUserToken'
import '../styles/globals.scss' import '../styles/globals.scss'
@ -15,6 +16,8 @@ function MyApp({ Component, pageProps }: AppProps) {
const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null
useEffect(() => { useEffect(() => {
setUserToken()
if (cookie) { if (cookie) {
console.log(`Logged in as user "${cookieData.username}"`) console.log(`Logged in as user "${cookieData.username}"`)

View file

@ -1,10 +1,11 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { getCookie } from 'cookies-next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from '~components/Party' import Party from '~components/Party'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import organizeRaids from '~utils/organizeRaids'
import setUserToken from '~utils/setUserToken'
import api from '~utils/api' import api from '~utils/api'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
@ -47,67 +48,33 @@ export const getServerSidePaths = async () => {
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// Cookies // Set headers for server-side requests
const cookie = getCookie("account", { req, res }) setUserToken(req, res)
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
let { raids, sortedRaids } = await api.endpoints.raids let { raids, sortedRaids } = await api.endpoints.raids
.getAll({ params: headers }) .getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid))) .then((response) => organizeRaids(response.data))
let jobs = await api.endpoints.jobs let jobs = await api.endpoints.jobs
.getAll({ params: headers }) .getAll()
.then((response) => { return response.data }) .then((response) => {
return response.data
let jobSkills = await api.allSkills(headers) })
.then((response) => { return response.data })
let jobSkills = await api.allJobSkills().then((response) => {
return response.data
})
return { return {
props: { props: {
jobs: jobs, jobs: jobs,
jobSkills: jobSkills, jobSkills: jobSkills,
raids: raids, raids: raids,
sortedRaids: sortedRaids, sortedRaids: sortedRaids,
...(await serverSideTranslations(locale, ["common"])), ...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }
} }
const organizeRaids = (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)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
return {
raids: raids,
sortedRaids: groupedRaids,
}
}
export default NewRoute export default NewRoute

View file

@ -5,6 +5,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from '~components/Party' import Party from '~components/Party'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import organizeRaids from '~utils/organizeRaids'
import api from '~utils/api' import api from '~utils/api'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
@ -55,7 +56,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
let { raids, sortedRaids } = await api.endpoints.raids let { raids, sortedRaids } = await api.endpoints.raids
.getAll() .getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid))) .then((response) => organizeRaids(response.data))
let jobs = await api.endpoints.jobs let jobs = await api.endpoints.jobs
.getAll({ params: headers }) .getAll({ params: headers })
@ -63,9 +64,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
return response.data return response.data
}) })
let jobSkills = await api.allSkills(headers).then((response) => { let jobSkills = await api.allJobSkills(headers).then((response) => response.data)
return response.data
})
let party: Party | null = null let party: Party | null = null
if (query.party) { if (query.party) {
@ -88,34 +87,4 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
} }
} }
const organizeRaids = (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)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
return {
raids: raids,
sortedRaids: groupedRaids,
}
}
export default PartyRoute export default PartyRoute

View file

@ -1,7 +1,6 @@
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 { getCookie } from 'cookies-next'
import { queryTypes, useQueryState } from 'next-usequerystate' 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'
@ -11,31 +10,28 @@ 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 setUserToken from '~utils/setUserToken'
import extractFilters from '~utils/extractFilters'
import organizeRaids from '~utils/organizeRaids'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~utils/Element'
import { emptyPaginationObject } from '~utils/emptyStates'
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'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import type { FilterObject, PaginationObject } from '~types'
interface Props { interface Props {
teams?: { count: number; total_pages: number; results: Party[] } teams?: Party[]
meta: PaginationObject
raids: Raid[] raids: Raid[]
sortedRaids: Raid[][] sortedRaids: Raid[][]
} }
const SavedRoute: React.FC<Props> = (props: Props) => { const SavedRoute: React.FC<Props> = (props: Props) => {
// Set up cookies
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {}
// Set up router // Set up router
const router = useRouter() const router = useRouter()
@ -60,16 +56,20 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
// Recency is in seconds // Recency is in seconds
const [element, setElement] = useQueryState('element', { const [element, setElement] = useQueryState('element', {
defaultValue: -1, defaultValue: -1,
history: 'push',
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value), serialize: (value) => serializeElement(value),
}) })
const [raidSlug, setRaidSlug] = useQueryState('raid', { const [raidSlug, setRaidSlug] = useQueryState('raid', {
defaultValue: 'all', defaultValue: 'all',
history: 'push',
})
const [recency, setRecency] = useQueryState('recency', {
defaultValue: -1,
history: 'push',
parse: (query: string) => parseInt(query),
serialize: (value) => `${value}`,
}) })
const [recency, setRecency] = useQueryState(
'recency',
queryTypes.integer.withDefault(-1)
)
// Define transformers for element // Define transformers for element
function parseElement(query: string) { function parseElement(query: string) {
@ -94,9 +94,9 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
// Set the initial parties from props // Set the initial parties from props
useEffect(() => { useEffect(() => {
if (props.teams) { if (props.teams) {
setTotalPages(props.teams.total_pages) setTotalPages(props.meta.totalPages)
setRecordCount(props.teams.count) setRecordCount(props.meta.count)
replaceResults(props.teams.count, props.teams.results) replaceResults(props.meta.count, props.teams)
} }
setCurrentPage(1) setCurrentPage(1)
}, []) }, [])
@ -118,24 +118,36 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
const fetchTeams = useCallback( const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => { ({ replace }: { replace: boolean }) => {
const filters = { const filters: {
[key: string]: any
} = {
element: element !== -1 ? element : undefined,
raid: raid ? raid.id : undefined,
recency: recency !== -1 ? recency : undefined,
page: currentPage,
}
Object.keys(filters).forEach(
(key) => filters[key] === undefined && delete filters[key]
)
const params = {
params: { params: {
element: element != -1 ? element : undefined, ...filters,
raid: raid ? raid.id : undefined,
recency: recency != -1 ? recency : undefined,
page: currentPage,
}, },
} }
api api
.savedTeams({ ...filters, ...{ headers: headers } }) .savedTeams(params)
.then((response) => { .then((response) => {
setTotalPages(response.data.total_pages) const results = response.data.results
setRecordCount(response.data.count) const meta = response.data.meta
if (replace) setTotalPages(meta.total_pages)
replaceResults(response.data.count, response.data.results) setRecordCount(meta.count)
else appendResults(response.data.results)
if (replace) replaceResults(meta.count, results)
else appendResults(results)
}) })
.catch((error) => handleError(error)) .catch((error) => handleError(error))
}, },
@ -157,12 +169,11 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
// Fetch all raids on mount, then find the raid in the URL if present // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
api.endpoints.raids.getAll().then((response) => { api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) setRaids(response.data)
setRaids(cleanRaids)
setRaidsLoading(false) setRaidsLoading(false)
const raid = cleanRaids.find((r) => r.slug === raidSlug) const raid = response.data.find((r: Raid) => r.slug === raidSlug)
setRaid(raid) setRaid(raid)
return raid return raid
@ -193,16 +204,16 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
raidSlug?: string raidSlug?: string
recency?: number recency?: number
}) { }) {
if (element == 0) setElement(0) if (element == 0) setElement(0, { shallow: true })
else if (element) setElement(element) else if (element) setElement(element, { shallow: true })
if (raids && raidSlug) { if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug) const raid = raids.find((raid) => raid.slug === raidSlug)
setRaid(raid) setRaid(raid)
setRaidSlug(raidSlug) setRaidSlug(raidSlug, { shallow: true })
} }
if (recency) setRecency(recency) if (recency) setRecency(recency, { shallow: true })
} }
// Methods: Favorites // Methods: Favorites
@ -212,7 +223,7 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
} }
function saveFavorite(teamId: string) { function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId, params: headers }).then((response) => { api.saveTeam({ id: teamId }).then((response) => {
if (response.status == 201) { if (response.status == 201) {
const index = parties.findIndex((p) => p.id === teamId) const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index] const party = parties[index]
@ -228,7 +239,7 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
} }
function unsaveFavorite(teamId: string) { function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId, params: headers }).then((response) => { api.unsaveTeam({ id: teamId }).then((response) => {
if (response.status == 200) { if (response.status == 200) {
const index = parties.findIndex((p) => p.id === teamId) const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index] const party = parties[index]
@ -336,94 +347,43 @@ export const getServerSidePaths = async () => {
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// Cookies // Set headers for server-side requests
const cookie = getCookie("account", { req, res }) setUserToken(req, res)
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData // Fetch and organize raids
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
let { raids, sortedRaids } = await api.endpoints.raids let { raids, sortedRaids } = await api.endpoints.raids
.getAll({ params: headers }) .getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid))) .then((response) => organizeRaids(response.data))
// Extract recency filter
const recencyParam: number = parseInt(query.recency)
// Extract element filter
const elementParam: string = query.element
const teamElement: TeamElement | undefined =
elementParam === "all"
? allElement
: elements.find(
(element) => element.name.en.toLowerCase() === elementParam
)
// Extract raid filter
const raidParam: string = query.raid
const raid: Raid | undefined = raids.find((r) => r.slug === raidParam)
// Create filter object // Create filter object
const filters: { const filters: FilterObject = extractFilters(query, raids)
raid?: string const params = {
element?: number params: { ...filters },
recency?: number }
} = {}
if (recencyParam) filters.recency = recencyParam // Set up empty variables
if (teamElement && teamElement.id > -1) filters.element = teamElement.id let teams: Party[] | null = null
if (raid) filters.raid = raid.id let meta: PaginationObject = emptyPaginationObject
// Fetch initial set of parties here // Fetch initial set of saved parties
const response = await api.savedTeams({ const response = await api.savedTeams(params)
params: {
...filters, // Assign values to pass to props
}, teams = response.data.results
...headers meta.count = response.data.meta.count
}) meta.totalPages = response.data.meta.total_pages
meta.perPage = response.data.meta.per_page
return { return {
props: { props: {
teams: response.data, teams: teams,
meta: meta,
raids: raids, raids: raids,
sortedRaids: sortedRaids, sortedRaids: sortedRaids,
...(await serverSideTranslations(locale, ["common"])), ...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }
} }
const organizeRaids = (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)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
return {
raids: raids,
sortedRaids: groupedRaids,
}
}
export default SavedRoute export default SavedRoute

View file

@ -1,7 +1,6 @@
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 { getCookie } from 'cookies-next'
import { queryTypes, useQueryState } from 'next-usequerystate' 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'
@ -11,31 +10,27 @@ 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 setUserToken from '~utils/setUserToken'
import extractFilters from '~utils/extractFilters'
import organizeRaids from '~utils/organizeRaids'
import useDidMountEffect from '~utils/useDidMountEffect' import useDidMountEffect from '~utils/useDidMountEffect'
import { elements, allElement } from '~utils/Element' import { elements, allElement } from '~utils/Element'
import { emptyPaginationObject } from '~utils/emptyStates'
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'
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import type { FilterObject, PaginationObject } from '~types'
interface Props { interface Props {
teams?: { count: number; total_pages: number; results: Party[] } teams?: Party[]
raids: Raid[] meta: PaginationObject
sortedRaids: Raid[][] sortedRaids: Raid[][]
} }
const TeamsRoute: React.FC<Props> = (props: Props) => { const TeamsRoute: React.FC<Props> = (props: Props) => {
// Set up cookies
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {}
// Set up router // Set up router
const router = useRouter() const router = useRouter()
@ -60,16 +55,20 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
// Recency is in seconds // Recency is in seconds
const [element, setElement] = useQueryState('element', { const [element, setElement] = useQueryState('element', {
defaultValue: -1, defaultValue: -1,
history: 'push',
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: (value) => serializeElement(value), serialize: (value) => serializeElement(value),
}) })
const [raidSlug, setRaidSlug] = useQueryState('raid', { const [raidSlug, setRaidSlug] = useQueryState('raid', {
defaultValue: 'all', defaultValue: 'all',
history: 'push',
})
const [recency, setRecency] = useQueryState('recency', {
defaultValue: -1,
history: 'push',
parse: (query: string) => parseInt(query),
serialize: (value) => `${value}`,
}) })
const [recency, setRecency] = useQueryState(
'recency',
queryTypes.integer.withDefault(-1)
)
// Define transformers for element // Define transformers for element
function parseElement(query: string) { function parseElement(query: string) {
@ -94,9 +93,9 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
// Set the initial parties from props // Set the initial parties from props
useEffect(() => { useEffect(() => {
if (props.teams) { if (props.teams) {
setTotalPages(props.teams.total_pages) setTotalPages(props.meta.totalPages)
setRecordCount(props.teams.count) setRecordCount(props.meta.count)
replaceResults(props.teams.count, props.teams.results) replaceResults(props.meta.count, props.teams)
} }
setCurrentPage(1) setCurrentPage(1)
}, []) }, [])
@ -118,24 +117,36 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
const fetchTeams = useCallback( const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => { ({ replace }: { replace: boolean }) => {
const filters = { const filters: {
[key: string]: any
} = {
element: element !== -1 ? element : undefined,
raid: raid ? raid.id : undefined,
recency: recency !== -1 ? recency : undefined,
page: currentPage,
}
Object.keys(filters).forEach(
(key) => filters[key] === undefined && delete filters[key]
)
const params = {
params: { params: {
element: element != -1 ? element : undefined, ...filters,
raid: raid ? raid.id : undefined,
recency: recency != -1 ? recency : undefined,
page: currentPage,
}, },
} }
api.endpoints.parties api.endpoints.parties
.getAll({ ...filters, ...{ headers: headers } }) .getAll(params)
.then((response) => { .then((response) => {
setTotalPages(response.data.total_pages) const results = response.data.results
setRecordCount(response.data.count) const meta = response.data.meta
if (replace) setTotalPages(meta.total_pages)
replaceResults(response.data.count, response.data.results) setRecordCount(meta.count)
else appendResults(response.data.results)
if (replace) replaceResults(meta.count, results)
else appendResults(results)
}) })
.catch((error) => handleError(error)) .catch((error) => handleError(error))
}, },
@ -157,12 +168,11 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
// Fetch all raids on mount, then find the raid in the URL if present // Fetch all raids on mount, then find the raid in the URL if present
useEffect(() => { useEffect(() => {
api.endpoints.raids.getAll().then((response) => { api.endpoints.raids.getAll().then((response) => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) setRaids(response.data)
setRaids(cleanRaids)
setRaidsLoading(false) setRaidsLoading(false)
const raid = cleanRaids.find((r) => r.slug === raidSlug) const raid = response.data.find((r: Raid) => r.slug === raidSlug)
setRaid(raid) setRaid(raid)
return raid return raid
@ -193,16 +203,16 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
raidSlug?: string raidSlug?: string
recency?: number recency?: number
}) { }) {
if (element == 0) setElement(0) if (element == 0) setElement(0, { shallow: true })
else if (element) setElement(element) else if (element) setElement(element, { shallow: true })
if (raids && raidSlug) { if (raids && raidSlug) {
const raid = raids.find((raid) => raid.slug === raidSlug) const raid = raids.find((raid) => raid.slug === raidSlug)
setRaid(raid) setRaid(raid)
setRaidSlug(raidSlug) setRaidSlug(raidSlug, { shallow: true })
} }
if (recency) setRecency(recency) if (recency) setRecency(recency, { shallow: true })
} }
// Methods: Favorites // Methods: Favorites
@ -212,7 +222,7 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
} }
function saveFavorite(teamId: string) { function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId, params: headers }).then((response) => { api.saveTeam({ id: teamId }).then((response) => {
if (response.status == 201) { if (response.status == 201) {
const index = parties.findIndex((p) => p.id === teamId) const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index] const party = parties[index]
@ -228,7 +238,7 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
} }
function unsaveFavorite(teamId: string) { function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId, params: headers }).then((response) => { api.unsaveTeam({ id: teamId }).then((response) => {
if (response.status == 200) { if (response.status == 200) {
const index = parties.findIndex((p) => p.id === teamId) const index = parties.findIndex((p) => p.id === teamId)
const party = parties[index] const party = parties[index]
@ -344,94 +354,43 @@ export const getServerSidePaths = async () => {
// prettier-ignore // prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => { export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// Cookies // Set headers for server-side requests
const cookie = getCookie("account", { req, res }) setUserToken(req, res)
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
// Fetch and organize raids
let { raids, sortedRaids } = await api.endpoints.raids let { raids, sortedRaids } = await api.endpoints.raids
.getAll({ params: headers }) .getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid))) .then((response) => organizeRaids(response.data))
// Extract recency filter
const recencyParam: number = parseInt(query.recency)
// Extract element filter
const elementParam: string = query.element
const teamElement: TeamElement | undefined =
elementParam === "all"
? allElement
: elements.find(
(element) => element.name.en.toLowerCase() === elementParam
)
// Extract raid filter
const raidParam: string = query.raid
const raid: Raid | undefined = raids.find((r) => r.slug === raidParam)
// Create filter object // Create filter object
const filters: { const filters: FilterObject = extractFilters(query, raids)
raid?: string const params = {
element?: number params: { ...filters },
recency?: number }
} = {}
if (recencyParam) filters.recency = recencyParam // Set up empty variables
if (teamElement && teamElement.id > -1) filters.element = teamElement.id let teams: Party[] | null = null
if (raid) filters.raid = raid.id let meta: PaginationObject = emptyPaginationObject
// Fetch initial set of parties here // Fetch initial set of parties
const response = await api.endpoints.parties.getAll({ const response = await api.endpoints.parties.getAll(params)
params: {
...filters, // Assign values to pass to props
}, teams = response.data.results
...headers, meta.count = response.data.meta.count
}) meta.totalPages = response.data.meta.total_pages
meta.perPage = response.data.meta.per_page
return { return {
props: { props: {
teams: response.data, teams: teams,
meta: meta,
raids: raids, raids: raids,
sortedRaids: sortedRaids, sortedRaids: sortedRaids,
...(await serverSideTranslations(locale, ["common"])), ...(await serverSideTranslations(locale, ['common'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }
} }
const organizeRaids = (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)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
return {
raids: raids,
sortedRaids: groupedRaids,
}
}
export default TeamsRoute export default TeamsRoute

2
types/User.d.ts vendored
View file

@ -2,7 +2,7 @@ interface User {
id: string id: string
username: string username: string
granblueId: number granblueId: number
picture: { avatar: {
picture: string picture: string
element: string element: string
} }

12
types/index.d.ts vendored
View file

@ -7,3 +7,15 @@ export type JobSkillObject = {
2: JobSkill | undefined 2: JobSkill | undefined
3: JobSkill | undefined 3: JobSkill | undefined
} }
export type FilterObject = {
raid?: string
element?: number
recency?: number
}
export type PaginationObject = {
count: number
totalPages: number
perPage: number
}

View file

@ -104,11 +104,16 @@ class Api {
return axios.put(resourceUrl, params) return axios.put(resourceUrl, params)
} }
allSkills(params: {}) { allJobSkills(params?: {}) {
const resourceUrl = `${this.url}/jobs/skills` const resourceUrl = `${this.url}/jobs/skills`
return axios.get(resourceUrl, params) return axios.get(resourceUrl, params)
} }
jobSkillsForJob(jobId: string, params?: {}) {
const resourceUrl = `${this.url}/jobs/${jobId}/skills`
return axios.get(resourceUrl, params)
}
savedTeams(params: {}) { savedTeams(params: {}) {
const resourceUrl = `${this.url}/parties/favorites` const resourceUrl = `${this.url}/parties/favorites`
return axios.get(resourceUrl, params) return axios.get(resourceUrl, params)

View file

@ -185,3 +185,9 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
checked: false, checked: false,
}, },
} }
export const emptyPaginationObject = {
count: 0,
totalPages: 0,
perPage: 15,
}

26
utils/extractFilters.tsx Normal file
View file

@ -0,0 +1,26 @@
import { elements, allElement } from '~utils/Element'
export default (query: { [index: string]: string }, raids: Raid[]) => {
// Extract recency filter
const recencyParam: number = parseInt(query.recency)
// Extract element filter
const elementParam: string = query.element
const teamElement: TeamElement | undefined =
elementParam === 'all'
? allElement
: elements.find(
(element) => element.name.en.toLowerCase() === elementParam
)
// Extract raid filter
const raidParam: string = query.raid
const raid: Raid | undefined = raids.find((r) => r.slug === raidParam)
// Return filter object
return {
recency: recencyParam && recencyParam !== -1 ? recencyParam : undefined,
element: teamElement && teamElement.id > -1 ? teamElement.id : undefined,
raid: raid ? raid.id : undefined,
}
}

16
utils/organizeRaids.tsx Normal file
View file

@ -0,0 +1,16 @@
export default (raids: Raid[]) => {
const numGroups = Math.max.apply(
Math,
raids.map((raid) => raid.group)
)
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter((raid) => raid.group == i)
}
return {
raids: raids,
sortedRaids: groupedRaids,
}
}

19
utils/setUserToken.tsx Normal file
View file

@ -0,0 +1,19 @@
import axios from 'axios'
import { getCookie } from 'cookies-next'
import type { NextApiRequest, NextApiResponse } from 'next'
export default (
req: NextApiRequest | undefined = undefined,
res: NextApiResponse | undefined = undefined
) => {
// Set up cookies
const options = req && res ? { req, res } : {}
const cookie = getCookie('account', options)
if (cookie) {
axios.defaults.headers.common['Authorization'] = `Bearer ${
JSON.parse(cookie as string).token
}`
} else {
delete axios.defaults.headers.common['Authorization']
}
}