Merge pull request #48 from jedmund/blueprinter

Support for Blueprinter migration
This commit is contained in:
Justin Edmund 2022-12-23 16:21:25 -08:00 committed by GitHub
commit c44651015e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 654 additions and 787 deletions

View file

@ -24,18 +24,6 @@ const AccountModal = () => {
const locale =
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
const [open, setOpen] = useState(false)
const [picture, setPicture] = useState('')
@ -171,12 +159,16 @@ const AccountModal = () => {
pictureData.find((i) => i.filename === picture)?.element
}`}
>
<img
alt="Profile preview"
srcSet={`/profile/${picture}.png,
{picture ? (
<img
alt="Profile preview"
srcSet={`/profile/${picture}.png,
/profile/${picture}@2x.png 2x`}
src={`/profile/${picture}.png`}
/>
src={`/profile/${picture}.png`}
/>
) : (
''
)}
</div>
<select

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,11 +32,6 @@ const FilterBar = (props: Props) => {
const [recencyOpen, setRecencyOpen] = 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
const classes = classNames({
FilterBar: true,
@ -69,10 +64,9 @@ const FilterBar = (props: Props) => {
<div className={classes}>
{props.children}
<Select
defaultValue={-1}
trigger={'All elements'}
value={`${props.element}`}
open={elementOpen}
onChange={elementSelectChanged}
onValueChange={elementSelectChanged}
onClick={openElementSelect}
>
<SelectItem data-element="all" key={-1} value={-1}>
@ -106,14 +100,13 @@ const FilterBar = (props: Props) => {
defaultRaid="all"
showAllRaidsOption={true}
onChange={raidSelectChanged}
ref={raidSelect}
/>
<Select
defaultValue={-1}
value={`${props.recency}`}
trigger={'All time'}
open={recencyOpen}
onChange={recencySelectChanged}
onValueChange={recencySelectChanged}
onClick={openRecencySelect}
>
<SelectItem key={-1} value={-1}>

View file

@ -79,7 +79,7 @@ const GridRep = (props: Props) => {
let url = ''
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`
} else {
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`
@ -164,7 +164,7 @@ const GridRep = (props: Props) => {
!props.user) ? (
<Button
className="Save"
accessoryIcon={<SaveIcon class="stroke" />}
accessoryIcon={<SaveIcon className="stroke" />}
active={props.favorited}
contained={true}
buttonSize="small"

View file

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

View file

@ -16,11 +16,12 @@ const Input = React.forwardRef<HTMLInputElement, Props>(function input(
forwardedRef
) {
const classes = classNames({ Input: true }, props.className)
const { value, ...inputProps } = props
return (
<label className="Label" htmlFor={props.name}>
<input
{...props}
{...inputProps}
autoComplete="off"
className={classes}
defaultValue={props.value || ''}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,18 +25,6 @@ interface 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
const router = useRouter()
@ -55,12 +43,13 @@ const Party = (props: Props) => {
async function createParty(extra: boolean = false) {
let body = {
party: {
...(accountData && { user_id: accountData.userId }),
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
@ -68,13 +57,9 @@ const Party = (props: Props) => {
appState.party.extra = event.target.checked
if (party.id) {
api.endpoints.parties.update(
party.id,
{
party: { extra: event.target.checked },
},
headers
)
api.endpoints.parties.update(party.id, {
party: { extra: event.target.checked },
})
}
}
@ -86,17 +71,13 @@ const Party = (props: Props) => {
) {
if (appState.party.id)
api.endpoints.parties
.update(
appState.party.id,
{
party: {
name: name,
description: description,
raid_id: raid?.id,
},
.update(appState.party.id, {
party: {
name: name,
description: description,
raid_id: raid?.id,
},
headers
)
})
.then(() => {
appState.party.name = name
appState.party.description = description
@ -110,7 +91,7 @@ const Party = (props: Props) => {
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (appState.party.editable && appState.party.id) {
api.endpoints.parties
.destroy({ id: appState.party.id, params: headers })
.destroy({ id: appState.party.id })
.then(() => {
// Push to route
router.push('/')
@ -131,26 +112,28 @@ const Party = (props: Props) => {
}
// Methods: Storing party data
const storeParty = function (party: Party) {
const storeParty = function (team: Party) {
// Store the important party and state-keeping values
appState.party.name = party.name
appState.party.description = party.description
appState.party.raid = party.raid
appState.party.updated_at = party.updated_at
appState.party.job = party.job
appState.party.jobSkills = party.job_skills
appState.party.name = team.name
appState.party.description = team.description
appState.party.raid = team.raid
appState.party.updated_at = team.updated_at
appState.party.job = team.job
appState.party.jobSkills = team.job_skills
appState.party.id = party.id
appState.party.extra = party.extra
appState.party.user = party.user
appState.party.favorited = party.favorited
appState.party.created_at = party.created_at
appState.party.updated_at = party.updated_at
appState.party.id = team.id
appState.party.extra = team.extra
appState.party.user = team.user
appState.party.favorited = team.favorited
appState.party.created_at = team.created_at
appState.party.updated_at = team.updated_at
appState.party.detailsVisible = false
// Populate state
storeCharacters(party.characters)
storeWeapons(party.weapons)
storeSummons(party.summons)
storeCharacters(team.characters)
storeWeapons(team.weapons)
storeSummons(team.summons)
}
const storeCharacters = (list: Array<GridCharacter>) => {
@ -256,13 +239,11 @@ const Party = (props: Props) => {
<React.Fragment>
{navigation}
<section id="Party">{currentGrid()}</section>
{
<PartyDetails
editable={party.editable}
updateCallback={updateDetails}
deleteCallback={deleteTeam}
/>
}
<PartyDetails
editable={party.editable}
updateCallback={updateDetails}
deleteCallback={deleteTeam}
/>
</React.Fragment>
)
}

View file

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

View file

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

View file

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

View file

@ -22,6 +22,14 @@
text-align: left;
}
&.Bound {
background-color: var(--select-contained-bg);
&:hover {
background-color: var(--select-contained-bg-hover);
}
}
.SelectIcon {
display: flex;
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 classNames from 'classnames'
@ -7,25 +7,37 @@ import ArrowIcon from '~public/icons/Arrow.svg'
import './index.scss'
// Props
interface Props {
interface Props
extends React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> {
open: boolean
defaultValue?: string | number
placeholder?: string
trigger?: React.ReactNode
children?: React.ReactNode
onClick?: () => void
onChange?: (value: string) => void
onValueChange?: (value: string) => void
triggerClass?: string
}
const Select = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(
props,
ref
) {
const Select = (props: Props) => {
const [value, setValue] = useState('')
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 (
<RadixSelect.Root
defaultValue={props.defaultValue as string}
onValueChange={props.onChange}
value={value !== '' ? value : undefined}
onValueChange={onValueChange}
>
<RadixSelect.Trigger
className={classNames('SelectTrigger', props.triggerClass)}
@ -50,6 +62,6 @@ const Select = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(
</RadixSelect.Portal>
</RadixSelect.Root>
)
})
}
export default Select

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from '~components/Party'
import { appState } from '~utils/appState'
import organizeRaids from '~utils/organizeRaids'
import api from '~utils/api'
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
.getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid)))
.then((response) => organizeRaids(response.data))
let jobs = await api.endpoints.jobs
.getAll({ params: headers })
@ -63,9 +64,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
return response.data
})
let jobSkills = await api.allSkills(headers).then((response) => {
return response.data
})
let jobSkills = await api.allJobSkills(headers).then((response) => response.data)
let party: Party | null = null
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

View file

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

View file

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

2
types/User.d.ts vendored
View file

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

12
types/index.d.ts vendored
View file

@ -7,3 +7,15 @@ export type JobSkillObject = {
2: 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)
}
allSkills(params: {}) {
allJobSkills(params?: {}) {
const resourceUrl = `${this.url}/jobs/skills`
return axios.get(resourceUrl, params)
}
jobSkillsForJob(jobId: string, params?: {}) {
const resourceUrl = `${this.url}/jobs/${jobId}/skills`
return axios.get(resourceUrl, params)
}
savedTeams(params: {}) {
const resourceUrl = `${this.url}/parties/favorites`
return axios.get(resourceUrl, params)

View file

@ -185,3 +185,9 @@ export const emptyWeaponSeriesState: WeaponSeriesState = {
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']
}
}