Merge pull request #43 from jedmund/ssr-refactor

Refactor app to use server-side rendering
This commit is contained in:
Justin Edmund 2022-11-16 06:10:35 -08:00 committed by GitHub
commit 56f61839de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 4899 additions and 4334 deletions

View file

@ -1,41 +1,45 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie } from "cookies-next"
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"
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from "@radix-ui/react-dialog"
import * as Switch from '@radix-ui/react-switch' import * as Switch from "@radix-ui/react-switch"
import api from '~utils/api' import api from "~utils/api"
import { accountState } from '~utils/accountState' import { accountState } from "~utils/accountState"
import { pictureData } from '~utils/pictureData' import { pictureData } from "~utils/pictureData"
import Button from '~components/Button' import Button from "~components/Button"
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg"
import './index.scss' import "./index.scss"
const AccountModal = () => { const AccountModal = () => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation("common")
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
// Cookies // Cookies
const [cookies, setCookies] = useCookies() const cookie = getCookie("account")
const headers = (cookies.account != null) ? { const headers = {}
headers: { // cookies.account != null
'Authorization': `Bearer ${cookies.account.access_token}` // ? {
} // 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("")
const [language, setLanguage] = useState('') const [language, setLanguage] = useState("")
const [gender, setGender] = useState(0) const [gender, setGender] = useState(0)
const [privateProfile, setPrivateProfile] = useState(false) const [privateProfile, setPrivateProfile] = useState(false)
@ -45,33 +49,32 @@ const AccountModal = () => {
const genderSelect = React.createRef<HTMLSelectElement>() const genderSelect = React.createRef<HTMLSelectElement>()
const privateSelect = React.createRef<HTMLInputElement>() const privateSelect = React.createRef<HTMLInputElement>()
useEffect(() => { // useEffect(() => {
if (cookies.user) setPicture(cookies.user.picture) // if (cookies.user) setPicture(cookies.user.picture)
if (cookies.user) setLanguage(cookies.user.language) // if (cookies.user) setLanguage(cookies.user.language)
if (cookies.user) setGender(cookies.user.gender) // if (cookies.user) setGender(cookies.user.gender)
}, [cookies]) // }, [cookies])
const pictureOptions = ( const pictureOptions = pictureData
pictureData.sort((a, b) => (a.name.en > b.name.en) ? 1 : -1).map((item, i) => { .sort((a, b) => (a.name.en > b.name.en ? 1 : -1))
.map((item, i) => {
return ( return (
<option key={`picture-${i}`} value={item.filename}>{item.name[locale]}</option> <option key={`picture-${i}`} value={item.filename}>
{item.name[locale]}
</option>
) )
}) })
)
function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) { function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (pictureSelect.current) if (pictureSelect.current) setPicture(pictureSelect.current.value)
setPicture(pictureSelect.current.value)
} }
function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (languageSelect.current) if (languageSelect.current) setLanguage(languageSelect.current.value)
setLanguage(languageSelect.current.value)
} }
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) { function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
if (genderSelect.current) if (genderSelect.current) setGender(parseInt(genderSelect.current.value))
setGender(parseInt(genderSelect.current.value))
} }
function handlePrivateChange(checked: boolean) { function handlePrivateChange(checked: boolean) {
@ -84,44 +87,45 @@ const AccountModal = () => {
const object = { const object = {
user: { user: {
picture: picture, picture: picture,
element: pictureData.find(i => i.filename === picture)?.element, element: pictureData.find((i) => i.filename === picture)?.element,
language: language, language: language,
gender: gender, gender: gender,
private: privateProfile private: privateProfile,
} },
} }
api.endpoints.users.update(cookies.account.user_id, object, headers) // api.endpoints.users
.then(response => { // .update(cookies.account.user_id, object, headers)
const user = response.data.user // .then((response) => {
// const user = response.data.user
const cookieObj = { // const cookieObj = {
picture: user.picture.picture, // picture: user.picture.picture,
element: user.picture.element, // element: user.picture.element,
gender: user.gender, // gender: user.gender,
language: user.language // language: user.language,
} // }
setCookies('user', cookieObj, { path: '/'}) // setCookies("user", cookieObj, { path: "/" })
accountState.account.user = { // accountState.account.user = {
id: user.id, // id: user.id,
username: user.username, // username: user.username,
picture: user.picture.picture, // picture: user.picture.picture,
element: user.picture.element, // element: user.picture.element,
gender: user.gender // gender: user.gender,
} // }
setOpen(false) // setOpen(false)
changeLanguage(user.language) // changeLanguage(user.language)
}) // })
} }
function changeLanguage(newLanguage: string) { function changeLanguage(newLanguage: string) {
if (newLanguage !== router.locale) { // if (newLanguage !== router.locale) {
setCookies('NEXT_LOCALE', newLanguage, { path: '/'}) // setCookies("NEXT_LOCALE", newLanguage, { path: "/" })
router.push(router.asPath, undefined, { locale: newLanguage }) // router.push(router.asPath, undefined, { locale: newLanguage })
} // }
} }
function openChange(open: boolean) { function openChange(open: boolean) {
@ -132,15 +136,22 @@ const AccountModal = () => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('menu.settings')}</span> <span>{t("menu.settings")}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Account Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
className="Account Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader"> <div className="DialogHeader">
<div className="DialogTop"> <div className="DialogTop">
<Dialog.Title className="SubTitle">{t('modals.settings.title')}</Dialog.Title> <Dialog.Title className="SubTitle">
<Dialog.Title className="DialogTitle">@{account.user?.username}</Dialog.Title> {t("modals.settings.title")}
</Dialog.Title>
<Dialog.Title className="DialogTitle">
@{account.user?.username}
</Dialog.Title>
</div> </div>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
@ -152,10 +163,14 @@ const AccountModal = () => {
<form onSubmit={update}> <form onSubmit={update}>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t('modals.settings.labels.picture')}</label> <label>{t("modals.settings.labels.picture")}</label>
</div> </div>
<div className={`preview ${pictureData.find(i => i.filename === picture)?.element}`}> <div
className={`preview ${
pictureData.find((i) => i.filename === picture)?.element
}`}
>
<img <img
alt="Profile preview" alt="Profile preview"
srcSet={`/profile/${picture}.png, srcSet={`/profile/${picture}.png,
@ -164,42 +179,71 @@ const AccountModal = () => {
/> />
</div> </div>
<select name="picture" onChange={handlePictureChange} value={picture} ref={pictureSelect}> <select
name="picture"
onChange={handlePictureChange}
value={picture}
ref={pictureSelect}
>
{pictureOptions} {pictureOptions}
</select> </select>
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t('modals.settings.labels.gender')}</label> <label>{t("modals.settings.labels.gender")}</label>
</div> </div>
<select name="gender" onChange={handleGenderChange} value={gender} ref={genderSelect}> <select
<option key="gran" value="0">{t('modals.settings.gender.gran')}</option> name="gender"
<option key="djeeta" value="1">{t('modals.settings.gender.djeeta')}</option> onChange={handleGenderChange}
value={gender}
ref={genderSelect}
>
<option key="gran" value="0">
{t("modals.settings.gender.gran")}
</option>
<option key="djeeta" value="1">
{t("modals.settings.gender.djeeta")}
</option>
</select> </select>
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t('modals.settings.labels.language')}</label> <label>{t("modals.settings.labels.language")}</label>
</div> </div>
<select name="language" onChange={handleLanguageChange} value={language} ref={languageSelect}> <select
<option key="en" value="en">{t('modals.settings.language.english')}</option> name="language"
<option key="jp" value="ja">{t('modals.settings.language.japanese')}</option> onChange={handleLanguageChange}
value={language}
ref={languageSelect}
>
<option key="en" value="en">
{t("modals.settings.language.english")}
</option>
<option key="jp" value="ja">
{t("modals.settings.language.japanese")}
</option>
</select> </select>
</div> </div>
<div className="field"> <div className="field">
<div className="left"> <div className="left">
<label>{t('modals.settings.labels.private')}</label> <label>{t("modals.settings.labels.private")}</label>
<p className={locale}>{t('modals.settings.descriptions.private')}</p> <p className={locale}>
{t("modals.settings.descriptions.private")}
</p>
</div> </div>
<Switch.Root className="Switch" onCheckedChange={handlePrivateChange} checked={privateProfile}> <Switch.Root
className="Switch"
onCheckedChange={handlePrivateChange}
checked={privateProfile}
>
<Switch.Thumb className="Thumb" /> <Switch.Thumb className="Thumb" />
</Switch.Root> </Switch.Root>
</div> </div>
<Button>{t('modals.settings.buttons.confirm')}</Button> <Button>{t("modals.settings.buttons.confirm")}</Button>
</form> </form>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,23 +1,23 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie } from "cookies-next"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import { AxiosResponse } from 'axios' import { AxiosResponse } from "axios"
import debounce from 'lodash.debounce' import debounce from "lodash.debounce"
import JobSection from '~components/JobSection' import JobSection from "~components/JobSection"
import CharacterUnit from '~components/CharacterUnit' import CharacterUnit from "~components/CharacterUnit"
import api from '~utils/api' import api from "~utils/api"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import './index.scss' import "./index.scss"
// Props // Props
interface Props { interface Props {
new: boolean new: boolean
slug?: string characters?: GridCharacter[]
createParty: () => Promise<AxiosResponse<any, any>> createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void pushHistory?: (path: string) => void
} }
@ -27,127 +27,85 @@ const CharacterGrid = (props: Props) => {
const numCharacters: number = 5 const numCharacters: number = 5
// Cookies // Cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account != null) ? { const accountData: AccountCookie = cookie
headers: { ? JSON.parse(cookie as string)
'Authorization': `Bearer ${cookies.account.access_token}` : 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)
const [slug, setSlug] = useState() const [slug, setSlug] = useState()
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
const [firstLoadComplete, setFirstLoadComplete] = useState(false)
// Create a temporary state to store previous character uncap values // Create a temporary state to store previous character uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) const [previousUncapValues, setPreviousUncapValues] = useState<{
[key: number]: number
// Fetch data from the server }>({})
useEffect(() => {
const shortcode = (props.slug) ? props.slug : slug
if (shortcode) fetchGrid(shortcode)
else appState.party.editable = true
}, [slug, props.slug])
// Set the editable flag only on first load // Set the editable flag only on first load
useEffect(() => { useEffect(() => {
if (!loading && !firstLoadComplete) {
// If user is logged in and matches // If user is logged in and matches
if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new) if (
(accountData && party.user && accountData.userId === party.user.id) ||
props.new
)
appState.party.editable = true appState.party.editable = true
else else appState.party.editable = false
appState.party.editable = false }, [props.new, accountData, party])
setFirstLoadComplete(true)
}
}, [props.new, cookies, party, loading, firstLoadComplete])
// Initialize an array of current uncap values for each characters // Initialize an array of current uncap values for each characters
useEffect(() => { useEffect(() => {
let initialPreviousUncapValues: {[key: number]: number} = {} let initialPreviousUncapValues: { [key: number]: number } = {}
Object.values(appState.grid.characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) Object.values(appState.grid.characters).map(
(o) => (initialPreviousUncapValues[o.position] = o.uncap_level)
)
setPreviousUncapValues(initialPreviousUncapValues) setPreviousUncapValues(initialPreviousUncapValues)
}, [appState.grid.characters]) }, [appState.grid.characters])
// Methods: Fetching an object from the server
async function fetchGrid(shortcode: string) {
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'characters', params: headers })
.then(response => processResult(response))
.catch(error => processError(error))
}
function processResult(response: AxiosResponse) {
// Store the response
const party: Party = response.data.party
// Store the important party and state-keeping values
appState.party.id = party.id
appState.party.user = party.user
appState.party.favorited = party.favorited
appState.party.created_at = party.created_at
appState.party.updated_at = party.updated_at
setFound(true)
setLoading(false)
// Populate the weapons in state
populateCharacters(party.characters)
}
function processError(error: any) {
if (error.response != null) {
if (error.response.status == 404) {
setFound(false)
setLoading(false)
}
} else {
console.error(error)
}
}
function populateCharacters(list: Array<GridCharacter>) {
list.forEach((object: GridCharacter) => {
if (object.position != null)
appState.grid.characters[object.position] = object
})
}
// Methods: Adding an object from search // Methods: Adding an object from search
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) { function receiveCharacterFromSearch(
object: Character | Weapon | Summon,
position: number
) {
const character = object as Character const character = object as Character
if (!party.id) { if (!party.id) {
props.createParty() props.createParty().then((response) => {
.then(response => {
const party = response.data.party const party = response.data.party
appState.party.id = party.id appState.party.id = party.id
setSlug(party.shortcode) setSlug(party.shortcode)
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.grid_character))
.catch(error => console.error(error)) .catch((error) => console.error(error))
}) })
} else { } else {
if (party.editable) if (party.editable)
saveCharacter(party.id, character, position) saveCharacter(party.id, character, position)
.then(response => storeGridCharacter(response.data.grid_character)) .then((response) => storeGridCharacter(response.data.grid_character))
.catch(error => console.error(error)) .catch((error) => console.error(error))
} }
} }
async function saveCharacter(partyId: string, character: Character, position: number) { async function saveCharacter(
return await api.endpoints.characters.create({ partyId: string,
'character': { character: Character,
'party_id': partyId, position: number
'character_id': character.id, ) {
'position': position, return await api.endpoints.characters.create(
'uncap_level': characterUncapLevel(character) {
} character: {
}, headers) party_id: partyId,
character_id: character.id,
position: position,
uncap_level: characterUncapLevel(character),
},
},
headers
)
} }
function storeGridCharacter(gridCharacter: GridCharacter) { function storeGridCharacter(gridCharacter: GridCharacter) {
@ -178,8 +136,9 @@ const CharacterGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) if (uncapLevel != previousUncapValues[position])
await api.updateUncap('character', id, uncapLevel) await api.updateUncap("character", id, uncapLevel).then((response) => {
.then(response => { storeGridCharacter(response.data.grid_character) }) storeGridCharacter(response.data.grid_character)
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -187,13 +146,17 @@ const CharacterGrid = (props: Props) => {
updateUncapLevel(position, previousUncapValues[position]) updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key // Remove optimistic key
let newPreviousValues = {...previousUncapValues} let newPreviousValues = { ...previousUncapValues }
delete newPreviousValues[position] delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues) setPreviousUncapValues(newPreviousValues)
} }
} }
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { function initiateUncapUpdate(
id: string,
position: number,
uncapLevel: number
) {
memoizeAction(id, position, uncapLevel) memoizeAction(id, position, uncapLevel)
// Optimistically update UI // Optimistically update UI
@ -203,13 +166,16 @@ const CharacterGrid = (props: Props) => {
const memoizeAction = useCallback( const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => { (id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel) debouncedAction(id, position, uncapLevel)
}, [props, previousUncapValues] },
[props, previousUncapValues]
) )
const debouncedAction = useMemo(() => const debouncedAction = useMemo(
() =>
debounce((id, position, number) => { debounce((id, position, number) => {
saveUncap(id, position, number) saveUncap(id, position, number)
}, 500), [props, saveUncap] }, 500),
[props, saveUncap]
) )
const updateUncapLevel = (position: number, uncapLevel: number) => { const updateUncapLevel = (position: number, uncapLevel: number) => {
@ -218,7 +184,7 @@ const CharacterGrid = (props: Props) => {
function storePreviousUncapValue(position: number) { function storePreviousUncapValue(position: number) {
// Save the current value in case of an unexpected result // Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues} let newPreviousValues = { ...previousUncapValues }
if (grid.characters[position]) { if (grid.characters[position]) {
newPreviousValues[position] = grid.characters[position].uncap_level newPreviousValues[position] = grid.characters[position].uncap_level
@ -234,7 +200,7 @@ const CharacterGrid = (props: Props) => {
<ul id="grid_characters"> <ul id="grid_characters">
{Array.from(Array(numCharacters)).map((x, i) => { {Array.from(Array(numCharacters)).map((x, i) => {
return ( return (
<li key={`grid_unit_${i}`} > <li key={`grid_unit_${i}`}>
<CharacterUnit <CharacterUnit
gridCharacter={grid.characters[i]} gridCharacter={grid.characters[i]}
editable={party.editable} editable={party.editable}

View file

@ -1,17 +1,16 @@
import React, { useEffect, useState } from "react"
import { useRouter } from "next/router"
import { useSnapshot } from "valtio"
import { useTranslation } from "next-i18next"
import classNames from "classnames"
import React, { useEffect, useState } from 'react' import { accountState } from "~utils/accountState"
import { useRouter } from 'next/router' import { formatTimeAgo } from "~utils/timeAgo"
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import { accountState } from '~utils/accountState' import Button from "~components/Button"
import { formatTimeAgo } from '~utils/timeAgo' import { ButtonType } from "~utils/enums"
import Button from '~components/Button' import "./index.scss"
import { ButtonType } from '~utils/enums'
import './index.scss'
interface Props { interface Props {
shortcode: string shortcode: string
@ -33,32 +32,32 @@ const GridRep = (props: Props) => {
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation("common")
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
const [mainhand, setMainhand] = useState<Weapon>() const [mainhand, setMainhand] = useState<Weapon>()
const [weapons, setWeapons] = useState<GridArray<Weapon>>({}) const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
const titleClass = classNames({ const titleClass = classNames({
'empty': !props.name empty: !props.name,
}) })
const raidClass = classNames({ const raidClass = classNames({
'raid': true, raid: true,
'empty': !props.raid empty: !props.raid,
}) })
const userClass = classNames({ const userClass = classNames({
'user': true, user: true,
'empty': !props.user empty: !props.user,
}) })
useEffect(() => { useEffect(() => {
const newWeapons = Array(numWeapons) const newWeapons = Array(numWeapons)
for (const [key, value] of Object.entries(props.grid)) { for (const [key, value] of Object.entries(props.grid)) {
if (value.position == -1) if (value.position == -1) setMainhand(value.object)
setMainhand(value.object)
else if (!value.mainhand && value.position != null) else if (!value.mainhand && value.position != null)
newWeapons[value.position] = value.object newWeapons[value.position] = value.object
} }
@ -71,7 +70,7 @@ const GridRep = (props: Props) => {
} }
function generateMainhandImage() { function generateMainhandImage() {
let url = '' let url = ""
if (mainhand) { if (mainhand) {
if (mainhand.element == 0 && props.grid[0].element) { if (mainhand.element == 0 && props.grid[0].element) {
@ -81,12 +80,15 @@ const GridRep = (props: Props) => {
} }
} }
return (mainhand) ? return mainhand && props.grid[0] ? (
<img alt={mainhand.name[locale]} src={url} /> : '' <img alt={mainhand.name[locale]} src={url} />
) : (
""
)
} }
function generateGridImage(position: number) { function generateGridImage(position: number) {
let url = '' let url = ""
if (weapons[position]) { if (weapons[position]) {
if (weapons[position].element == 0 && props.grid[position].element) { if (weapons[position].element == 0 && props.grid[position].element) {
@ -96,13 +98,15 @@ const GridRep = (props: Props) => {
} }
} }
return (weapons[position]) ? return weapons[position] ? (
<img alt={weapons[position].name[locale]} src={url} /> : '' <img alt={weapons[position].name[locale]} src={url} />
) : (
""
)
} }
function sendSaveData() { function sendSaveData() {
if (props.onSave) if (props.onSave) props.onSave(props.id, props.favorited)
props.onSave(props.id, props.favorited)
} }
const userImage = () => { const userImage = () => {
@ -116,16 +120,21 @@ const GridRep = (props: Props) => {
src={`/profile/${props.user.picture.picture}.png`} src={`/profile/${props.user.picture.picture}.png`}
/> />
) )
else else return <div className="no-user" />
return (<div className="no-user" />)
} }
const details = ( const details = (
<div className="Details"> <div className="Details">
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2> <h2 className={titleClass} onClick={navigate}>
{props.name ? props.name : t("no_title")}
</h2>
<div className="bottom"> <div className="bottom">
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div> <div className={raidClass}>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, locale)}</time> {props.raid ? props.raid.name[locale] : t("no_raid")}
</div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
{formatTimeAgo(props.createdAt, locale)}
</time>
</div> </div>
</div> </div>
) )
@ -134,50 +143,55 @@ const GridRep = (props: Props) => {
<div className="Details"> <div className="Details">
<div className="top"> <div className="top">
<div className="info"> <div className="info">
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2> <h2 className={titleClass} onClick={navigate}>
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div> {props.name ? props.name : t("no_title")}
</h2>
<div className={raidClass}>
{props.raid ? props.raid.name[locale] : t("no_raid")}
</div> </div>
{ </div>
(account.authorized && ( {account.authorized &&
(props.user && account.user && account.user.id !== props.user.id) ((props.user && account.user && account.user.id !== props.user.id) ||
|| (!props.user) !props.user) ? (
)) ?
<Button <Button
active={props.favorited} active={props.favorited}
icon="save" icon="save"
type={ButtonType.IconOnly} type={ButtonType.IconOnly}
onClick={sendSaveData} /> onClick={sendSaveData}
: '' />
} ) : (
""
)}
</div> </div>
<div className="bottom"> <div className="bottom">
<div className={userClass}> <div className={userClass}>
{ userImage() } {userImage()}
{ (props.user) ? props.user.username : t('no_user') } {props.user ? props.user.username : t("no_user")}
</div> </div>
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, locale)}</time> <time className="last-updated" dateTime={props.createdAt.toISOString()}>
{formatTimeAgo(props.createdAt, locale)}
</time>
</div> </div>
</div> </div>
) )
return ( return (
<div className="GridRep"> <div className="GridRep">
{ (props.displayUser) ? detailsWithUsername : details} {props.displayUser ? detailsWithUsername : details}
<div className="Grid" onClick={navigate}> <div className="Grid" onClick={navigate}>
<div className="weapon grid_mainhand"> <div className="weapon grid_mainhand">{generateMainhandImage()}</div>
{generateMainhandImage()}
</div>
<ul className="grid_weapons"> <ul className="grid_weapons">
{ {Array.from(Array(numWeapons)).map((x, i) => {
Array.from(Array(numWeapons)).map((x, i) => {
return ( return (
<li key={`${props.shortcode}-${i}`} className="weapon grid_weapon"> <li
key={`${props.shortcode}-${i}`}
className="weapon grid_weapon"
>
{generateGridImage(i)} {generateGridImage(i)}
</li> </li>
) )
}) })}
}
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -2,14 +2,9 @@
display: grid; display: grid;
grid-template-columns: auto auto auto; grid-template-columns: auto auto auto;
margin: 0 auto; margin: 0 auto;
opacity: 0;
padding: 0; padding: 0;
width: fit-content; width: fit-content;
transition: opacity 0.14s ease-in-out; transition: opacity 0.14s ease-in-out;
// width: fit-content; // width: fit-content;
max-width: 996px; max-width: 996px;
&.visible {
opacity: 1;
}
} }

View file

@ -1,24 +1,18 @@
import classNames from 'classnames' import classNames from "classnames"
import React from 'react' import React from "react"
import './index.scss' import "./index.scss"
interface Props { interface Props {
loading: boolean
children: React.ReactNode children: React.ReactNode
} }
const GridRepCollection = (props: Props) => { const GridRepCollection = (props: Props) => {
const classes = classNames({ const classes = classNames({
'GridRepCollection': true, GridRepCollection: true,
'visible': !props.loading
}) })
return ( return <div className={classes}>{props.children}</div>
<div className={classes}>
{props.children}
</div>
)
} }
export default GridRepCollection export default GridRepCollection

View file

@ -1,42 +1,50 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie, setCookie } from "cookies-next"
import Router, { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useTranslation } from 'next-i18next' import { useTranslation } from "next-i18next"
import Link from 'next/link' import Link from "next/link"
import * as Switch from '@radix-ui/react-switch' import * as Switch from "@radix-ui/react-switch"
import AboutModal from '~components/AboutModal' import AboutModal from "~components/AboutModal"
import AccountModal from '~components/AccountModal' import AccountModal from "~components/AccountModal"
import LoginModal from '~components/LoginModal' import LoginModal from "~components/LoginModal"
import SignupModal from '~components/SignupModal' import SignupModal from "~components/SignupModal"
import './index.scss' import "./index.scss"
interface Props { interface Props {
authenticated: boolean, authenticated: boolean
username?: string, username?: string
logout?: () => void logout?: () => void
} }
const HeaderMenu = (props: Props) => { const HeaderMenu = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation("common")
const [accountCookies] = useCookies(['account']) const accountCookie = getCookie("account")
const [userCookies] = useCookies(['user']) const accountData: AccountCookie = accountCookie
const [cookies, setCookies] = useCookies() ? JSON.parse(accountCookie as string)
: null
const userCookie = getCookie("user")
const userData: UserCookie = userCookie
? JSON.parse(userCookie as string)
: null
const localeCookie = getCookie("NEXT_LOCALE")
const [checked, setChecked] = useState(false) const [checked, setChecked] = useState(false)
useEffect(() => { useEffect(() => {
const locale = cookies['NEXT_LOCALE'] const locale = localeCookie
setChecked((locale === 'ja') ? true : false) setChecked(locale === "ja" ? true : false)
}, [cookies]) }, [localeCookie])
function handleCheckedChange(value: boolean) { function handleCheckedChange(value: boolean) {
const language = (value) ? 'ja' : 'en' const language = value ? "ja" : "en"
setCookies('NEXT_LOCALE', language, { path: '/'}) setCookie("NEXT_LOCALE", language, { path: "/" })
router.push(router.asPath, undefined, { locale: language }) router.push(router.asPath, undefined, { locale: language })
} }
@ -46,32 +54,32 @@ const HeaderMenu = (props: Props) => {
<ul className="Menu auth"> <ul className="Menu auth">
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem profile"> <li className="MenuItem profile">
<Link href={`/${accountCookies.account.username}` || ''} passHref> <Link href={`/${accountData.username}` || ""} passHref>
<div> <div>
<span>{accountCookies.account.username}</span> <span>{accountData.username}</span>
<img <img
alt={userCookies.user.picture} alt={userData.picture}
className={`profile ${userCookies.user.element}`} className={`profile ${userData.element}`}
srcSet={`/profile/${userCookies.user.picture}.png, srcSet={`/profile/${userData.picture}.png,
/profile/${userCookies.user.picture}@2x.png 2x`} /profile/${userData.picture}@2x.png 2x`}
src={`/profile/${userCookies.user.picture}.png`} src={`/profile/${userData.picture}.png`}
/> />
</div> </div>
</Link> </Link>
</li> </li>
<li className="MenuItem"> <li className="MenuItem">
<Link href={`/saved` || ''}>{t('menu.saved')}</Link> <Link href={`/saved` || ""}>{t("menu.saved")}</Link>
</li> </li>
</div> </div>
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem"> <li className="MenuItem">
<Link href='/teams'>{t('menu.teams')}</Link> <Link href="/teams">{t("menu.teams")}</Link>
</li> </li>
<li className="MenuItem disabled"> <li className="MenuItem disabled">
<div> <div>
<span>{t('menu.guides')}</span> <span>{t("menu.guides")}</span>
<i className="tag">{t('coming_soon')}</i> <i className="tag">{t("coming_soon")}</i>
</div> </div>
</li> </li>
</div> </div>
@ -79,7 +87,7 @@ const HeaderMenu = (props: Props) => {
<AboutModal /> <AboutModal />
<AccountModal /> <AccountModal />
<li className="MenuItem" onClick={props.logout}> <li className="MenuItem" onClick={props.logout}>
<span>{t('menu.logout')}</span> <span>{t("menu.logout")}</span>
</li> </li>
</div> </div>
</ul> </ul>
@ -92,8 +100,12 @@ const HeaderMenu = (props: Props) => {
<ul className="Menu unauth"> <ul className="Menu unauth">
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem language"> <li className="MenuItem language">
<span>{t('menu.language')}</span> <span>{t("menu.language")}</span>
<Switch.Root className="Switch" onCheckedChange={handleCheckedChange} checked={checked}> <Switch.Root
className="Switch"
onCheckedChange={handleCheckedChange}
checked={checked}
>
<Switch.Thumb className="Thumb" /> <Switch.Thumb className="Thumb" />
<span className="left">JP</span> <span className="left">JP</span>
<span className="right">EN</span> <span className="right">EN</span>
@ -102,13 +114,13 @@ const HeaderMenu = (props: Props) => {
</div> </div>
<div className="MenuGroup"> <div className="MenuGroup">
<li className="MenuItem"> <li className="MenuItem">
<Link href='/teams'>{t('menu.teams')}</Link> <Link href="/teams">{t("menu.teams")}</Link>
</li> </li>
<li className="MenuItem disabled"> <li className="MenuItem disabled">
<div> <div>
<span>{t('menu.guides')}</span> <span>{t("menu.guides")}</span>
<i className="tag">{t('coming_soon')}</i> <i className="tag">{t("coming_soon")}</i>
</div> </div>
</li> </li>
</div> </div>
@ -123,7 +135,7 @@ const HeaderMenu = (props: Props) => {
) )
} }
return (props.authenticated) ? authItems() : unauthItems() return props.authenticated ? authItems() : unauthItems()
} }
export default HeaderMenu export default HeaderMenu

View file

@ -1,19 +1,19 @@
import React, { useState } from 'react' import React, { useState } from "react"
import { useCookies } from 'react-cookie' import { setCookie } from "cookies-next"
import Router, { useRouter } from 'next/router' 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 * 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 Fieldset from '~components/Fieldset' import Fieldset from "~components/Fieldset"
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg"
import './index.scss' import "./index.scss"
interface Props {} interface Props {}
@ -23,22 +23,20 @@ interface ErrorMap {
password: string password: string
} }
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const emailRegex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const LoginModal = (props: Props) => { const LoginModal = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Set up form states and error handling // Set up form states and error handling
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
email: '', email: "",
password: '' password: "",
}) })
// Cookies
const [cookies, setCookies] = useCookies()
// States // States
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -49,22 +47,20 @@ const LoginModal = (props: Props) => {
function handleChange(event: React.ChangeEvent<HTMLInputElement>) { function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { name, value } = event.target const { name, value } = event.target
let newErrors = {...errors} let newErrors = { ...errors }
switch(name) { switch (name) {
case 'email': case "email":
if (value.length == 0) if (value.length == 0)
newErrors.email = t('modals.login.errors.empty_email') newErrors.email = t("modals.login.errors.empty_email")
else if (!emailRegex.test(value)) else if (!emailRegex.test(value))
newErrors.email = t('modals.login.errors.invalid_email') newErrors.email = t("modals.login.errors.invalid_email")
else else newErrors.email = ""
newErrors.email = ''
break break
case 'password': case "password":
newErrors.password = value.length == 0 newErrors.password =
? t('modals.login.errors.empty_password') value.length == 0 ? t("modals.login.errors.empty_password") : ""
: ''
break break
default: default:
@ -95,17 +91,18 @@ const LoginModal = (props: Props) => {
const body = { const body = {
email: emailInput.current?.value, email: emailInput.current?.value,
password: passwordInput.current?.value, password: passwordInput.current?.value,
grant_type: 'password' grant_type: "password",
} }
if (formValid) { if (formValid) {
api.login(body) api
.then(response => { .login(body)
.then((response) => {
storeCookieInfo(response) storeCookieInfo(response)
return response.data.user.id return response.data.user.id
}) })
.then(id => fetchUserInfo(id)) .then((id) => fetchUserInfo(id))
.then(infoResponse => storeUserInfo(infoResponse)) .then((infoResponse) => storeUserInfo(infoResponse))
} }
} }
@ -116,35 +113,36 @@ const LoginModal = (props: Props) => {
function storeCookieInfo(response: AxiosResponse) { function storeCookieInfo(response: AxiosResponse) {
const user = response.data.user const user = response.data.user
const cookieObj = { const cookieObj: AccountCookie = {
user_id: user.id, userId: user.id,
username: user.username, username: user.username,
access_token: response.data.access_token token: response.data.access_token,
} }
setCookies('account', cookieObj, { path: '/' }) setCookie("account", cookieObj, { path: "/" })
} }
function storeUserInfo(response: AxiosResponse) { function storeUserInfo(response: AxiosResponse) {
const user = response.data.user const user = response.data.user
const cookieObj = { const cookieObj: UserCookie = {
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
language: user.language, language: user.language,
gender: user.gender gender: user.gender,
} }
setCookies('user', cookieObj, { path: '/' }) setCookie("user", cookieObj, { path: "/" })
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
gender: user.gender gender: user.gender,
} }
console.log("Authorizing account...")
accountState.account.authorized = true accountState.account.authorized = true
setOpen(false) setOpen(false)
@ -153,7 +151,7 @@ const LoginModal = (props: Props) => {
function changeLanguage(newLanguage: string) { function changeLanguage(newLanguage: string) {
if (newLanguage !== router.locale) { if (newLanguage !== router.locale) {
setCookies('NEXT_LOCALE', newLanguage, { path: '/'}) setCookie("NEXT_LOCALE", newLanguage, { path: "/" })
router.push(router.asPath, undefined, { locale: newLanguage }) router.push(router.asPath, undefined, { locale: newLanguage })
} }
} }
@ -161,8 +159,8 @@ const LoginModal = (props: Props) => {
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open)
setErrors({ setErrors({
email: '', email: "",
password: '' password: "",
}) })
} }
@ -170,13 +168,18 @@ const LoginModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('menu.login')}</span> <span>{t("menu.login")}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Login Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
className="Login Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">{t('modals.login.title')}</Dialog.Title> <Dialog.Title className="DialogTitle">
{t("modals.login.title")}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />
@ -187,7 +190,7 @@ const LoginModal = (props: Props) => {
<form className="form" onSubmit={login}> <form className="form" onSubmit={login}>
<Fieldset <Fieldset
fieldName="email" fieldName="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}
@ -195,13 +198,13 @@ const LoginModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="password" fieldName="password"
placeholder={t('modals.login.placeholders.password')} placeholder={t("modals.login.placeholders.password")}
onChange={handleChange} onChange={handleChange}
error={errors.password} error={errors.password}
ref={passwordInput} ref={passwordInput}
/> />
<Button>{t('modals.login.buttons.confirm')}</Button> <Button>{t("modals.login.buttons.confirm")}</Button>
</form> </form>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,38 +1,41 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import { useCookies } from 'react-cookie' import { getCookie } from "cookies-next"
import clonedeep from 'lodash.clonedeep' import clonedeep from "lodash.clonedeep"
import { subscribeKey } from 'valtio/utils'
import PartySegmentedControl from '~components/PartySegmentedControl' import PartySegmentedControl from "~components/PartySegmentedControl"
import PartyDetails from '~components/PartyDetails' import PartyDetails from "~components/PartyDetails"
import WeaponGrid from '~components/WeaponGrid' import WeaponGrid from "~components/WeaponGrid"
import SummonGrid from '~components/SummonGrid' import SummonGrid from "~components/SummonGrid"
import CharacterGrid from '~components/CharacterGrid' import CharacterGrid from "~components/CharacterGrid"
import api from '~utils/api' import api from "~utils/api"
import { appState, initialAppState } from '~utils/appState' import { appState, initialAppState } from "~utils/appState"
import { GridType, TeamElement } from '~utils/enums' import { GridType, TeamElement } from "~utils/enums"
import './index.scss' import "./index.scss"
import { AxiosResponse } from 'axios'
// Props // Props
interface Props { interface Props {
new?: boolean new?: boolean
slug?: string team?: Party
raids: Raid[][]
pushHistory?: (path: string) => void pushHistory?: (path: string) => void
} }
const Party = (props: Props) => { const Party = (props: Props) => {
// Cookies // Cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = useMemo(() => { const headers = useMemo(() => {
return (cookies.account != null) ? { return accountData
headers: { 'Authorization': `Bearer ${cookies.account.access_token}` } ? { headers: { Authorization: `Bearer ${accountData.token}` } }
} : {} : {}
}, [cookies.account]) }, [accountData])
// Set up router // Set up router
const router = useRouter() const router = useRouter()
@ -48,6 +51,7 @@ const Party = (props: Props) => {
useEffect(() => { useEffect(() => {
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState)
appState.grid = resetState.grid appState.grid = resetState.grid
if (props.team) storeParty(props.team)
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -62,9 +66,9 @@ const Party = (props: Props) => {
async function createParty(extra: boolean = false) { async function createParty(extra: boolean = false) {
let body = { let body = {
party: { party: {
...(cookies.account) && { user_id: cookies.account.user_id }, ...(accountData && { user_id: accountData.userId }),
extra: extra extra: extra,
} },
} }
return await api.endpoints.parties.create(body, headers) return await api.endpoints.parties.create(body, headers)
@ -75,32 +79,47 @@ 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(party.id, { api.endpoints.parties.update(
'party': { 'extra': event.target.checked } party.id,
}, headers) {
party: { extra: event.target.checked },
},
headers
)
} }
} }
function jobChanged() { function jobChanged() {
if (party.id) { if (party.id && appState.party.editable) {
api.endpoints.parties.update(party.id, { api.endpoints.parties.update(
'party': { 'job_id': (job) ? job.id : '' } party.id,
}, headers) {
party: { job_id: job ? job.id : "" },
},
headers
)
} }
} }
function updateDetails(name?: string, description?: string, raid?: Raid) { function updateDetails(name?: string, description?: string, raid?: Raid) {
if (appState.party.name !== name || if (
appState.party.name !== name ||
appState.party.description !== description || appState.party.description !== description ||
appState.party.raid?.id !== raid?.id) { appState.party.raid?.id !== raid?.id
) {
if (appState.party.id) if (appState.party.id)
api.endpoints.parties.update(appState.party.id, { api.endpoints.parties
'party': { .update(
'name': name, appState.party.id,
'description': description, {
'raid_id': raid?.id party: {
} name: name,
}, headers) 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
@ -113,10 +132,11 @@ const Party = (props: Props) => {
// Deleting the party // Deleting the party
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.destroy({ id: appState.party.id, params: headers }) api.endpoints.parties
.destroy({ id: appState.party.id, params: headers })
.then(() => { .then(() => {
// Push to route // Push to route
router.push('/') router.push("/")
// Clean state // Clean state
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState)
@ -133,19 +153,72 @@ const Party = (props: Props) => {
} }
} }
// Methods: Storing party data
const storeParty = function (party: 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.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
// Populate state
storeCharacters(party.characters)
storeWeapons(party.weapons)
storeSummons(party.summons)
}
const storeCharacters = (list: Array<GridCharacter>) => {
list.forEach((object: GridCharacter) => {
if (object.position != null)
appState.grid.characters[object.position] = object
})
}
const storeWeapons = (list: Array<GridWeapon>) => {
list.forEach((gridObject: GridWeapon) => {
if (gridObject.mainhand) {
appState.grid.weapons.mainWeapon = gridObject
appState.party.element = gridObject.object.element
} else if (!gridObject.mainhand && gridObject.position != null) {
appState.grid.weapons.allWeapons[gridObject.position] = gridObject
}
})
}
const storeSummons = (list: Array<GridSummon>) => {
list.forEach((gridObject: GridSummon) => {
if (gridObject.main) appState.grid.summons.mainSummon = gridObject
else if (gridObject.friend)
appState.grid.summons.friendSummon = gridObject
else if (
!gridObject.main &&
!gridObject.friend &&
gridObject.position != null
)
appState.grid.summons.allSummons[gridObject.position] = gridObject
})
}
// Methods: Navigating with segmented control // Methods: Navigating with segmented control
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) { function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
switch(event.target.value) { switch (event.target.value) {
case 'class': case "class":
setCurrentTab(GridType.Class) setCurrentTab(GridType.Class)
break break
case 'characters': case "characters":
setCurrentTab(GridType.Character) setCurrentTab(GridType.Character)
break break
case 'weapons': case "weapons":
setCurrentTab(GridType.Weapon) setCurrentTab(GridType.Weapon)
break break
case 'summons': case "summons":
setCurrentTab(GridType.Summon) setCurrentTab(GridType.Summon)
break break
default: default:
@ -153,42 +226,6 @@ const Party = (props: Props) => {
} }
} }
// Methods: Fetch party details
const processResult = useCallback((response: AxiosResponse) => {
appState.party.id = response.data.party.id
appState.party.user = response.data.party.user
appState.party.favorited = response.data.party.favorited
appState.party.created_at = response.data.party.created_at
appState.party.updated_at = response.data.party.updated_at
// Store the party's user-generated details
appState.party.name = response.data.party.name
appState.party.description = response.data.party.description
appState.party.raid = response.data.party.raid
appState.party.job = response.data.party.job
}, [])
const handleError = useCallback((error: any) => {
if (error.response != null && error.response.status == 404) {
// setFound(false)
} else if (error.response != null) {
console.error(error)
} else {
console.error("There was an error.")
}
}, [])
const fetchDetails = useCallback((shortcode: string) => {
return api.endpoints.parties.getOne({ id: shortcode, params: headers })
.then(response => processResult(response))
.catch(error => handleError(error))
}, [headers, processResult, handleError])
useEffect(() => {
const shortcode = (props.slug) ? props.slug : undefined
if (shortcode) fetchDetails(shortcode)
}, [props.slug, fetchDetails])
// Render: JSX components // Render: JSX components
const navigation = ( const navigation = (
<PartySegmentedControl <PartySegmentedControl
@ -201,7 +238,7 @@ const Party = (props: Props) => {
const weaponGrid = ( const weaponGrid = (
<WeaponGrid <WeaponGrid
new={props.new || false} new={props.new || false}
slug={props.slug} weapons={props.team?.weapons}
createParty={createParty} createParty={createParty}
pushHistory={props.pushHistory} pushHistory={props.pushHistory}
/> />
@ -210,7 +247,7 @@ const Party = (props: Props) => {
const summonGrid = ( const summonGrid = (
<SummonGrid <SummonGrid
new={props.new || false} new={props.new || false}
slug={props.slug} summons={props.team?.summons}
createParty={createParty} createParty={createParty}
pushHistory={props.pushHistory} pushHistory={props.pushHistory}
/> />
@ -219,14 +256,14 @@ const Party = (props: Props) => {
const characterGrid = ( const characterGrid = (
<CharacterGrid <CharacterGrid
new={props.new || false} new={props.new || false}
slug={props.slug} characters={props.team?.characters}
createParty={createParty} createParty={createParty}
pushHistory={props.pushHistory} pushHistory={props.pushHistory}
/> />
) )
const currentGrid = () => { const currentGrid = () => {
switch(currentTab) { switch (currentTab) {
case GridType.Character: case GridType.Character:
return characterGrid return characterGrid
case GridType.Weapon: case GridType.Weapon:
@ -238,15 +275,15 @@ const Party = (props: Props) => {
return ( return (
<div> <div>
{ navigation } {navigation}
<section id="Party"> <section id="Party">{currentGrid()}</section>
{ currentGrid() } {
</section> <PartyDetails
{ <PartyDetails
editable={party.editable} editable={party.editable}
updateCallback={updateDetails} updateCallback={updateDetails}
deleteCallback={deleteTeam} deleteCallback={deleteTeam}
/>} />
}
</div> </div>
) )
} }

View file

@ -1,92 +1,94 @@
import React, { useState } from 'react' import React, { useState } from "react"
import Head from 'next/head' 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"
import Linkify from 'react-linkify' import Linkify from "react-linkify"
import classNames from 'classnames' import classNames from "classnames"
import * as AlertDialog from '@radix-ui/react-alert-dialog' import * as AlertDialog from "@radix-ui/react-alert-dialog"
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg"
import Button from '~components/Button' import Button from "~components/Button"
import CharLimitedFieldset from '~components/CharLimitedFieldset' 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 { accountState } from "~utils/accountState"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import './index.scss' 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 = { const emptyRaid: Raid = {
id: '', id: "",
name: { name: {
en: '', en: "",
ja: '' ja: "",
}, },
slug: '', slug: "",
level: 0, level: 0,
group: 0, group: 0,
element: 0 element: 0,
} }
// Props // Props
interface Props { interface Props {
editable: boolean editable: boolean
updateCallback: (name?: string, description?: string, raid?: Raid) => void updateCallback: (name?: string, description?: string, raid?: Raid) => void
deleteCallback: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void deleteCallback: (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => void
} }
const PartyDetails = (props: Props) => { const PartyDetails = (props: Props) => {
const { party, raids } = useSnapshot(appState) const { party, raids } = useSnapshot(appState)
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const { t } = useTranslation('common') const { t } = useTranslation("common")
const router = useRouter() const router = useRouter()
const locale = router.locale || 'en' const locale = router.locale || "en"
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 raidSelect = React.createRef<HTMLSelectElement>()
const readOnlyClasses = classNames({ const readOnlyClasses = classNames({
'PartyDetails': true, PartyDetails: true,
'ReadOnly': true, ReadOnly: true,
'Visible': !party.detailsVisible Visible: !party.detailsVisible,
}) })
const editableClasses = classNames({ const editableClasses = classNames({
'PartyDetails': true, PartyDetails: true,
'Editable': true, Editable: true,
'Visible': party.detailsVisible Visible: party.detailsVisible,
}) })
const emptyClasses = classNames({ const emptyClasses = classNames({
'EmptyDetails': true, EmptyDetails: true,
'Visible': !party.detailsVisible Visible: !party.detailsVisible,
}) })
const userClass = classNames({ const userClass = classNames({
'user': true, user: true,
'empty': !party.user empty: !party.user,
}) })
const linkClass = classNames({ const linkClass = classNames({
'wind': party && party.element == 1, wind: party && party.element == 1,
'fire': party && party.element == 2, fire: party && party.element == 2,
'water': party && party.element == 3, water: party && party.element == 3,
'earth': party && party.element == 4, earth: party && party.element == 4,
'dark': party && party.element == 5, dark: party && party.element == 5,
'light': party && party.element == 6 light: party && party.element == 6,
}) })
const [errors, setErrors] = useState<{ [key: string]: string }>({ const [errors, setErrors] = useState<{ [key: string]: string }>({
name: '', name: "",
description: '' description: "",
}) })
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) { function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
@ -114,7 +116,7 @@ const PartyDetails = (props: Props) => {
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 === raidSelect.current?.value)
props.updateCallback(nameValue, descriptionValue, raid) props.updateCallback(nameValue, descriptionValue, raid)
toggleDetails() toggleDetails()
@ -131,15 +133,14 @@ const PartyDetails = (props: Props) => {
src={`/profile/${party.user.picture.picture}.png`} src={`/profile/${party.user.picture.picture}.png`}
/> />
) )
else else return <div className="no-user" />
return (<div className="no-user" />)
} }
const userBlock = () => { const userBlock = () => {
return ( return (
<div className={userClass}> <div className={userClass}>
{ userImage() } {userImage()}
{ (party.user) ? party.user.username : t('no_user') } {party.user ? party.user.username : t("no_user")}
</div> </div>
) )
} }
@ -158,9 +159,7 @@ const PartyDetails = (props: Props) => {
return ( return (
<div> <div>
<Link href={`/teams?raid=${raid.slug}`} passHref> <Link href={`/teams?raid=${raid.slug}`} passHref>
<a className={`Raid ${linkClass}`}> <a className={`Raid ${linkClass}`}>{raid.name[locale]}</a>
{raid.name[locale]}
</a>
</Link> </Link>
</div> </div>
) )
@ -171,30 +170,37 @@ const PartyDetails = (props: Props) => {
return ( return (
<AlertDialog.Root> <AlertDialog.Root>
<AlertDialog.Trigger className="Button destructive"> <AlertDialog.Trigger className="Button destructive">
<span className='icon'> <span className="icon">
<CrossIcon /> <CrossIcon />
</span> </span>
<span className="text">{t('buttons.delete')}</span> <span className="text">{t("buttons.delete")}</span>
</AlertDialog.Trigger> </AlertDialog.Trigger>
<AlertDialog.Portal> <AlertDialog.Portal>
<AlertDialog.Overlay className="Overlay" /> <AlertDialog.Overlay className="Overlay" />
<AlertDialog.Content className="Dialog"> <AlertDialog.Content className="Dialog">
<AlertDialog.Title className="DialogTitle"> <AlertDialog.Title className="DialogTitle">
{t('modals.delete_team.title')} {t("modals.delete_team.title")}
</AlertDialog.Title> </AlertDialog.Title>
<AlertDialog.Description className="DialogDescription"> <AlertDialog.Description className="DialogDescription">
{t('modals.delete_team.description')} {t("modals.delete_team.description")}
</AlertDialog.Description> </AlertDialog.Description>
<div className="actions"> <div className="actions">
<AlertDialog.Cancel className="Button modal">{t('modals.delete_team.buttons.cancel')}</AlertDialog.Cancel> <AlertDialog.Cancel className="Button modal">
<AlertDialog.Action className="Button modal destructive" onClick={(e) => props.deleteCallback(e)}>{t('modals.delete_team.buttons.confirm')}</AlertDialog.Action> {t("modals.delete_team.buttons.cancel")}
</AlertDialog.Cancel>
<AlertDialog.Action
className="Button modal destructive"
onClick={(e) => props.deleteCallback(e)}
>
{t("modals.delete_team.buttons.confirm")}
</AlertDialog.Action>
</div> </div>
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Portal> </AlertDialog.Portal>
</AlertDialog.Root> </AlertDialog.Root>
) )
} else { } else {
return ('') return ""
} }
} }
@ -211,12 +217,14 @@ const PartyDetails = (props: Props) => {
/> />
<RaidDropdown <RaidDropdown
showAllRaidsOption={false} showAllRaidsOption={false}
currentRaid={party.raid?.slug || ''} currentRaid={party.raid?.slug || ""}
ref={raidSelect} ref={raidSelect}
/> />
<TextFieldset <TextFieldset
fieldName="name" fieldName="name"
placeholder={"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!"} placeholder={
"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!"
}
value={party.description} value={party.description}
onChange={handleTextAreaChange} onChange={handleTextAreaChange}
error={errors.description} error={errors.description}
@ -225,20 +233,15 @@ const PartyDetails = (props: Props) => {
<div className="bottom"> <div className="bottom">
<div className="left"> <div className="left">
{ (router.pathname !== '/new') ? deleteButton() : '' } {router.pathname !== "/new" ? deleteButton() : ""}
</div> </div>
<div className="right"> <div className="right">
<Button <Button active={true} onClick={toggleDetails}>
active={true} {t("buttons.cancel")}
onClick={toggleDetails}>
{t('buttons.cancel')}
</Button> </Button>
<Button <Button active={true} icon="check" onClick={updateDetails}>
active={true} {t("buttons.save_info")}
icon="check"
onClick={updateDetails}>
{t('buttons.save_info')}
</Button> </Button>
</div> </div>
</div> </div>
@ -249,46 +252,68 @@ const PartyDetails = (props: Props) => {
<section className={readOnlyClasses}> <section className={readOnlyClasses}>
<div className="info"> <div className="info">
<div className="left"> <div className="left">
{ (party.name) ? <h1>{party.name}</h1> : '' } {party.name ? <h1>{party.name}</h1> : ""}
<div className="attribution"> <div className="attribution">
{ (party.user) ? linkedUserBlock(party.user) : userBlock() } {party.user ? linkedUserBlock(party.user) : userBlock()}
{ (party.raid) ? linkedRaidBlock(party.raid) : '' } {party.raid ? linkedRaidBlock(party.raid) : ""}
{ (party.created_at != undefined) {party.created_at != undefined ? (
? <time <time
className="last-updated" className="last-updated"
dateTime={new Date(party.created_at).toString()}> dateTime={new Date(party.created_at).toString()}
>
{formatTimeAgo(new Date(party.created_at), locale)} {formatTimeAgo(new Date(party.created_at), locale)}
</time> </time>
: '' } ) : (
""
)}
</div> </div>
</div> </div>
<div className="right"> <div className="right">
{ (party.editable) {party.editable ? (
? <Button active={true} icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button> <Button active={true} icon="edit" onClick={toggleDetails}>
: <div /> } {t("buttons.show_info")}
</Button>
) : (
<div />
)}
</div> </div>
</div> </div>
{ (party.description) ? <p><Linkify>{party.description}</Linkify></p> : '' } {party.description ? (
<p>
<Linkify>{party.description}</Linkify>
</p>
) : (
""
)}
</section> </section>
) )
const emptyDetails = ( const emptyDetails = (
<div className={emptyClasses}> <div className={emptyClasses}>
<Button active={true} icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button> {party.editable ? (
<Button active={true} icon="edit" onClick={toggleDetails}>
{t("buttons.show_info")}
</Button>
) : (
<div />
)}
</div> </div>
) )
const generateTitle = () => { const generateTitle = () => {
let title = '' let title = party.raid ? `[${party.raid?.name[locale]}] ` : ""
const username = (party.user != null) ? `@${party.user?.username}` : 'Anonymous' const username =
party.user != null ? `@${party.user?.username}` : t("header.anonymous")
if (party.name != null) if (party.name != null)
title = `${party.name} by ${username}` title += t("header.byline", { partyName: party.name, username: username })
else if (party.name == null && party.editable && router.route === '/new') else if (party.name == null && party.editable && router.route === "/new")
title = "New Team" title = t("header.new_team")
else else
title = `Untitled team by ${username}` title += t("header.untitled_team", {
username: username,
})
return title return title
} }
@ -299,16 +324,24 @@ const PartyDetails = (props: Props) => {
<title>{generateTitle()}</title> <title>{generateTitle()}</title>
<meta property="og:title" content={generateTitle()} /> <meta property="og:title" content={generateTitle()} />
<meta property="og:description" content={ (party.description) ? party.description : '' } /> <meta
property="og:description"
content={party.description ? party.description : ""}
/>
<meta property="og:url" content="https://app.granblue.team" /> <meta property="og:url" content="https://app.granblue.team" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" /> <meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content={generateTitle()} /> <meta name="twitter:title" content={generateTitle()} />
<meta name="twitter:description" content={ (party.description) ? party.description : '' } /> <meta
name="twitter:description"
content={party.description ? party.description : ""}
/>
</Head> </Head>
{ (editable && (party.name || party.description || party.raid)) ? readOnly : emptyDetails} {editable && (party.name || party.description || party.raid)
? readOnly
: emptyDetails}
{editable} {editable}
</div> </div>
) )

View file

@ -1,32 +1,32 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie, setCookie } from "cookies-next"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next"
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import api from '~utils/api' import api from "~utils/api"
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from "@radix-ui/react-dialog"
import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar' import CharacterSearchFilterBar from "~components/CharacterSearchFilterBar"
import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar' import WeaponSearchFilterBar from "~components/WeaponSearchFilterBar"
import SummonSearchFilterBar from '~components/SummonSearchFilterBar' import SummonSearchFilterBar from "~components/SummonSearchFilterBar"
import CharacterResult from '~components/CharacterResult' import CharacterResult from "~components/CharacterResult"
import WeaponResult from '~components/WeaponResult' import WeaponResult from "~components/WeaponResult"
import SummonResult from '~components/SummonResult' import SummonResult from "~components/SummonResult"
import './index.scss' import "./index.scss"
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg"
import cloneDeep from 'lodash.clonedeep' import cloneDeep from "lodash.clonedeep"
interface Props { interface Props {
send: (object: Character | Weapon | Summon, position: number) => any send: (object: Character | Weapon | Summon, position: number) => any
placeholderText: string placeholderText: string
fromPosition: number fromPosition: number
object: 'weapons' | 'characters' | 'summons', object: "weapons" | "characters" | "summons"
children: React.ReactNode children: React.ReactNode
} }
@ -39,19 +39,18 @@ const SearchModal = (props: Props) => {
const locale = router.locale const locale = router.locale
// Set up translation // Set up translation
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Set up cookies
const [cookies, setCookies] = useCookies()
let searchInput = React.createRef<HTMLInputElement>() let searchInput = React.createRef<HTMLInputElement>()
let scrollContainer = React.createRef<HTMLDivElement>() let scrollContainer = React.createRef<HTMLDivElement>()
const [firstLoad, setFirstLoad] = useState(true) const [firstLoad, setFirstLoad] = useState(true)
const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>() const [objects, setObjects] = useState<{
[id: number]: GridCharacter | GridWeapon | GridSummon
}>()
const [filters, setFilters] = useState<{ [key: string]: number[] }>() const [filters, setFilters] = useState<{ [key: string]: number[] }>()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [query, setQuery] = useState('') const [query, setQuery] = useState("")
const [results, setResults] = useState<(Weapon | Summon | Character)[]>([]) const [results, setResults] = useState<(Weapon | Summon | Character)[]>([])
// Pagination states // Pagination states
@ -64,8 +63,7 @@ const SearchModal = (props: Props) => {
}, [grid, props.object]) }, [grid, props.object])
useEffect(() => { useEffect(() => {
if (searchInput.current) if (searchInput.current) searchInput.current.focus()
searchInput.current.focus()
}, [searchInput]) }, [searchInput])
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) { function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
@ -73,18 +71,20 @@ const SearchModal = (props: Props) => {
if (text.length) { if (text.length) {
setQuery(text) setQuery(text)
} else { } else {
setQuery('') setQuery("")
} }
} }
function fetchResults({ replace = false }: { replace?: boolean }) { function fetchResults({ replace = false }: { replace?: boolean }) {
api.search({ api
.search({
object: props.object, object: props.object,
query: query, query: query,
filters: filters, filters: filters,
locale: locale, locale: locale,
page: currentPage page: currentPage,
}).then(response => { })
.then((response) => {
setTotalPages(response.data.total_pages) setTotalPages(response.data.total_pages)
setRecordCount(response.data.count) setRecordCount(response.data.count)
@ -93,12 +93,16 @@ const SearchModal = (props: Props) => {
} else { } else {
appendResults(response.data.results) appendResults(response.data.results)
} }
}).catch(error => { })
.catch((error) => {
console.error(error) console.error(error)
}) })
} }
function replaceResults(count: number, list: Weapon[] | Summon[] | Character[]) { function replaceResults(
count: number,
list: Weapon[] | Summon[] | Character[]
) {
if (count > 0) { if (count > 0) {
setResults(list) setResults(list)
} else { } else {
@ -112,22 +116,26 @@ const SearchModal = (props: Props) => {
function storeRecentResult(result: Character | Weapon | Summon) { function storeRecentResult(result: Character | Weapon | Summon) {
const key = `recent_${props.object}` const key = `recent_${props.object}`
const cookie = getCookie(key)
const cookieObj: Character[] | Weapon[] | Summon[] = cookie
? JSON.parse(cookie as string)
: []
let recents: Character[] | Weapon[] | Summon[] = [] let recents: Character[] | Weapon[] | Summon[] = []
if (props.object === "weapons") { if (props.object === "weapons") {
recents = cloneDeep(cookies[key] as Weapon[]) || [] recents = cloneDeep(cookieObj as Weapon[]) || []
if (!recents.find(item => item.granblue_id === result.granblue_id)) { if (!recents.find((item) => item.granblue_id === result.granblue_id)) {
recents.unshift(result as Weapon) recents.unshift(result as Weapon)
} }
} else if (props.object === "summons") { } else if (props.object === "summons") {
recents = cloneDeep(cookies[key] as Summon[]) || [] recents = cloneDeep(cookieObj as Summon[]) || []
if (!recents.find(item => item.granblue_id === result.granblue_id)) { if (!recents.find((item) => item.granblue_id === result.granblue_id)) {
recents.unshift(result as Summon) recents.unshift(result as Summon)
} }
} }
if (recents && recents.length > 5) recents.pop() if (recents && recents.length > 5) recents.pop()
setCookies(`recent_${props.object}`, recents, { path: '/' }) setCookie(`recent_${props.object}`, recents, { path: "/" })
sendData(result) sendData(result)
} }
@ -154,11 +162,15 @@ const SearchModal = (props: Props) => {
useEffect(() => { useEffect(() => {
// Filters changed // Filters changed
const key = `recent_${props.object}` const key = `recent_${props.object}`
const cookie = getCookie(key)
const cookieObj: Weapon[] | Summon[] | Character[] = cookie
? JSON.parse(cookie as string)
: []
if (open) { if (open) {
if (firstLoad && cookies[key] && cookies[key].length > 0) { if (firstLoad && cookieObj && cookieObj.length > 0) {
setResults(cookies[key]) setResults(cookieObj)
setRecordCount(cookies[key].length) setRecordCount(cookieObj.length)
setFirstLoad(false) setFirstLoad(false)
} else { } else {
setCurrentPage(1) setCurrentPage(1)
@ -178,25 +190,26 @@ const SearchModal = (props: Props) => {
function renderResults() { function renderResults() {
let jsx let jsx
switch(props.object) { switch (props.object) {
case 'weapons': case "weapons":
jsx = renderWeaponSearchResults() jsx = renderWeaponSearchResults()
break break
case 'summons': case "summons":
jsx = renderSummonSearchResults(results) jsx = renderSummonSearchResults(results)
break break
case 'characters': case "characters":
jsx = renderCharacterSearchResults(results) jsx = renderCharacterSearchResults(results)
break break
} }
return ( return (
<InfiniteScroll <InfiniteScroll
dataLength={ (results && results.length > 0) ? results.length : 0} dataLength={results && results.length > 0 ? results.length : 0}
next={ () => setCurrentPage(currentPage + 1) } next={() => setCurrentPage(currentPage + 1)}
hasMore={totalPages > currentPage} hasMore={totalPages > currentPage}
scrollableTarget="Results" scrollableTarget="Results"
loader={<div className="footer">Loading...</div>}> loader={<div className="footer">Loading...</div>}
>
{jsx} {jsx}
</InfiniteScroll> </InfiniteScroll>
) )
@ -208,11 +221,15 @@ const SearchModal = (props: Props) => {
const castResults: Weapon[] = results as Weapon[] const castResults: Weapon[] = results as Weapon[]
if (castResults && Object.keys(castResults).length > 0) { if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: Weapon) => { jsx = castResults.map((result: Weapon) => {
return <WeaponResult return (
<WeaponResult
key={result.id} key={result.id}
data={result} data={result}
onClick={() => { storeRecentResult(result) }} onClick={() => {
storeRecentResult(result)
}}
/> />
)
}) })
} }
@ -225,11 +242,15 @@ const SearchModal = (props: Props) => {
const castResults: Summon[] = results as Summon[] const castResults: Summon[] = results as Summon[]
if (castResults && Object.keys(castResults).length > 0) { if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: Summon) => { jsx = castResults.map((result: Summon) => {
return <SummonResult return (
<SummonResult
key={result.id} key={result.id}
data={result} data={result}
onClick={() => { storeRecentResult(result) }} onClick={() => {
storeRecentResult(result)
}}
/> />
)
}) })
} }
@ -242,11 +263,15 @@ const SearchModal = (props: Props) => {
const castResults: Character[] = results as Character[] const castResults: Character[] = results as Character[]
if (castResults && Object.keys(castResults).length > 0) { if (castResults && Object.keys(castResults).length > 0) {
jsx = castResults.map((result: Character) => { jsx = castResults.map((result: Character) => {
return <CharacterResult return (
<CharacterResult
key={result.id} key={result.id}
data={result} data={result}
onClick={() => { storeRecentResult(result) }} onClick={() => {
storeRecentResult(result)
}}
/> />
)
}) })
} }
@ -255,7 +280,7 @@ const SearchModal = (props: Props) => {
function openChange() { function openChange() {
if (open) { if (open) {
setQuery('') setQuery("")
setFirstLoad(true) setFirstLoad(true)
setResults([]) setResults([])
setRecordCount(0) setRecordCount(0)
@ -268,9 +293,7 @@ const SearchModal = (props: Props) => {
return ( return (
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
{props.children}
</Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Search Dialog"> <Dialog.Content className="Search Dialog">
<div id="Header"> <div id="Header">
@ -292,14 +315,28 @@ const SearchModal = (props: Props) => {
<CrossIcon /> <CrossIcon />
</Dialog.Close> </Dialog.Close>
</div> </div>
{ (props.object === 'characters') ? <CharacterSearchFilterBar sendFilters={receiveFilters} /> : '' } {props.object === "characters" ? (
{ (props.object === 'weapons') ? <WeaponSearchFilterBar sendFilters={receiveFilters} /> : '' } <CharacterSearchFilterBar sendFilters={receiveFilters} />
{ (props.object === 'summons') ? <SummonSearchFilterBar sendFilters={receiveFilters} /> : '' } ) : (
""
)}
{props.object === "weapons" ? (
<WeaponSearchFilterBar sendFilters={receiveFilters} />
) : (
""
)}
{props.object === "summons" ? (
<SummonSearchFilterBar sendFilters={receiveFilters} />
) : (
""
)}
</div> </div>
<div id="Results" ref={scrollContainer}> <div id="Results" ref={scrollContainer}>
<h5 className="total">{t('search.result_count', { "record_count": recordCount })}</h5> <h5 className="total">
{ (open) ? renderResults() : ''} {t("search.result_count", { record_count: recordCount })}
</h5>
{open ? renderResults() : ""}
</div> </div>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

View file

@ -1,20 +1,20 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from "react"
import Link from 'next/link' import Link from "next/link"
import { useCookies } from 'react-cookie' import { setCookie } from "cookies-next"
import { useRouter } from 'next/router' 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 * 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 Fieldset from '~components/Fieldset' import Fieldset from "~components/Fieldset"
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg"
import './index.scss' import "./index.scss"
interface Props {} interface Props {}
@ -26,24 +26,22 @@ interface ErrorMap {
passwordConfirmation: string passwordConfirmation: string
} }
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const emailRegex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const SignupModal = (props: Props) => { const SignupModal = (props: Props) => {
const router = useRouter() const router = useRouter()
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Set up form states and error handling // Set up form states and error handling
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({ const [errors, setErrors] = useState<ErrorMap>({
username: '', username: "",
email: '', email: "",
password: '', password: "",
passwordConfirmation: '' passwordConfirmation: "",
}) })
// Cookies
const [cookies, setCookies] = useCookies()
// States // States
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -52,7 +50,12 @@ const SignupModal = (props: Props) => {
const emailInput = React.createRef<HTMLInputElement>() const emailInput = React.createRef<HTMLInputElement>()
const passwordInput = React.createRef<HTMLInputElement>() const passwordInput = React.createRef<HTMLInputElement>()
const passwordConfirmationInput = React.createRef<HTMLInputElement>() const passwordConfirmationInput = React.createRef<HTMLInputElement>()
const form = [usernameInput, emailInput, passwordInput, passwordConfirmationInput] const form = [
usernameInput,
emailInput,
passwordInput,
passwordConfirmationInput,
]
function register(event: React.FormEvent) { function register(event: React.FormEvent) {
event.preventDefault() event.preventDefault()
@ -63,30 +66,31 @@ const SignupModal = (props: Props) => {
email: emailInput.current?.value, email: emailInput.current?.value,
password: passwordInput.current?.value, password: passwordInput.current?.value,
password_confirmation: passwordConfirmationInput.current?.value, password_confirmation: passwordConfirmationInput.current?.value,
language: router.locale language: router.locale,
} },
} }
if (formValid) if (formValid)
api.endpoints.users.create(body) api.endpoints.users
.then(response => { .create(body)
.then((response) => {
storeCookieInfo(response) storeCookieInfo(response)
return response.data.user.user_id return response.data.user.user_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 user = response.data.user
const cookieObj = { const cookieObj: AccountCookie = {
user_id: user.user_id, userId: user.user_id,
username: user.username, username: user.username,
access_token: user.token token: user.token,
} }
setCookies('account', cookieObj, { path: '/'}) setCookie("account", cookieObj, { path: "/" })
} }
function fetchUserInfo(id: string) { function fetchUserInfo(id: string) {
@ -96,22 +100,22 @@ const SignupModal = (props: Props) => {
function storeUserInfo(response: AxiosResponse) { function storeUserInfo(response: AxiosResponse) {
const user = response.data.user const user = response.data.user
const cookieObj = { const cookieObj: UserCookie = {
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
language: user.language, language: user.language,
gender: user.gender gender: user.gender,
} }
// TODO: Set language // TODO: Set language
setCookies('user', cookieObj, { path: '/'}) setCookie("user", cookieObj, { path: "/" })
accountState.account.user = { accountState.account.user = {
id: user.id, id: user.id,
username: user.username, username: user.username,
picture: user.picture.picture, picture: user.picture.picture,
element: user.picture.element, element: user.picture.element,
gender: user.gender gender: user.gender,
} }
accountState.account.authorized = true accountState.account.authorized = true
@ -125,52 +129,59 @@ const SignupModal = (props: Props) => {
const value = event.target.value const value = event.target.value
if (value.length >= 3) { if (value.length >= 3) {
api.check(fieldName, value) api.check(fieldName, value).then(
.then((response) => { (response) => {
processNameCheck(fieldName, value, response.data.available) processNameCheck(fieldName, value, response.data.available)
}, (error) => { },
(error) => {
console.error(error) console.error(error)
}) }
)
} else { } else {
validateName(fieldName, value) validateName(fieldName, value)
} }
} }
function processNameCheck(fieldName: string, value: string, available: boolean) { function processNameCheck(
const newErrors = {...errors} fieldName: string,
value: string,
available: boolean
) {
const newErrors = { ...errors }
if (available) { if (available) {
// Continue checking for errors // Continue checking for errors
newErrors[fieldName] = '' newErrors[fieldName] = ""
setErrors(newErrors) setErrors(newErrors)
setFormValid(true) setFormValid(true)
validateName(fieldName, value) validateName(fieldName, value)
} else { } else {
newErrors[fieldName] = t('modals.signup.errors.field_in_use', { field: fieldName}) newErrors[fieldName] = t("modals.signup.errors.field_in_use", {
field: fieldName,
})
setErrors(newErrors) setErrors(newErrors)
setFormValid(false) setFormValid(false)
} }
} }
function validateName(fieldName: string, value: string) { function validateName(fieldName: string, value: string) {
let newErrors = {...errors} let newErrors = { ...errors }
switch(fieldName) { switch (fieldName) {
case 'username': case "username":
if (value.length < 3) if (value.length < 3)
newErrors.username = t('modals.signup.errors.username_too_short') newErrors.username = t("modals.signup.errors.username_too_short")
else if (value.length > 20) else if (value.length > 20)
newErrors.username = t('modals.signup.errors.username_too_long') newErrors.username = t("modals.signup.errors.username_too_long")
else else newErrors.username = ""
newErrors.username = ''
break break
case 'email': case "email":
newErrors.email = emailRegex.test(value) newErrors.email = emailRegex.test(value)
? '' ? ""
: t('modals.signup.errors.invalid_email') : t("modals.signup.errors.invalid_email")
break break
default: default:
@ -184,25 +195,28 @@ const SignupModal = (props: Props) => {
event.preventDefault() event.preventDefault()
const { name, value } = event.target const { name, value } = event.target
let newErrors = {...errors} let newErrors = { ...errors }
switch(name) { switch (name) {
case 'password': case "password":
newErrors.password = passwordInput.current?.value.includes(usernameInput.current?.value!) newErrors.password = passwordInput.current?.value.includes(
? t('modals.signup.errors.password_contains_username') usernameInput.current?.value!
: '' )
? t("modals.signup.errors.password_contains_username")
: ""
break break
case 'password': case "password":
newErrors.password = value.length < 8 newErrors.password =
? t('modals.signup.errors.password_too_short') value.length < 8 ? t("modals.signup.errors.password_too_short") : ""
: ''
break break
case 'confirm_password': case "confirm_password":
newErrors.passwordConfirmation = passwordInput.current?.value === passwordConfirmationInput.current?.value newErrors.passwordConfirmation =
? '' passwordInput.current?.value ===
: t('modals.signup.errors.passwords_dont_match') passwordConfirmationInput.current?.value
? ""
: t("modals.signup.errors.passwords_dont_match")
break break
default: default:
@ -229,10 +243,10 @@ const SignupModal = (props: Props) => {
function openChange(open: boolean) { function openChange(open: boolean) {
setOpen(open) setOpen(open)
setErrors({ setErrors({
username: '', username: "",
email: '', email: "",
password: '', password: "",
passwordConfirmation: '' passwordConfirmation: "",
}) })
} }
@ -240,13 +254,18 @@ const SignupModal = (props: Props) => {
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<li className="MenuItem"> <li className="MenuItem">
<span>{t('menu.signup')}</span> <span>{t("menu.signup")}</span>
</li> </li>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Signup Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
className="Signup Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader"> <div className="DialogHeader">
<Dialog.Title className="DialogTitle">{t('modals.signup.title')}</Dialog.Title> <Dialog.Title className="DialogTitle">
{t("modals.signup.title")}
</Dialog.Title>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
<CrossIcon /> <CrossIcon />
@ -257,7 +276,7 @@ const SignupModal = (props: Props) => {
<form className="form" onSubmit={register}> <form className="form" onSubmit={register}>
<Fieldset <Fieldset
fieldName="username" fieldName="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}
@ -265,7 +284,7 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="email" fieldName="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}
@ -273,7 +292,7 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="password" fieldName="password"
placeholder={t('modals.signup.placeholders.password')} placeholder={t("modals.signup.placeholders.password")}
onChange={handlePasswordChange} onChange={handlePasswordChange}
error={errors.password} error={errors.password}
ref={passwordInput} ref={passwordInput}
@ -281,13 +300,13 @@ const SignupModal = (props: Props) => {
<Fieldset <Fieldset
fieldName="confirm_password" fieldName="confirm_password"
placeholder={t('modals.signup.placeholders.password_confirm')} placeholder={t("modals.signup.placeholders.password_confirm")}
onChange={handlePasswordChange} onChange={handlePasswordChange}
error={errors.passwordConfirmation} error={errors.passwordConfirmation}
ref={passwordConfirmationInput} ref={passwordConfirmationInput}
/> />
<Button>{t('modals.signup.buttons.confirm')}</Button> <Button>{t("modals.signup.buttons.confirm")}</Button>
<Dialog.Description className="terms"> <Dialog.Description className="terms">
{/* <Trans i18nKey="modals.signup.agreement"> {/* <Trans i18nKey="modals.signup.agreement">
@ -302,5 +321,4 @@ const SignupModal = (props: Props) => {
) )
} }
export default SignupModal export default SignupModal

View file

@ -1,24 +1,24 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie } from "cookies-next"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import { useTranslation } from 'next-i18next' import { useTranslation } from "next-i18next"
import { AxiosResponse } from 'axios' import { AxiosResponse } from "axios"
import debounce from 'lodash.debounce' import debounce from "lodash.debounce"
import SummonUnit from '~components/SummonUnit' import SummonUnit from "~components/SummonUnit"
import ExtraSummons from '~components/ExtraSummons' import ExtraSummons from "~components/ExtraSummons"
import api from '~utils/api' import api from "~utils/api"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import './index.scss' import "./index.scss"
// Props // Props
interface Props { interface Props {
new: boolean new: boolean
slug?: string summons?: GridSummon[]
createParty: () => Promise<AxiosResponse<any, any>> createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void pushHistory?: (path: string) => void
} }
@ -27,130 +27,85 @@ const SummonGrid = (props: Props) => {
// Constants // Constants
const numSummons: number = 4 const numSummons: number = 4
const { t } = useTranslation('common')
// Cookies // Cookies
const [cookies, _] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account != null) ? { const accountData: AccountCookie = cookie
headers: { ? JSON.parse(cookie as string)
'Authorization': `Bearer ${cookies.account.access_token}` : null
} const headers = accountData
} : {} ? { headers: { Authorization: `Bearer ${accountData.token}` } }
: {}
// Localization
const { t } = useTranslation("common")
// Set up state for view management // Set up state for view management
const { party, grid } = useSnapshot(appState) const { party, grid } = useSnapshot(appState)
const [slug, setSlug] = useState() const [slug, setSlug] = useState()
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
const [firstLoadComplete, setFirstLoadComplete] = useState(false)
// Create a temporary state to store previous weapon uncap value // Create a temporary state to store previous weapon uncap value
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) const [previousUncapValues, setPreviousUncapValues] = useState<{
[key: number]: number
// Fetch data from the server }>({})
useEffect(() => {
const shortcode = (props.slug) ? props.slug : slug
if (shortcode) fetchGrid(shortcode)
else appState.party.editable = true
}, [slug, props.slug])
// Set the editable flag only on first load // Set the editable flag only on first load
useEffect(() => { useEffect(() => {
if (!loading && !firstLoadComplete) {
// If user is logged in and matches // If user is logged in and matches
if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new) if (
(accountData && party.user && accountData.userId === party.user.id) ||
props.new
)
appState.party.editable = true appState.party.editable = true
else else appState.party.editable = false
appState.party.editable = false }, [props.new, accountData, party])
setFirstLoadComplete(true)
}
}, [props.new, cookies, party, loading, firstLoadComplete])
// Initialize an array of current uncap values for each summon // Initialize an array of current uncap values for each summon
useEffect(() => { useEffect(() => {
let initialPreviousUncapValues: {[key: number]: number} = {} let initialPreviousUncapValues: { [key: number]: number } = {}
if (appState.grid.summons.mainSummon) if (appState.grid.summons.mainSummon)
initialPreviousUncapValues[-1] = appState.grid.summons.mainSummon.uncap_level initialPreviousUncapValues[-1] =
appState.grid.summons.mainSummon.uncap_level
if (appState.grid.summons.friendSummon) if (appState.grid.summons.friendSummon)
initialPreviousUncapValues[6] = appState.grid.summons.friendSummon.uncap_level initialPreviousUncapValues[6] =
appState.grid.summons.friendSummon.uncap_level
Object.values(appState.grid.summons.allSummons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) Object.values(appState.grid.summons.allSummons).map(
(o) => (initialPreviousUncapValues[o.position] = o.uncap_level)
)
setPreviousUncapValues(initialPreviousUncapValues) setPreviousUncapValues(initialPreviousUncapValues)
}, [appState.grid.summons.mainSummon, appState.grid.summons.friendSummon, appState.grid.summons.allSummons]) }, [
appState.grid.summons.mainSummon,
appState.grid.summons.friendSummon,
// Methods: Fetching an object from the server appState.grid.summons.allSummons,
async function fetchGrid(shortcode: string) { ])
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'summons', params: headers })
.then(response => processResult(response))
.catch(error => processError(error))
}
function processResult(response: AxiosResponse) {
// Store the response
const party: Party = response.data.party
// Store the important party and state-keeping values
appState.party.id = party.id
appState.party.user = party.user
appState.party.favorited = party.favorited
appState.party.created_at = party.created_at
appState.party.updated_at = party.updated_at
setFound(true)
setLoading(false)
// Populate the weapons in state
populateSummons(party.summons)
}
function processError(error: any) {
if (error.response != null) {
if (error.response.status == 404) {
setFound(false)
setLoading(false)
}
} else {
console.error(error)
}
}
function populateSummons(list: Array<GridSummon>) {
list.forEach((gridObject: GridSummon) => {
if (gridObject.main)
appState.grid.summons.mainSummon = gridObject
else if (gridObject.friend)
appState.grid.summons.friendSummon = gridObject
else if (!gridObject.main && !gridObject.friend && gridObject.position != null)
appState.grid.summons.allSummons[gridObject.position] = gridObject
})
}
// Methods: Adding an object from search // Methods: Adding an object from search
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) { function receiveSummonFromSearch(
object: Character | Weapon | Summon,
position: number
) {
const summon = object as Summon const summon = object as Summon
if (!party.id) { if (!party.id) {
props.createParty() props.createParty().then((response) => {
.then(response => {
const party = response.data.party const party = response.data.party
appState.party.id = party.id appState.party.id = party.id
setSlug(party.shortcode) setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveSummon(party.id, summon, position) saveSummon(party.id, summon, position).then((response) =>
.then(response => storeGridSummon(response.data.grid_summon)) storeGridSummon(response.data.grid_summon)
)
}) })
} else { } else {
if (party.editable) if (party.editable)
saveSummon(party.id, summon, position) saveSummon(party.id, summon, position).then((response) =>
.then(response => storeGridSummon(response.data.grid_summon)) storeGridSummon(response.data.grid_summon)
)
} }
} }
@ -159,25 +114,26 @@ const SummonGrid = (props: Props) => {
if (summon.uncap.ulb) uncapLevel = 5 if (summon.uncap.ulb) uncapLevel = 5
else if (summon.uncap.flb) uncapLevel = 4 else if (summon.uncap.flb) uncapLevel = 4
return await api.endpoints.summons.create({ return await api.endpoints.summons.create(
'summon': { {
'party_id': partyId, summon: {
'summon_id': summon.id, party_id: partyId,
'position': position, summon_id: summon.id,
'main': (position == -1), position: position,
'friend': (position == 6), main: position == -1,
'uncap_level': uncapLevel friend: position == 6,
} uncap_level: uncapLevel,
}, headers) },
},
headers
)
} }
function storeGridSummon(gridSummon: GridSummon) { function storeGridSummon(gridSummon: GridSummon) {
if (gridSummon.position == -1) if (gridSummon.position == -1) appState.grid.summons.mainSummon = gridSummon
appState.grid.summons.mainSummon = gridSummon
else if (gridSummon.position == 6) else if (gridSummon.position == 6)
appState.grid.summons.friendSummon = gridSummon appState.grid.summons.friendSummon = gridSummon
else else appState.grid.summons.allSummons[gridSummon.position] = gridSummon
appState.grid.summons.allSummons[gridSummon.position] = gridSummon
} }
// Methods: Updating uncap level // Methods: Updating uncap level
@ -187,8 +143,9 @@ const SummonGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) if (uncapLevel != previousUncapValues[position])
await api.updateUncap('summon', id, uncapLevel) await api.updateUncap("summon", id, uncapLevel).then((response) => {
.then(response => { storeGridSummon(response.data.grid_summon) }) storeGridSummon(response.data.grid_summon)
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -196,13 +153,17 @@ const SummonGrid = (props: Props) => {
updateUncapLevel(position, previousUncapValues[position]) updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key // Remove optimistic key
let newPreviousValues = {...previousUncapValues} let newPreviousValues = { ...previousUncapValues }
delete newPreviousValues[position] delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues) setPreviousUncapValues(newPreviousValues)
} }
} }
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { function initiateUncapUpdate(
id: string,
position: number,
uncapLevel: number
) {
memoizeAction(id, position, uncapLevel) memoizeAction(id, position, uncapLevel)
// Optimistically update UI // Optimistically update UI
@ -212,13 +173,16 @@ const SummonGrid = (props: Props) => {
const memoizeAction = useCallback( const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => { (id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel) debouncedAction(id, position, uncapLevel)
}, [props, previousUncapValues] },
[props, previousUncapValues]
) )
const debouncedAction = useMemo(() => const debouncedAction = useMemo(
() =>
debounce((id, position, number) => { debounce((id, position, number) => {
saveUncap(id, position, number) saveUncap(id, position, number)
}, 500), [props, saveUncap] }, 500),
[props, saveUncap]
) )
const updateUncapLevel = (position: number, uncapLevel: number) => { const updateUncapLevel = (position: number, uncapLevel: number) => {
@ -226,17 +190,21 @@ const SummonGrid = (props: Props) => {
appState.grid.summons.mainSummon.uncap_level = uncapLevel appState.grid.summons.mainSummon.uncap_level = uncapLevel
else if (appState.grid.summons.friendSummon && position == 6) else if (appState.grid.summons.friendSummon && position == 6)
appState.grid.summons.friendSummon.uncap_level = uncapLevel appState.grid.summons.friendSummon.uncap_level = uncapLevel
else else appState.grid.summons.allSummons[position].uncap_level = uncapLevel
appState.grid.summons.allSummons[position].uncap_level = uncapLevel
} }
function storePreviousUncapValue(position: number) { function storePreviousUncapValue(position: number) {
// Save the current value in case of an unexpected result // Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues} let newPreviousValues = { ...previousUncapValues }
if (appState.grid.summons.mainSummon && position == -1) newPreviousValues[position] = appState.grid.summons.mainSummon.uncap_level if (appState.grid.summons.mainSummon && position == -1)
else if (appState.grid.summons.friendSummon && position == 6) newPreviousValues[position] = appState.grid.summons.friendSummon.uncap_level newPreviousValues[position] = appState.grid.summons.mainSummon.uncap_level
else newPreviousValues[position] = appState.grid.summons.allSummons[position].uncap_level else if (appState.grid.summons.friendSummon && position == 6)
newPreviousValues[position] =
appState.grid.summons.friendSummon.uncap_level
else
newPreviousValues[position] =
appState.grid.summons.allSummons[position].uncap_level
setPreviousUncapValues(newPreviousValues) setPreviousUncapValues(newPreviousValues)
} }
@ -244,7 +212,7 @@ const SummonGrid = (props: Props) => {
// Render: JSX components // Render: JSX components
const mainSummonElement = ( const mainSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
<div className="Label">{t('summons.main')}</div> <div className="Label">{t("summons.main")}</div>
<SummonUnit <SummonUnit
gridSummon={grid.summons.mainSummon} gridSummon={grid.summons.mainSummon}
editable={party.editable} editable={party.editable}
@ -259,7 +227,7 @@ const SummonGrid = (props: Props) => {
const friendSummonElement = ( const friendSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
<div className="Label">{t('summons.friend')}</div> <div className="Label">{t("summons.friend")}</div>
<SummonUnit <SummonUnit
gridSummon={grid.summons.friendSummon} gridSummon={grid.summons.friendSummon}
editable={party.editable} editable={party.editable}
@ -273,10 +241,11 @@ const SummonGrid = (props: Props) => {
) )
const summonGridElement = ( const summonGridElement = (
<div id="LabeledGrid"> <div id="LabeledGrid">
<div className="Label">{t('summons.summons')}</div> <div className="Label">{t("summons.summons")}</div>
<ul id="grid_summons"> <ul id="grid_summons">
{Array.from(Array(numSummons)).map((x, i) => { {Array.from(Array(numSummons)).map((x, i) => {
return (<li key={`grid_unit_${i}`} > return (
<li key={`grid_unit_${i}`}>
<SummonUnit <SummonUnit
gridSummon={grid.summons.allSummons[i]} gridSummon={grid.summons.allSummons[i]}
editable={party.editable} editable={party.editable}
@ -285,7 +254,8 @@ const SummonGrid = (props: Props) => {
updateObject={receiveSummonFromSearch} updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
/> />
</li>) </li>
)
})} })}
</ul> </ul>
</div> </div>
@ -303,12 +273,12 @@ const SummonGrid = (props: Props) => {
return ( return (
<div> <div>
<div id="SummonGrid"> <div id="SummonGrid">
{ mainSummonElement } {mainSummonElement}
{ friendSummonElement } {friendSummonElement}
{ summonGridElement } {summonGridElement}
</div> </div>
{ subAuraSummonElement } {subAuraSummonElement}
</div> </div>
) )
} }

View file

@ -1,48 +1,51 @@
import React, { useEffect } from 'react' import React from "react"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import { useCookies } from 'react-cookie' import { getCookie, deleteCookie } from "cookies-next"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useTranslation } from 'next-i18next' import { useTranslation } from "next-i18next"
import clonedeep from 'lodash.clonedeep' import clonedeep from "lodash.clonedeep"
import api from '~utils/api' import api from "~utils/api"
import { accountState, initialAccountState } from '~utils/accountState' import { accountState, initialAccountState } from "~utils/accountState"
import { appState, initialAppState } from '~utils/appState' import { appState, initialAppState } from "~utils/appState"
import Header from '~components/Header' import Header from "~components/Header"
import Button from '~components/Button' import Button from "~components/Button"
import HeaderMenu from '~components/HeaderMenu' import HeaderMenu from "~components/HeaderMenu"
const TopHeader = () => { const TopHeader = () => {
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Cookies // Cookies
const [accountCookies, setAccountCookie, removeAccountCookie] = useCookies(['account']) const accountCookie = getCookie("account")
const [userCookies, setUserCookies, removeUserCookie] = useCookies(['user']) const userCookie = getCookie("user")
const headers = (accountCookies.account != null) ? { const headers = {}
'Authorization': `Bearer ${accountCookies.account.access_token}` // accountCookies.account != null
} : {} // ? {
// Authorization: `Bearer ${accountCookies.account.access_token}`,
// }
// : {}
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState)
const router = useRouter() const router = useRouter()
function copyToClipboard() { function copyToClipboard() {
const el = document.createElement('input') const el = document.createElement("input")
el.value = window.location.href el.value = window.location.href
el.id = 'url-input' el.id = "url-input"
document.body.appendChild(el) document.body.appendChild(el)
el.select() el.select()
document.execCommand('copy') document.execCommand("copy")
el.remove() el.remove()
} }
function newParty() { function newParty() {
// Push the root URL // Push the root URL
router.push('/') router.push("/")
// Clean state // Clean state
const resetState = clonedeep(initialAppState) const resetState = clonedeep(initialAppState)
@ -55,93 +58,97 @@ const TopHeader = () => {
} }
function logout() { function logout() {
removeAccountCookie('account') deleteCookie("account")
removeUserCookie('user') deleteCookie("user")
// Clean state // Clean state
const resetState = clonedeep(initialAccountState) const resetState = clonedeep(initialAccountState)
Object.keys(resetState).forEach((key) => { Object.keys(resetState).forEach((key) => {
if (key !== 'language') if (key !== "language") accountState[key] = resetState[key]
accountState[key] = resetState[key]
}) })
if (router.route != '/new') if (router.route != "/new") appState.party.editable = false
appState.party.editable = false
router.push('/') router.push("/")
return false return false
} }
function toggleFavorite() { function toggleFavorite() {
if (party.favorited) if (party.favorited) unsaveFavorite()
unsaveFavorite() else saveFavorite()
else
saveFavorite()
} }
function saveFavorite() { function saveFavorite() {
if (party.id) if (party.id)
api.saveTeam({ id: party.id, params: headers }) api.saveTeam({ id: party.id, params: headers }).then((response) => {
.then((response) => { if (response.status == 201) appState.party.favorited = true
if (response.status == 201)
appState.party.favorited = true
}) })
else else console.error("Failed to save team: No party ID")
console.error("Failed to save team: No party ID")
} }
function unsaveFavorite() { function unsaveFavorite() {
if (party.id) if (party.id)
api.unsaveTeam({ id: party.id, params: headers }) api.unsaveTeam({ id: party.id, params: headers }).then((response) => {
.then((response) => { if (response.status == 200) appState.party.favorited = false
if (response.status == 200)
appState.party.favorited = false
}) })
else else console.error("Failed to unsave team: No party ID")
console.error("Failed to unsave team: No party ID")
} }
const leftNav = () => { const leftNav = () => {
return ( return (
<div className="dropdown"> <div className="dropdown">
<Button icon="menu">{t('buttons.menu')}</Button> <Button icon="menu">{t("buttons.menu")}</Button>
{ (account.user) ? {account.user ? (
<HeaderMenu authenticated={account.authorized} username={account.user.username} logout={logout} /> : <HeaderMenu
authenticated={account.authorized}
username={account.user.username}
logout={logout}
/>
) : (
<HeaderMenu authenticated={account.authorized} /> <HeaderMenu authenticated={account.authorized} />
} )}
</div> </div>
) )
} }
const saveButton = () => { const saveButton = () => {
if (party.favorited) if (party.favorited)
return (<Button icon="save" active={true} onClick={toggleFavorite}>Saved</Button>) return (
<Button icon="save" active={true} onClick={toggleFavorite}>
Saved
</Button>
)
else else
return (<Button icon="save" onClick={toggleFavorite}>Save</Button>) return (
<Button icon="save" onClick={toggleFavorite}>
Save
</Button>
)
} }
const rightNav = () => { const rightNav = () => {
return ( return (
<div> <div>
{ (router.route === '/p/[party]' && account.user && (!party.user || party.user.id !== account.user.id)) ? {router.route === "/p/[party]" &&
saveButton() : '' account.user &&
} (!party.user || party.user.id !== account.user.id)
{ (router.route === '/p/[party]') ? ? saveButton()
<Button icon="link" onClick={copyToClipboard}>{t('buttons.copy')}</Button> : '' : ""}
} {router.route === "/p/[party]" ? (
<Button icon="new" onClick={newParty}>{t('buttons.new')}</Button> <Button icon="link" onClick={copyToClipboard}>
{t("buttons.copy")}
</Button>
) : (
""
)}
<Button icon="new" onClick={newParty}>
{t("buttons.new")}
</Button>
</div> </div>
) )
} }
return <Header position="top" left={leftNav()} right={rightNav()} />
return (
<Header
position="top"
left={ leftNav() }
right={ rightNav() }
/>
)
} }
export default TopHeader export default TopHeader

View file

@ -1,23 +1,23 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie } from "cookies-next"
import { useSnapshot } from 'valtio' import { useSnapshot } from "valtio"
import { AxiosResponse } from 'axios' import { AxiosResponse } from "axios"
import debounce from 'lodash.debounce' import debounce from "lodash.debounce"
import WeaponUnit from '~components/WeaponUnit' import WeaponUnit from "~components/WeaponUnit"
import ExtraWeapons from '~components/ExtraWeapons' import ExtraWeapons from "~components/ExtraWeapons"
import api from '~utils/api' import api from "~utils/api"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import './index.scss' import "./index.scss"
// Props // Props
interface Props { interface Props {
new: boolean new: boolean
slug?: string weapons?: GridWeapon[]
createParty: (extra: boolean) => Promise<AxiosResponse<any, any>> createParty: (extra: boolean) => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void pushHistory?: (path: string) => void
} }
@ -27,125 +27,73 @@ const WeaponGrid = (props: Props) => {
const numWeapons: number = 9 const numWeapons: number = 9
// Cookies // Cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account != null) ? { const accountData: AccountCookie = cookie
headers: { ? JSON.parse(cookie as string)
'Authorization': `Bearer ${cookies.account.access_token}` : 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)
const [slug, setSlug] = useState() const [slug, setSlug] = useState()
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
const [firstLoadComplete, setFirstLoadComplete] = useState(false)
// Create a temporary state to store previous weapon uncap values // Create a temporary state to store previous weapon uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) const [previousUncapValues, setPreviousUncapValues] = useState<{
[key: number]: number
// Fetch data from the server }>({})
useEffect(() => {
const shortcode = (props.slug) ? props.slug : slug
if (shortcode) fetchGrid(shortcode)
else appState.party.editable = true
}, [slug, props.slug])
// Set the editable flag only on first load // Set the editable flag only on first load
useEffect(() => { useEffect(() => {
if (!loading && !firstLoadComplete) {
// If user is logged in and matches // If user is logged in and matches
if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new) if (
(accountData && party.user && accountData.userId === party.user.id) ||
props.new
)
appState.party.editable = true appState.party.editable = true
else else appState.party.editable = false
appState.party.editable = false }, [props.new, accountData, party])
setFirstLoadComplete(true)
}
}, [props.new, cookies, party, loading, firstLoadComplete])
// Initialize an array of current uncap values for each weapon // Initialize an array of current uncap values for each weapon
useEffect(() => { useEffect(() => {
let initialPreviousUncapValues: {[key: number]: number} = {} let initialPreviousUncapValues: { [key: number]: number } = {}
if (appState.grid.weapons.mainWeapon) if (appState.grid.weapons.mainWeapon)
initialPreviousUncapValues[-1] = appState.grid.weapons.mainWeapon.uncap_level initialPreviousUncapValues[-1] =
appState.grid.weapons.mainWeapon.uncap_level
Object.values(appState.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) Object.values(appState.grid.weapons.allWeapons).map(
(o) => (initialPreviousUncapValues[o.position] = o.uncap_level)
)
setPreviousUncapValues(initialPreviousUncapValues) setPreviousUncapValues(initialPreviousUncapValues)
}, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons]) }, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons])
// Methods: Fetching an object from the server
async function fetchGrid(shortcode: string) {
return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'weapons', params: headers })
.then(response => processResult(response))
.catch(error => processError(error))
}
function processResult(response: AxiosResponse) {
// Store the response
const party: Party = response.data.party
// Store the important party and state-keeping values
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
setFound(true)
setLoading(false)
// Populate the weapons in state
populateWeapons(party.weapons)
}
function processError(error: any) {
if (error.response != null) {
if (error.response.status == 404) {
setFound(false)
setLoading(false)
}
} else {
console.error(error)
}
}
function populateWeapons(list: Array<GridWeapon>) {
list.forEach((gridObject: GridWeapon) => {
if (gridObject.mainhand) {
appState.grid.weapons.mainWeapon = gridObject
appState.party.element = gridObject.object.element
} else if (!gridObject.mainhand && gridObject.position != null) {
appState.grid.weapons.allWeapons[gridObject.position] = gridObject
}
})
}
// Methods: Adding an object from search // Methods: Adding an object from search
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) { function receiveWeaponFromSearch(
object: Character | Weapon | Summon,
position: number
) {
const weapon = object as Weapon const weapon = object as Weapon
if (position == 1) if (position == 1) appState.party.element = weapon.element
appState.party.element = weapon.element
if (!party.id) { if (!party.id) {
props.createParty(party.extra) props.createParty(party.extra).then((response) => {
.then(response => {
const party = response.data.party const party = response.data.party
appState.party.id = party.id appState.party.id = party.id
setSlug(party.shortcode) setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveWeapon(party.id, weapon, position) saveWeapon(party.id, weapon, position).then((response) =>
.then(response => storeGridWeapon(response.data.grid_weapon)) storeGridWeapon(response.data.grid_weapon)
)
}) })
} else { } else {
saveWeapon(party.id, weapon, position) saveWeapon(party.id, weapon, position).then((response) =>
.then(response => storeGridWeapon(response.data.grid_weapon)) storeGridWeapon(response.data.grid_weapon)
)
} }
} }
@ -154,15 +102,18 @@ const WeaponGrid = (props: Props) => {
if (weapon.uncap.ulb) uncapLevel = 5 if (weapon.uncap.ulb) uncapLevel = 5
else if (weapon.uncap.flb) uncapLevel = 4 else if (weapon.uncap.flb) uncapLevel = 4
return await api.endpoints.weapons.create({ return await api.endpoints.weapons.create(
'weapon': { {
'party_id': partyId, weapon: {
'weapon_id': weapon.id, party_id: partyId,
'position': position, weapon_id: weapon.id,
'mainhand': (position == -1), position: position,
'uncap_level': uncapLevel mainhand: position == -1,
} uncap_level: uncapLevel,
}, headers) },
},
headers
)
} }
function storeGridWeapon(gridWeapon: GridWeapon) { function storeGridWeapon(gridWeapon: GridWeapon) {
@ -182,8 +133,9 @@ const WeaponGrid = (props: Props) => {
try { try {
if (uncapLevel != previousUncapValues[position]) if (uncapLevel != previousUncapValues[position])
await api.updateUncap('weapon', id, uncapLevel) await api.updateUncap("weapon", id, uncapLevel).then((response) => {
.then(response => { storeGridWeapon(response.data.grid_weapon) }) storeGridWeapon(response.data.grid_weapon)
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -191,13 +143,17 @@ const WeaponGrid = (props: Props) => {
updateUncapLevel(position, previousUncapValues[position]) updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key // Remove optimistic key
let newPreviousValues = {...previousUncapValues} let newPreviousValues = { ...previousUncapValues }
delete newPreviousValues[position] delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues) setPreviousUncapValues(newPreviousValues)
} }
} }
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { function initiateUncapUpdate(
id: string,
position: number,
uncapLevel: number
) {
memoizeAction(id, position, uncapLevel) memoizeAction(id, position, uncapLevel)
// Optimistically update UI // Optimistically update UI
@ -207,27 +163,31 @@ const WeaponGrid = (props: Props) => {
const memoizeAction = useCallback( const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => { (id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel) debouncedAction(id, position, uncapLevel)
}, [props, previousUncapValues] },
[props, previousUncapValues]
) )
const debouncedAction = useMemo(() => const debouncedAction = useMemo(
() =>
debounce((id, position, number) => { debounce((id, position, number) => {
saveUncap(id, position, number) saveUncap(id, position, number)
}, 500), [props, saveUncap] }, 500),
[props, saveUncap]
) )
const updateUncapLevel = (position: number, uncapLevel: number) => { const updateUncapLevel = (position: number, uncapLevel: number) => {
if (appState.grid.weapons.mainWeapon && position == -1) if (appState.grid.weapons.mainWeapon && position == -1)
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel appState.grid.weapons.mainWeapon.uncap_level = uncapLevel
else else appState.grid.weapons.allWeapons[position].uncap_level = uncapLevel
appState.grid.weapons.allWeapons[position].uncap_level = uncapLevel
} }
function storePreviousUncapValue(position: number) { function storePreviousUncapValue(position: number) {
// Save the current value in case of an unexpected result // Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues} let newPreviousValues = { ...previousUncapValues }
newPreviousValues[position] = (appState.grid.weapons.mainWeapon && position == -1) ? newPreviousValues[position] =
appState.grid.weapons.mainWeapon.uncap_level : appState.grid.weapons.allWeapons[position].uncap_level appState.grid.weapons.mainWeapon && position == -1
? appState.grid.weapons.mainWeapon.uncap_level
: appState.grid.weapons.allWeapons[position].uncap_level
setPreviousUncapValues(newPreviousValues) setPreviousUncapValues(newPreviousValues)
} }
@ -244,10 +204,9 @@ const WeaponGrid = (props: Props) => {
/> />
) )
const weaponGridElement = ( const weaponGridElement = Array.from(Array(numWeapons)).map((x, i) => {
Array.from(Array(numWeapons)).map((x, i) => {
return ( return (
<li key={`grid_unit_${i}`} > <li key={`grid_unit_${i}`}>
<WeaponUnit <WeaponUnit
gridWeapon={appState.grid.weapons.allWeapons[i]} gridWeapon={appState.grid.weapons.allWeapons[i]}
editable={party.editable} editable={party.editable}
@ -259,7 +218,6 @@ const WeaponGrid = (props: Props) => {
</li> </li>
) )
}) })
)
const extraGridElement = ( const extraGridElement = (
<ExtraWeapons <ExtraWeapons
@ -274,11 +232,13 @@ const WeaponGrid = (props: Props) => {
return ( return (
<div id="WeaponGrid"> <div id="WeaponGrid">
<div id="MainGrid"> <div id="MainGrid">
{ mainhandElement } {mainhandElement}
<ul className="grid_weapons">{ weaponGridElement }</ul> <ul className="grid_weapons">{weaponGridElement}</ul>
</div> </div>
{ (() => { return (party.extra) ? extraGridElement : '' })() } {(() => {
return party.extra ? extraGridElement : ""
})()}
</div> </div>
) )
} }

View file

@ -1,21 +1,21 @@
import React, { useState } from 'react' import React, { useState } from "react"
import { useCookies } from 'react-cookie' import { getCookie } from "cookies-next"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useTranslation } from 'next-i18next' import { useTranslation } from "next-i18next"
import { AxiosResponse } from 'axios' import { AxiosResponse } from "axios"
import * as Dialog from '@radix-ui/react-dialog' import * as Dialog from "@radix-ui/react-dialog"
import AXSelect from '~components/AxSelect' import AXSelect from "~components/AxSelect"
import ElementToggle from '~components/ElementToggle' import ElementToggle from "~components/ElementToggle"
import WeaponKeyDropdown from '~components/WeaponKeyDropdown' import WeaponKeyDropdown from "~components/WeaponKeyDropdown"
import Button from '~components/Button' import Button from "~components/Button"
import api from '~utils/api' import api from "~utils/api"
import { appState } from '~utils/appState' import { appState } from "~utils/appState"
import CrossIcon from '~public/icons/Cross.svg' import CrossIcon from "~public/icons/Cross.svg"
import './index.scss' import "./index.scss"
interface GridWeaponObject { interface GridWeaponObject {
weapon: { weapon: {
@ -37,16 +37,18 @@ interface Props {
const WeaponModal = (props: Props) => { const WeaponModal = (props: Props) => {
const router = useRouter() const router = useRouter()
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en' const locale =
const { t } = useTranslation('common') router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
const { t } = useTranslation("common")
// Cookies // Cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account != null) ? { const accountData: AccountCookie = cookie
headers: { ? JSON.parse(cookie as string)
'Authorization': `Bearer ${cookies.account.access_token}` : null
} const headers = accountData
} : {} ? { Authorization: `Bearer ${accountData.token}` }
: {}
// Refs // Refs
const weaponKey1Select = React.createRef<HTMLSelectElement>() const weaponKey1Select = React.createRef<HTMLSelectElement>()
@ -63,7 +65,12 @@ const WeaponModal = (props: Props) => {
const [primaryAxValue, setPrimaryAxValue] = useState(0.0) const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0) const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
function receiveAxValues(primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) { function receiveAxValues(
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) {
setPrimaryAxModifier(primaryAxModifier) setPrimaryAxModifier(primaryAxModifier)
setSecondaryAxModifier(secondaryAxModifier) setSecondaryAxModifier(secondaryAxModifier)
@ -82,8 +89,7 @@ const WeaponModal = (props: Props) => {
function prepareObject() { function prepareObject() {
let object: GridWeaponObject = { weapon: {} } let object: GridWeaponObject = { weapon: {} }
if (props.gridWeapon.object.element == 0) if (props.gridWeapon.object.element == 0) object.weapon.element = element
object.weapon.element = element
if ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) if ([2, 3, 17, 24].includes(props.gridWeapon.object.series))
object.weapon.weapon_key1_id = weaponKey1Select.current?.value object.weapon.weapon_key1_id = weaponKey1Select.current?.value
@ -106,18 +112,17 @@ const WeaponModal = (props: Props) => {
async function updateWeapon() { async function updateWeapon() {
const updateObject = prepareObject() const updateObject = prepareObject()
return await api.endpoints.grid_weapons.update(props.gridWeapon.id, updateObject, headers) return await api.endpoints.grid_weapons
.then(response => processResult(response)) .update(props.gridWeapon.id, updateObject, headers)
.catch(error => processError(error)) .then((response) => processResult(response))
.catch((error) => processError(error))
} }
function processResult(response: AxiosResponse) { function processResult(response: AxiosResponse) {
const gridWeapon: GridWeapon = response.data.grid_weapon const gridWeapon: GridWeapon = response.data.grid_weapon
if (gridWeapon.mainhand) if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon
appState.grid.weapons.mainWeapon = gridWeapon else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
else
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
setOpen(false) setOpen(false)
} }
@ -129,7 +134,7 @@ const WeaponModal = (props: Props) => {
const elementSelect = () => { const elementSelect = () => {
return ( return (
<section> <section>
<h3>{t('modals.weapon.subtitles.element')}</h3> <h3>{t("modals.weapon.subtitles.element")}</h3>
<ElementToggle <ElementToggle
currentElement={props.gridWeapon.element} currentElement={props.gridWeapon.element}
sendValue={receiveElementValue} sendValue={receiveElementValue}
@ -141,30 +146,51 @@ const WeaponModal = (props: Props) => {
const keySelect = () => { const keySelect = () => {
return ( return (
<section> <section>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3> <h3>{t("modals.weapon.subtitles.weapon_keys")}</h3>
{ ([2, 3, 17, 22].includes(props.gridWeapon.object.series)) ? {[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? (
<WeaponKeyDropdown <WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[0] : undefined } currentValue={
props.gridWeapon.weapon_keys
? props.gridWeapon.weapon_keys[0]
: undefined
}
series={props.gridWeapon.object.series} series={props.gridWeapon.object.series}
slot={0} slot={0}
ref={weaponKey1Select} /> ref={weaponKey1Select}
: ''} />
) : (
""
)}
{ ([2, 3, 17].includes(props.gridWeapon.object.series)) ? {[2, 3, 17].includes(props.gridWeapon.object.series) ? (
<WeaponKeyDropdown <WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[1] : undefined } currentValue={
props.gridWeapon.weapon_keys
? props.gridWeapon.weapon_keys[1]
: undefined
}
series={props.gridWeapon.object.series} series={props.gridWeapon.object.series}
slot={1} slot={1}
ref={weaponKey2Select} /> ref={weaponKey2Select}
: ''} />
) : (
""
)}
{ (props.gridWeapon.object.series == 17) ? {props.gridWeapon.object.series == 17 ? (
<WeaponKeyDropdown <WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[2] : undefined } currentValue={
props.gridWeapon.weapon_keys
? props.gridWeapon.weapon_keys[2]
: undefined
}
series={props.gridWeapon.object.series} series={props.gridWeapon.object.series}
slot={2} slot={2}
ref={weaponKey3Select} /> ref={weaponKey3Select}
: ''} />
) : (
""
)}
</section> </section>
) )
} }
@ -172,7 +198,7 @@ const WeaponModal = (props: Props) => {
const axSelect = () => { const axSelect = () => {
return ( return (
<section> <section>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3> <h3>{t("modals.weapon.subtitles.ax_skills")}</h3>
<AXSelect <AXSelect
axType={props.gridWeapon.object.ax} axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax} currentSkills={props.gridWeapon.ax}
@ -190,15 +216,20 @@ const WeaponModal = (props: Props) => {
return ( return (
<Dialog.Root open={open} onOpenChange={openChange}> <Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
{ props.children }
</Dialog.Trigger>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Content className="Weapon Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }> <Dialog.Content
className="Weapon Dialog"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div className="DialogHeader"> <div className="DialogHeader">
<div className="DialogTop"> <div className="DialogTop">
<Dialog.Title className="SubTitle">{t('modals.weapon.title')}</Dialog.Title> <Dialog.Title className="SubTitle">
<Dialog.Title className="DialogTitle">{props.gridWeapon.object.name[locale]}</Dialog.Title> {t("modals.weapon.title")}
</Dialog.Title>
<Dialog.Title className="DialogTitle">
{props.gridWeapon.object.name[locale]}
</Dialog.Title>
</div> </div>
<Dialog.Close className="DialogClose" asChild> <Dialog.Close className="DialogClose" asChild>
<span> <span>
@ -208,10 +239,17 @@ const WeaponModal = (props: Props) => {
</div> </div>
<div className="mods"> <div className="mods">
{ (props.gridWeapon.object.element == 0) ? elementSelect() : '' } {props.gridWeapon.object.element == 0 ? elementSelect() : ""}
{ ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) ? keySelect() : '' } {[2, 3, 17, 24].includes(props.gridWeapon.object.series)
{ (props.gridWeapon.object.ax > 0) ? axSelect() : '' } ? keySelect()
<Button onClick={updateWeapon} disabled={props.gridWeapon.object.ax > 0 && !formValid}>{t('modals.weapon.buttons.confirm')}</Button> : ""}
{props.gridWeapon.object.ax > 0 ? axSelect() : ""}
<Button
onClick={updateWeapon}
disabled={props.gridWeapon.object.ax > 0 && !formValid}
>
{t("modals.weapon.buttons.confirm")}
</Button>
</div> </div>
</Dialog.Content> </Dialog.Content>
<Dialog.Overlay className="Overlay" /> <Dialog.Overlay className="Overlay" />

95
package-lock.json generated
View file

@ -15,6 +15,7 @@
"@svgr/webpack": "^6.2.0", "@svgr/webpack": "^6.2.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cookies-next": "^2.1.1",
"i18next": "^21.6.13", "i18next": "^21.6.13",
"i18next-browser-languagedetector": "^6.1.3", "i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2", "i18next-http-backend": "^1.3.2",
@ -25,7 +26,6 @@
"next-i18next": "^10.5.0", "next-i18next": "^10.5.0",
"next-usequerystate": "^1.7.0", "next-usequerystate": "^1.7.0",
"react": "17.0.2", "react": "17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^11.15.5", "react-i18next": "^11.15.5",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
@ -3353,11 +3353,6 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"node_modules/@types/hoist-non-react-statics": { "node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@ -4050,6 +4045,26 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookies-next": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-2.1.1.tgz",
"integrity": "sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/node": "^16.10.2",
"cookie": "^0.4.0"
}
},
"node_modules/cookies-next/node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/cookies-next/node_modules/@types/node": {
"version": "16.18.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz",
"integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg=="
},
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.21.1", "version": "3.21.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz",
@ -6333,19 +6348,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-cookie": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz",
"integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
"universal-cookie": "^4.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@ -7184,15 +7186,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"dependencies": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"node_modules/uri-js": { "node_modules/uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@ -9691,11 +9684,6 @@
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
}, },
"@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/hoist-non-react-statics": { "@types/hoist-non-react-statics": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@ -10218,6 +10206,28 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
}, },
"cookies-next": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-2.1.1.tgz",
"integrity": "sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/node": "^16.10.2",
"cookie": "^0.4.0"
},
"dependencies": {
"@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"@types/node": {
"version": "16.18.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.3.tgz",
"integrity": "sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg=="
}
}
},
"core-js": { "core-js": {
"version": "3.21.1", "version": "3.21.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz",
@ -11886,16 +11896,6 @@
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
}, },
"react-cookie": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz",
"integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==",
"requires": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
"universal-cookie": "^4.0.0"
}
},
"react-dom": { "react-dom": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@ -12468,15 +12468,6 @@
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz",
"integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==" "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ=="
}, },
"universal-cookie": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz",
"integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==",
"requires": {
"@types/cookie": "^0.3.3",
"cookie": "^0.4.0"
}
},
"uri-js": { "uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View file

@ -20,6 +20,7 @@
"@svgr/webpack": "^6.2.0", "@svgr/webpack": "^6.2.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"cookies-next": "^2.1.1",
"i18next": "^21.6.13", "i18next": "^21.6.13",
"i18next-browser-languagedetector": "^6.1.3", "i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2", "i18next-http-backend": "^1.3.2",
@ -30,7 +31,6 @@
"next-i18next": "^10.5.0", "next-i18next": "^10.5.0",
"next-usequerystate": "^1.7.0", "next-usequerystate": "^1.7.0",
"react": "17.0.2", "react": "17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^11.15.5", "react-i18next": "^11.15.5",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",

View file

@ -1,51 +1,49 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from "react"
import Head from 'next/head' import Head from "next/head"
import { useCookies } from 'react-cookie' import { 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"
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from "next-i18next/serverSideTranslations"
import clonedeep from 'lodash.clonedeep'
import api from '~utils/api' import api from "~utils/api"
import { elements, allElement } from '~utils/Element' import useDidMountEffect from "~utils/useDidMountEffect"
import { elements, allElement } from "~utils/Element"
import GridRep from '~components/GridRep' import GridRep from "~components/GridRep"
import GridRepCollection from '~components/GridRepCollection' import GridRepCollection from "~components/GridRepCollection"
import FilterBar from '~components/FilterBar' import FilterBar from "~components/FilterBar"
const emptyUser = { import type { NextApiRequest, NextApiResponse } from "next"
id: '',
username: '', interface Props {
granblueId: 0, user?: User
picture: { teams?: { count: number; total_pages: number; results: Party[] }
picture: '', raids: Raid[]
element: '' sortedRaids: Raid[][]
},
private: false,
gender: 0
} }
const ProfileRoute: React.FC = () => { const ProfileRoute: React.FC<Props> = (props: Props) => {
// Set up cookies // Set up cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account) ? { const accountData: AccountCookie = cookie
'Authorization': `Bearer ${cookies.account.access_token}` ? 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
// Import translations // Import translations
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Set up app-specific states // Set up app-specific states
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
const [raidsLoading, setRaidsLoading] = useState(true) const [raidsLoading, setRaidsLoading] = useState(true)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
@ -53,7 +51,6 @@ const ProfileRoute: React.FC = () => {
const [parties, setParties] = useState<Party[]>([]) const [parties, setParties] = useState<Party[]>([])
const [raids, setRaids] = useState<Raid[]>() const [raids, setRaids] = useState<Raid[]>()
const [raid, setRaid] = useState<Raid>() const [raid, setRaid] = useState<Raid>()
const [user, setUser] = useState<User>(emptyUser)
// Set up infinite scrolling-related states // Set up infinite scrolling-related states
const [recordCount, setRecordCount] = useState(0) const [recordCount, setRecordCount] = useState(0)
@ -65,36 +62,48 @@ const ProfileRoute: React.FC = () => {
const [element, setElement] = useQueryState("element", { const [element, setElement] = useQueryState("element", {
defaultValue: -1, defaultValue: -1,
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: value => serializeElement(value) serialize: (value) => serializeElement(value),
}) })
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" }) const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1)) 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) {
let element: TeamElement | undefined = let element: TeamElement | undefined =
(query === 'all') ? query === "all"
allElement : elements.find(element => element.name.en.toLowerCase() === query) ? allElement
return (element) ? element.id : -1 : elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
} }
function serializeElement(value: number | undefined) { function serializeElement(value: number | undefined) {
let name = '' let name = ""
if (value != undefined) { if (value != undefined) {
if (value == -1) if (value == -1) name = allElement.name.en.toLowerCase()
name = allElement.name.en.toLowerCase() else name = elements[value].name.en.toLowerCase()
else
name = elements[value].name.en.toLowerCase()
} }
return name return name
} }
// 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)
}
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount // Add scroll event listener for shadow on FilterBar on mount
useEffect(() => { useEffect(() => {
window.addEventListener("scroll", handleScroll) window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll)
}, []) }, [])
// Handle errors // Handle errors
@ -106,46 +115,43 @@ const ProfileRoute: React.FC = () => {
} }
}, []) }, [])
const fetchProfile = useCallback(({ replace }: { replace: boolean }) => { const fetchProfile = useCallback(
({ replace }: { replace: boolean }) => {
const filters = { const filters = {
params: { params: {
element: (element != -1) ? element : undefined, element: element != -1 ? element : undefined,
raid: (raid) ? raid.id : undefined, raid: raid ? raid.id : undefined,
recency: (recency != -1) ? recency : undefined, recency: recency != -1 ? recency : undefined,
page: currentPage page: currentPage,
} },
} }
if (username && !Array.isArray(username)) if (username && !Array.isArray(username)) {
api.endpoints.users.getOne({ id: username , params: {...filters, ...{ headers: headers }}}) api.endpoints.users
.then(response => { .getOne({
setUser({ id: username,
id: response.data.user.id, params: { ...filters, ...{ headers: headers } },
username: response.data.user.username,
granblueId: response.data.user.granblue_id,
picture: response.data.user.picture,
private: response.data.user.private,
gender: response.data.user.gender
}) })
.then((response) => {
setTotalPages(response.data.parties.total_pages) setTotalPages(response.data.parties.total_pages)
setRecordCount(response.data.parties.count) setRecordCount(response.data.parties.count)
if (replace) if (replace)
replaceResults(response.data.parties.count, response.data.parties.results) replaceResults(
else response.data.parties.count,
appendResults(response.data.parties.results) response.data.parties.results
)
else appendResults(response.data.parties.results)
}) })
.then(() => { .catch((error) => handleError(error))
setFound(true) }
setLoading(false) },
}) [currentPage, parties, element, raid, recency]
.catch(error => handleError(error)) )
}, [currentPage, parties, element, raid, recency])
function replaceResults(count: number, list: Party[]) { function replaceResults(count: number, list: Party[]) {
if (count > 0) { if (count > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at) ? -1 : 1)) setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else { } else {
setParties([]) setParties([])
} }
@ -157,14 +163,13 @@ const ProfileRoute: React.FC = () => {
// 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() api.endpoints.raids.getAll().then((response) => {
.then(response => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
setRaids(cleanRaids) setRaids(cleanRaids)
setRaidsLoading(false) setRaidsLoading(false)
const raid = cleanRaids.find(r => r.slug === raidSlug) const raid = cleanRaids.find((r) => r.slug === raidSlug)
setRaid(raid) setRaid(raid)
return raid return raid
@ -173,30 +178,33 @@ const ProfileRoute: React.FC = () => {
// When the element, raid or recency filter changes, // When the element, raid or recency filter changes,
// fetch all teams again. // fetch all teams again.
useEffect(() => { useDidMountEffect(() => {
if (!raidsLoading) {
setCurrentPage(1) setCurrentPage(1)
fetchProfile({ replace: true }) fetchProfile({ replace: true })
}
}, [element, raid, recency]) }, [element, raid, recency])
useEffect(() => { // When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed // Current page changed
if (currentPage > 1) if (currentPage > 1) fetchProfile({ replace: false })
fetchProfile({ replace: false }) else if (currentPage == 1) fetchProfile({ replace: true })
else if (currentPage == 1)
fetchProfile({ replace: true })
}, [currentPage]) }, [currentPage])
// Receive filters from the filter bar // Receive filters from the filter bar
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) { function receiveFilters({
if (element == 0) element,
setElement(0) raidSlug,
else if (element) recency,
setElement(element) }: {
element?: number
raidSlug?: string
recency?: number
}) {
if (element == 0) setElement(0)
else if (element) setElement(element)
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)
} }
@ -206,10 +214,8 @@ const ProfileRoute: React.FC = () => {
// Methods: Navigation // Methods: Navigation
function handleScroll() { function handleScroll() {
if (window.pageYOffset > 90) if (window.pageYOffset > 90) setScrolled(true)
setScrolled(true) else setScrolled(false)
else
setScrolled(false)
} }
function goTo(shortcode: string) { function goTo(shortcode: string) {
@ -220,7 +226,8 @@ const ProfileRoute: React.FC = () => {
function renderParties() { function renderParties() {
return parties.map((party, i) => { return parties.map((party, i) => {
return <GridRep return (
<GridRep
id={party.id} id={party.id}
shortcode={party.shortcode} shortcode={party.shortcode}
name={party.name} name={party.name}
@ -231,80 +238,194 @@ const ProfileRoute: React.FC = () => {
key={`party-${i}`} key={`party-${i}`}
onClick={goTo} onClick={goTo}
/> />
)
}) })
} }
return ( return (
<div id="Profile"> <div id="Profile">
<Head> <Head>
<title>@{user.username}&apos;s Teams</title> <title>@{props.user?.username}&apos;s Teams</title>
<meta property="og:title" content={`@${user.username}\'s Teams`} /> <meta
<meta property="og:description" content={`Browse @${user.username}\'s Teams and filter raid, element or recency`} /> property="og:title"
<meta property="og:url" content={`https://app.granblue.team/${user.username}`} /> content={`@${props.user?.username}\'s Teams`}
/>
<meta
property="og:description"
content={`Browse @${props.user?.username}\'s Teams and filter raid, element or recency`}
/>
<meta
property="og:url"
content={`https://app.granblue.team/${props.user?.username}`}
/>
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" /> <meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content={`@${user.username}\'s Teams`} /> <meta
<meta name="twitter:description" content={`Browse @${user.username}\''s Teams and filter raid, element or recency`} /> name="twitter:title"
content={`@${props.user?.username}\'s Teams`}
/>
<meta
name="twitter:description"
content={`Browse @${props.user?.username}\''s Teams and filter raid, element or recency`}
/>
</Head> </Head>
<FilterBar <FilterBar
onFilter={receiveFilters} onFilter={receiveFilters}
scrolled={scrolled} scrolled={scrolled}
element={element} element={element}
raidSlug={ (raidSlug) ? raidSlug : undefined } raidSlug={raidSlug ? raidSlug : undefined}
recency={recency}> recency={recency}
>
<div className="UserInfo"> <div className="UserInfo">
<img <img
alt={user.picture.picture} alt={props.user?.picture.picture}
className={`profile ${user.picture.element}`} className={`profile ${props.user?.picture.element}`}
srcSet={`/profile/${user.picture.picture}.png, srcSet={`/profile/${props.user?.picture.picture}.png,
/profile/${user.picture.picture}@2x.png 2x`} /profile/${props.user?.picture.picture}@2x.png 2x`}
src={`/profile/${user.picture.picture}.png`} src={`/profile/${props.user?.picture.picture}.png`}
/> />
<h1>{user.username}</h1> <h1>{props.user?.username}</h1>
</div> </div>
</FilterBar> </FilterBar>
<section> <section>
<InfiniteScroll <InfiniteScroll
dataLength={ (parties && parties.length > 0) ? parties.length : 0} dataLength={parties && parties.length > 0 ? parties.length : 0}
next={ () => setCurrentPage(currentPage + 1) } next={() => setCurrentPage(currentPage + 1)}
hasMore={totalPages > currentPage} hasMore={totalPages > currentPage}
loader={ <div id="NotFound"><h2>Loading...</h2></div> }> loader={
<GridRepCollection loading={loading}> <div id="NotFound">
{ renderParties() } <h2>Loading...</h2>
</GridRepCollection> </div>
}
>
<GridRepCollection>{renderParties()}</GridRepCollection>
</InfiniteScroll> </InfiniteScroll>
{ (parties.length == 0) ? {parties.length == 0 ? (
<div id="NotFound"> <div id="NotFound">
<h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2> <h2>{t("teams.not_found")}</h2>
</div> </div>
: '' } ) : (
""
)}
</section> </section>
</div> </div>
) )
} }
export async function getStaticPaths() { export const getServerSidePaths = async () => {
return { return {
paths: [ paths: [
// Object variant: // Object variant:
{ params: { username: 'string' } }, { params: { party: "string" } },
], ],
fallback: true, fallback: true,
} }
} }
export async function getStaticProps({ locale }: { locale: string }) { // 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}` } }
: {}
let { raids, sortedRaids } = await api.endpoints.raids
.getAll(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)
// Create filter object
const filters: {
raid?: string
element?: number
recency?: number
} = {}
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
let user: User | null = null
let teams: Party[] | null = null
if (query.username) {
const response = await api.endpoints.users.getOne({
id: query.username,
params: {
params: filters,
...headers
}
})
user = response.data.user
teams = response.data.parties
}
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), user: user,
teams: teams,
raids: raids,
sortedRaids: sortedRaids,
...(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

@ -1,40 +1,39 @@
import { useEffect } from 'react' import { useEffect } from "react"
import { useCookies, CookiesProvider } from 'react-cookie' import { getCookie } from "cookies-next"
import { appWithTranslation } from 'next-i18next' import { appWithTranslation } from "next-i18next"
import type { AppProps } from 'next/app' 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 '../styles/globals.scss' import "../styles/globals.scss"
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null
useEffect(() => { useEffect(() => {
if (cookies.account) { if (cookie) {
console.log(`Logged in as user "${cookies.account.username}"`) console.log(`Logged in as user "${cookieData.username}"`)
accountState.account.authorized = true accountState.account.authorized = true
accountState.account.user = { accountState.account.user = {
id: cookies.account.user_id, id: cookieData.userId,
username: cookies.account.username, username: cookieData.username,
picture: '', picture: "",
element: '', element: "",
gender: 0 gender: 0,
} }
} else { } else {
console.log(`You are not currently logged in.`) console.log(`You are not currently logged in.`)
} }
}, [cookies.account]) }, [cookieData])
return ( return (
<CookiesProvider>
<Layout> <Layout>
<Component {...pageProps} /> <Component {...pageProps} />
</Layout> </Layout>
</CookiesProvider>
) )
} }

View file

@ -1,53 +1,100 @@
import React from 'react' import React from "react"
import { useRouter } from 'next/router' 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"
const PartyRoute: React.FC = () => { import api from "~utils/api"
const { party: slug } = useRouter().query
return ( import type { NextApiRequest, NextApiResponse } from "next"
<div id="Content">
<Party slug={slug as string} />
</div>
)
// function renderNotFound() { interface Props {
// return ( party: Party
// <div id="NotFound"> raids: Raid[]
// <h2>There&apos;s no grid here.</h2> sortedRaids: Raid[][]
// <Button type="new">New grid</Button>
// </div>
// )
// }
// if (!found && !loading) {
// return renderNotFound()
// } else if (found && !loading) {
// return render()
// } else {
// return (<div />)
// }
} }
export async function getStaticPaths() { const PartyRoute: React.FC<Props> = (props: Props) => {
return (
<div id="Content">
<Party team={props.party} raids={props.sortedRaids} />
</div>
)
}
export const getServerSidePaths = async () => {
return { return {
paths: [ paths: [
// Object variant: // Object variant:
{ params: { party: 'string' } }, { params: { party: "string" } },
], ],
fallback: true, fallback: true,
} }
} }
export async function getStaticProps({ locale }: { locale: string }) { // 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}` } }
: {}
let { raids, sortedRaids } = await api.endpoints.raids
.getAll()
.then((response) => organizeRaids(response.data.map((r: any) => r.raid)))
let party: Party | null = null
if (query.party) {
let response = await api.endpoints.parties.getOne({ id: query.party, params: headers })
party = response.data.party
} else {
console.log("No party code")
}
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), party: party,
raids: raids,
sortedRaids: sortedRaids,
...(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 PartyRoute export default PartyRoute

View file

@ -1,37 +1,48 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from "react"
import Head from 'next/head' import Head from "next/head"
import { useCookies } from 'react-cookie' import { 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"
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from "next-i18next/serverSideTranslations"
import clonedeep from 'lodash.clonedeep' import clonedeep from "lodash.clonedeep"
import api from '~utils/api' import api from "~utils/api"
import { elements, allElement } from '~utils/Element' import useDidMountEffect from "~utils/useDidMountEffect"
import { elements, allElement } from "~utils/Element"
import GridRep from '~components/GridRep' import GridRep from "~components/GridRep"
import GridRepCollection from '~components/GridRepCollection' import GridRepCollection from "~components/GridRepCollection"
import FilterBar from '~components/FilterBar' import FilterBar from "~components/FilterBar"
const SavedRoute: React.FC = () => { import type { NextApiRequest, NextApiResponse } from "next"
interface Props {
teams?: { count: number; total_pages: number; results: Party[] }
raids: Raid[]
sortedRaids: Raid[][]
}
const SavedRoute: React.FC<Props> = (props: Props) => {
// Set up cookies // Set up cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account) ? { const accountData: AccountCookie = cookie
'Authorization': `Bearer ${cookies.account.access_token}` ? 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()
// Import translations // Import translations
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Set up app-specific states // Set up app-specific states
const [loading, setLoading] = useState(true)
const [raidsLoading, setRaidsLoading] = useState(true) const [raidsLoading, setRaidsLoading] = useState(true)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
@ -50,36 +61,48 @@ const SavedRoute: React.FC = () => {
const [element, setElement] = useQueryState("element", { const [element, setElement] = useQueryState("element", {
defaultValue: -1, defaultValue: -1,
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: value => serializeElement(value) serialize: (value) => serializeElement(value),
}) })
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" }) const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1)) 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) {
let element: TeamElement | undefined = let element: TeamElement | undefined =
(query === 'all') ? query === "all"
allElement : elements.find(element => element.name.en.toLowerCase() === query) ? allElement
return (element) ? element.id : -1 : elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
} }
function serializeElement(value: number | undefined) { function serializeElement(value: number | undefined) {
let name = '' let name = ""
if (value != undefined) { if (value != undefined) {
if (value == -1) if (value == -1) name = allElement.name.en.toLowerCase()
name = allElement.name.en.toLowerCase() else name = elements[value].name.en.toLowerCase()
else
name = elements[value].name.en.toLowerCase()
} }
return name return name
} }
// 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)
}
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount // Add scroll event listener for shadow on FilterBar on mount
useEffect(() => { useEffect(() => {
window.addEventListener("scroll", handleScroll) window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll)
}, []) }, [])
// Handle errors // Handle errors
@ -91,31 +114,31 @@ const SavedRoute: React.FC = () => {
} }
}, []) }, [])
const fetchTeams = useCallback(({ replace }: { replace: boolean }) => { const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => {
const filters = { const filters = {
params: { params: {
element: (element != -1) ? element : undefined, element: element != -1 ? element : undefined,
raid: (raid) ? raid.id : undefined, raid: raid ? raid.id : undefined,
recency: (recency != -1) ? recency : undefined, recency: recency != -1 ? recency : undefined,
page: currentPage page: currentPage,
} },
} }
api.savedTeams({...filters, ...{ headers: headers }}) api
.then(response => { .savedTeams({ ...filters, ...{ headers: headers } })
.then((response) => {
setTotalPages(response.data.total_pages) setTotalPages(response.data.total_pages)
setRecordCount(response.data.count) setRecordCount(response.data.count)
if (replace) if (replace)
replaceResults(response.data.count, response.data.results) replaceResults(response.data.count, response.data.results)
else else appendResults(response.data.results)
appendResults(response.data.results)
}) })
.then(() => { .catch((error) => handleError(error))
setLoading(false) },
}) [currentPage, parties, element, raid, recency]
.catch(error => handleError(error)) )
}, [currentPage, parties, element, raid, recency])
function replaceResults(count: number, list: Party[]) { function replaceResults(count: number, list: Party[]) {
if (count > 0) { if (count > 0) {
@ -131,14 +154,13 @@ const SavedRoute: React.FC = () => {
// 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() api.endpoints.raids.getAll().then((response) => {
.then(response => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
setRaids(cleanRaids) setRaids(cleanRaids)
setRaidsLoading(false) setRaidsLoading(false)
const raid = cleanRaids.find(r => r.slug === raidSlug) const raid = cleanRaids.find((r) => r.slug === raidSlug)
setRaid(raid) setRaid(raid)
return raid return raid
@ -147,30 +169,33 @@ const SavedRoute: React.FC = () => {
// When the element, raid or recency filter changes, // When the element, raid or recency filter changes,
// fetch all teams again. // fetch all teams again.
useEffect(() => { useDidMountEffect(() => {
if (!raidsLoading) {
setCurrentPage(1) setCurrentPage(1)
fetchTeams({ replace: true }) fetchTeams({ replace: true })
}
}, [element, raid, recency]) }, [element, raid, recency])
useEffect(() => { // When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed // Current page changed
if (currentPage > 1) if (currentPage > 1) fetchTeams({ replace: false })
fetchTeams({ replace: false }) else if (currentPage == 1) fetchTeams({ replace: true })
else if (currentPage == 1)
fetchTeams({ replace: true })
}, [currentPage]) }, [currentPage])
// Receive filters from the filter bar // Receive filters from the filter bar
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) { function receiveFilters({
if (element == 0) element,
setElement(0) raidSlug,
else if (element) recency,
setElement(element) }: {
element?: number
raidSlug?: string
recency?: number
}) {
if (element == 0) setElement(0)
else if (element) setElement(element)
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)
} }
@ -180,17 +205,14 @@ const SavedRoute: React.FC = () => {
// Methods: Favorites // Methods: Favorites
function toggleFavorite(teamId: string, favorited: boolean) { function toggleFavorite(teamId: string, favorited: boolean) {
if (favorited) if (favorited) unsaveFavorite(teamId)
unsaveFavorite(teamId) else saveFavorite(teamId)
else
saveFavorite(teamId)
} }
function saveFavorite(teamId: string) { function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId, params: headers }) api.saveTeam({ id: teamId, params: headers }).then((response) => {
.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]
party.favorited = true party.favorited = true
@ -204,10 +226,9 @@ const SavedRoute: React.FC = () => {
} }
function unsaveFavorite(teamId: string) { function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId, params: headers }) api.unsaveTeam({ id: teamId, params: headers }).then((response) => {
.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]
party.favorited = false party.favorited = false
@ -222,10 +243,8 @@ const SavedRoute: React.FC = () => {
// Methods: Navigation // Methods: Navigation
function handleScroll() { function handleScroll() {
if (window.pageYOffset > 90) if (window.pageYOffset > 90) setScrolled(true)
setScrolled(true) else setScrolled(false)
else
setScrolled(false)
} }
function goTo(shortcode: string) { function goTo(shortcode: string) {
@ -234,7 +253,8 @@ const SavedRoute: React.FC = () => {
function renderParties() { function renderParties() {
return parties.map((party, i) => { return parties.map((party, i) => {
return <GridRep return (
<GridRep
id={party.id} id={party.id}
shortcode={party.shortcode} shortcode={party.shortcode}
name={party.name} name={party.name}
@ -246,14 +266,16 @@ const SavedRoute: React.FC = () => {
key={`party-${i}`} key={`party-${i}`}
displayUser={true} displayUser={true}
onClick={goTo} onClick={goTo}
onSave={toggleFavorite} /> onSave={toggleFavorite}
/>
)
}) })
} }
return ( return (
<div id="Teams"> <div id="Teams">
<Head> <Head>
<title>{t('saved.title')}</title> <title>{t("saved.title")}</title>
<meta property="og:title" content="Your saved Teams" /> <meta property="og:title" content="Your saved Teams" />
<meta property="og:url" content="https://app.granblue.team/saved" /> <meta property="og:url" content="https://app.granblue.team/saved" />
@ -268,39 +290,136 @@ const SavedRoute: React.FC = () => {
onFilter={receiveFilters} onFilter={receiveFilters}
scrolled={scrolled} scrolled={scrolled}
element={element} element={element}
raidSlug={ (raidSlug) ? raidSlug : undefined } raidSlug={raidSlug ? raidSlug : undefined}
recency={recency}> recency={recency}
<h1>{t('saved.title')}</h1> >
<h1>{t("saved.title")}</h1>
</FilterBar> </FilterBar>
<section> <section>
<InfiniteScroll <InfiniteScroll
dataLength={ (parties && parties.length > 0) ? parties.length : 0} dataLength={parties && parties.length > 0 ? parties.length : 0}
next={ () => setCurrentPage(currentPage + 1) } next={() => setCurrentPage(currentPage + 1)}
hasMore={totalPages > currentPage} hasMore={totalPages > currentPage}
loader={ <div id="NotFound"><h2>Loading...</h2></div> }> loader={
<GridRepCollection loading={loading}> <div id="NotFound">
{ renderParties() } <h2>Loading...</h2>
</GridRepCollection> </div>
}
>
<GridRepCollection>{renderParties()}</GridRepCollection>
</InfiniteScroll> </InfiniteScroll>
{ (parties.length == 0) ? {parties.length == 0 ? (
<div id="NotFound"> <div id="NotFound">
<h2>{ (loading) ? t('saved.loading') : t('saved.not_found') }</h2> <h2>{t("saved.not_found")}</h2>
</div> </div>
: '' } ) : (
""
)}
</section> </section>
</div> </div>
) )
} }
export async function getStaticProps({ locale }: { locale: string }) { export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: "string" } },
],
fallback: true,
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// 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}` } }
: {}
let { raids, sortedRaids } = await api.endpoints.raids
.getAll(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)
// Create filter object
const filters: {
raid?: string
element?: number
recency?: number
} = {}
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
const response = await api.savedTeams({
params: filters,
...headers
})
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), teams: response.data,
raids: raids,
sortedRaids: sortedRaids,
...(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,37 +1,48 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from "react"
import Head from 'next/head' import Head from "next/head"
import { useCookies } from 'react-cookie' import { 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"
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from "next-i18next/serverSideTranslations"
import clonedeep from 'lodash.clonedeep' import clonedeep from "lodash.clonedeep"
import api from '~utils/api' import api from "~utils/api"
import { elements, allElement } from '~utils/Element' import useDidMountEffect from "~utils/useDidMountEffect"
import { elements, allElement } from "~utils/Element"
import GridRep from '~components/GridRep' import GridRep from "~components/GridRep"
import GridRepCollection from '~components/GridRepCollection' import GridRepCollection from "~components/GridRepCollection"
import FilterBar from '~components/FilterBar' import FilterBar from "~components/FilterBar"
const TeamsRoute: React.FC = () => { import type { NextApiRequest, NextApiResponse } from "next"
interface Props {
teams?: { count: number; total_pages: number; results: Party[] }
raids: Raid[]
sortedRaids: Raid[][]
}
const TeamsRoute: React.FC<Props> = (props: Props) => {
// Set up cookies // Set up cookies
const [cookies] = useCookies(['account']) const cookie = getCookie("account")
const headers = (cookies.account) ? { const accountData: AccountCookie = cookie
'Authorization': `Bearer ${cookies.account.access_token}` ? 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()
// Import translations // Import translations
const { t } = useTranslation('common') const { t } = useTranslation("common")
// Set up app-specific states // Set up app-specific states
const [loading, setLoading] = useState(true)
const [raidsLoading, setRaidsLoading] = useState(true) const [raidsLoading, setRaidsLoading] = useState(true)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
@ -50,36 +61,48 @@ const TeamsRoute: React.FC = () => {
const [element, setElement] = useQueryState("element", { const [element, setElement] = useQueryState("element", {
defaultValue: -1, defaultValue: -1,
parse: (query: string) => parseElement(query), parse: (query: string) => parseElement(query),
serialize: value => serializeElement(value) serialize: (value) => serializeElement(value),
}) })
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" }) const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1)) 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) {
let element: TeamElement | undefined = let element: TeamElement | undefined =
(query === 'all') ? query === "all"
allElement : elements.find(element => element.name.en.toLowerCase() === query) ? allElement
return (element) ? element.id : -1 : elements.find((element) => element.name.en.toLowerCase() === query)
return element ? element.id : -1
} }
function serializeElement(value: number | undefined) { function serializeElement(value: number | undefined) {
let name = '' let name = ""
if (value != undefined) { if (value != undefined) {
if (value == -1) if (value == -1) name = allElement.name.en.toLowerCase()
name = allElement.name.en.toLowerCase() else name = elements[value].name.en.toLowerCase()
else
name = elements[value].name.en.toLowerCase()
} }
return name return name
} }
// 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)
}
setCurrentPage(1)
}, [])
// Add scroll event listener for shadow on FilterBar on mount // Add scroll event listener for shadow on FilterBar on mount
useEffect(() => { useEffect(() => {
window.addEventListener("scroll", handleScroll) window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll)
}, []) }, [])
// Handle errors // Handle errors
@ -91,35 +114,35 @@ const TeamsRoute: React.FC = () => {
} }
}, []) }, [])
const fetchTeams = useCallback(({ replace }: { replace: boolean }) => { const fetchTeams = useCallback(
({ replace }: { replace: boolean }) => {
const filters = { const filters = {
params: { params: {
element: (element != -1) ? element : undefined, element: element != -1 ? element : undefined,
raid: (raid) ? raid.id : undefined, raid: raid ? raid.id : undefined,
recency: (recency != -1) ? recency : undefined, recency: recency != -1 ? recency : undefined,
page: currentPage page: currentPage,
} },
} }
api.endpoints.parties.getAll({...filters, ...{ headers: headers }}) api.endpoints.parties
.then(response => { .getAll({ ...filters, ...{ headers: headers } })
.then((response) => {
setTotalPages(response.data.total_pages) setTotalPages(response.data.total_pages)
setRecordCount(response.data.count) setRecordCount(response.data.count)
if (replace) if (replace)
replaceResults(response.data.count, response.data.results) replaceResults(response.data.count, response.data.results)
else else appendResults(response.data.results)
appendResults(response.data.results)
}) })
.then(() => { .catch((error) => handleError(error))
setLoading(false) },
}) [currentPage, parties, element, raid, recency]
.catch(error => handleError(error)) )
}, [currentPage, parties, element, raid, recency])
function replaceResults(count: number, list: Party[]) { function replaceResults(count: number, list: Party[]) {
if (count > 0) { if (count > 0) {
setParties(list.sort((a, b) => (a.created_at > b.created_at) ? -1 : 1)) setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
} else { } else {
setParties([]) setParties([])
} }
@ -131,14 +154,13 @@ const TeamsRoute: React.FC = () => {
// 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() api.endpoints.raids.getAll().then((response) => {
.then(response => {
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid) const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
setRaids(cleanRaids) setRaids(cleanRaids)
setRaidsLoading(false) setRaidsLoading(false)
const raid = cleanRaids.find(r => r.slug === raidSlug) const raid = cleanRaids.find((r) => r.slug === raidSlug)
setRaid(raid) setRaid(raid)
return raid return raid
@ -147,30 +169,33 @@ const TeamsRoute: React.FC = () => {
// When the element, raid or recency filter changes, // When the element, raid or recency filter changes,
// fetch all teams again. // fetch all teams again.
useEffect(() => { useDidMountEffect(() => {
if (!raidsLoading) {
setCurrentPage(1) setCurrentPage(1)
fetchTeams({ replace: true }) fetchTeams({ replace: true })
}
}, [element, raid, recency]) }, [element, raid, recency])
useEffect(() => { // When the page changes, fetch all teams again.
useDidMountEffect(() => {
// Current page changed // Current page changed
if (currentPage > 1) if (currentPage > 1) fetchTeams({ replace: false })
fetchTeams({ replace: false }) else if (currentPage == 1) fetchTeams({ replace: true })
else if (currentPage == 1)
fetchTeams({ replace: true })
}, [currentPage]) }, [currentPage])
// Receive filters from the filter bar // Receive filters from the filter bar
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) { function receiveFilters({
if (element == 0) element,
setElement(0) raidSlug,
else if (element) recency,
setElement(element) }: {
element?: number
raidSlug?: string
recency?: number
}) {
if (element == 0) setElement(0)
else if (element) setElement(element)
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)
} }
@ -180,17 +205,14 @@ const TeamsRoute: React.FC = () => {
// Methods: Favorites // Methods: Favorites
function toggleFavorite(teamId: string, favorited: boolean) { function toggleFavorite(teamId: string, favorited: boolean) {
if (favorited) if (favorited) unsaveFavorite(teamId)
unsaveFavorite(teamId) else saveFavorite(teamId)
else
saveFavorite(teamId)
} }
function saveFavorite(teamId: string) { function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId, params: headers }) api.saveTeam({ id: teamId, params: headers }).then((response) => {
.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]
party.favorited = true party.favorited = true
@ -204,10 +226,9 @@ const TeamsRoute: React.FC = () => {
} }
function unsaveFavorite(teamId: string) { function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId, params: headers }) api.unsaveTeam({ id: teamId, params: headers }).then((response) => {
.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]
party.favorited = false party.favorited = false
@ -222,10 +243,8 @@ const TeamsRoute: React.FC = () => {
// Methods: Navigation // Methods: Navigation
function handleScroll() { function handleScroll() {
if (window.pageYOffset > 90) if (window.pageYOffset > 90) setScrolled(true)
setScrolled(true) else setScrolled(false)
else
setScrolled(false)
} }
function goTo(shortcode: string) { function goTo(shortcode: string) {
@ -234,7 +253,8 @@ const TeamsRoute: React.FC = () => {
function renderParties() { function renderParties() {
return parties.map((party, i) => { return parties.map((party, i) => {
return <GridRep return (
<GridRep
id={party.id} id={party.id}
shortcode={party.shortcode} shortcode={party.shortcode}
name={party.name} name={party.name}
@ -246,63 +266,168 @@ const TeamsRoute: React.FC = () => {
key={`party-${i}`} key={`party-${i}`}
displayUser={true} displayUser={true}
onClick={goTo} onClick={goTo}
onSave={toggleFavorite} /> onSave={toggleFavorite}
/>
)
}) })
} }
return ( return (
<div id="Teams"> <div id="Teams">
<Head> <Head>
<title>{ t('teams.title') }</title> <title>{t("teams.title")}</title>
<meta property="og:title" content="Discover Teams" /> <meta property="og:title" content="Discover Teams" />
<meta property="og:description" content="Find different Granblue Fantasy teams by raid, element or recency" /> <meta
property="og:description"
content="Find different Granblue Fantasy teams by raid, element or recency"
/>
<meta property="og:url" content="https://app.granblue.team/teams" /> <meta property="og:url" content="https://app.granblue.team/teams" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="app.granblue.team" /> <meta property="twitter:domain" content="app.granblue.team" />
<meta name="twitter:title" content="Discover Teams" /> <meta name="twitter:title" content="Discover Teams" />
<meta name="twitter:description" content="Find different Granblue Fantasy teams by raid, element or recency" /> <meta
name="twitter:description"
content="Find different Granblue Fantasy teams by raid, element or recency"
/>
</Head> </Head>
<FilterBar <FilterBar
onFilter={receiveFilters} onFilter={receiveFilters}
scrolled={scrolled} scrolled={scrolled}
element={element} element={element}
raidSlug={ (raidSlug) ? raidSlug : undefined } raidSlug={raidSlug ? raidSlug : undefined}
recency={recency}> recency={recency}
<h1>{t('teams.title')}</h1> >
<h1>{t("teams.title")}</h1>
</FilterBar> </FilterBar>
<section> <section>
<InfiniteScroll <InfiniteScroll
dataLength={ (parties && parties.length > 0) ? parties.length : 0} dataLength={parties && parties.length > 0 ? parties.length : 0}
next={ () => setCurrentPage(currentPage + 1) } next={() => setCurrentPage(currentPage + 1)}
hasMore={totalPages > currentPage} hasMore={totalPages > currentPage}
loader={ <div id="NotFound"><h2>Loading...</h2></div> }> loader={
<GridRepCollection loading={loading}> <div id="NotFound">
{ renderParties() } <h2>Loading...</h2>
</GridRepCollection> </div>
}
>
<GridRepCollection>{renderParties()}</GridRepCollection>
</InfiniteScroll> </InfiniteScroll>
{ (parties.length == 0) ? {parties.length == 0 ? (
<div id="NotFound"> <div id="NotFound">
<h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2> <h2>{t("teams.not_found")}</h2>
</div> </div>
: '' } ) : (
""
)}
</section> </section>
</div> </div>
) )
} }
export async function getStaticProps({ locale }: { locale: string }) { export const getServerSidePaths = async () => {
return {
paths: [
// Object variant:
{ params: { party: "string" } },
],
fallback: true,
}
}
// prettier-ignore
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
// 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}` } }
: {}
let { raids, sortedRaids } = await api.endpoints.raids
.getAll(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)
// Create filter object
const filters: {
raid?: string
element?: number
recency?: number
} = {}
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
const response = await api.endpoints.parties.getAll({
params: filters,
...headers
})
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), teams: response.data,
raids: raids,
sortedRaids: sortedRaids,
...(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

View file

@ -27,6 +27,12 @@
"rarity": "Rarity" "rarity": "Rarity"
} }
}, },
"header": {
"anonymous": "Anonymous",
"untitled_team": "Untitled team by {{username}}",
"new_team": "New team",
"byline": "{{partyName}} by {{username}}"
},
"rarities": { "rarities": {
"sr": "SR", "sr": "SR",
"ssr": "SSR" "ssr": "SSR"

View file

@ -27,6 +27,12 @@
"rarity": "レアリティ" "rarity": "レアリティ"
} }
}, },
"header": {
"anonymous": "無名",
"untitled_team": "{{username}}さんからの無題編成",
"new_team": "新編成",
"byline": "{{username}}さんからの{{partyName}}"
},
"rarities": { "rarities": {
"sr": "SR", "sr": "SR",
"ssr": "SSR" "ssr": "SSR"
@ -241,4 +247,3 @@
"no_raid": "マルチなし", "no_raid": "マルチなし",
"no_user": "無名" "no_user": "無名"
} }

5
types/AccountCookie.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
interface AccountCookie {
userId: string
username: string
token: string
}

1
types/Party.d.ts vendored
View file

@ -1,6 +1,7 @@
interface Party { interface Party {
id: string id: string
name: string name: string
description: string
raid: Raid raid: Raid
shortcode: string shortcode: string
extra: boolean extra: boolean

6
types/UserCookie.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
interface UserCookie {
picture: string
element: string
language: string
gender: number
}

View file

@ -9,122 +9,122 @@ interface RaidGroup {
export const raidGroups: RaidGroup[] = [ export const raidGroups: RaidGroup[] = [
{ {
name: { name: {
en: 'Assorted', en: "Assorted",
ja: 'その他' ja: "その他",
} },
}, },
{ {
name: { name: {
en: 'Guild Wars', en: "Guild Wars",
ja: '星の古戦場' ja: "星の古戦場",
} },
}, },
{ {
name: { name: {
en: 'Omega', en: "Omega",
ja: 'マグナ' ja: "マグナ",
} },
}, },
{ {
name: { name: {
en: 'T1 Summons', en: "T1 Summons",
ja: '召喚石マルチ1' ja: "召喚石マルチ1",
} },
}, },
{ {
name: { name: {
en: 'T2 Summons', en: "T2 Summons",
ja: '召喚石マルチ2' ja: "召喚石マルチ2",
} },
}, },
{ {
name: { name: {
en: 'Primarchs', en: "Primarchs",
ja: '四大天使' ja: "四大天使",
} },
}, },
{ {
name: { name: {
en: 'Nightmare', en: "Nightmare",
ja: 'HELL' ja: "HELL",
} },
}, },
{ {
name: { name: {
en: 'Omega (Impossible)', en: "Omega (Impossible)",
ja: 'マグナHL' ja: "マグナHL",
} },
}, },
{ {
name: { name: {
en: 'Omega II', en: "Omega II",
ja: 'マグナII' ja: "マグナII",
} },
}, },
{ {
name: { name: {
en: 'Tier 1 Summons (Impossible)', en: "Tier 1 Summons (Impossible)",
ja: '旧召喚石HL' ja: "旧召喚石HL",
} },
}, },
{ {
name: { name: {
en: 'Tier 3 Summons', en: "Tier 3 Summons",
ja: 'エピックHL' ja: "エピックHL",
} },
}, },
{ {
name: { name: {
en: 'Ennead', en: "Ennead",
ja: 'エニアド' ja: "エニアド",
} },
}, },
{ {
name: { name: {
en: 'Malice', en: "Malice",
ja: 'マリス' ja: "マリス",
} },
}, },
{ {
name: { name: {
en: '6-Star Raids', en: "6-Star Raids",
ja: '★★★★★★' ja: "★★★★★★",
} },
}, },
{ {
name: { name: {
en: 'Six-Dragons', en: "Six-Dragons",
ja: '六竜HL' ja: "六竜HL",
} },
}, },
{ {
name: { name: {
en: 'Nightmare (Impossible)', en: "Nightmare (Impossible)",
ja: '高級HELL' ja: "高級HELL",
} },
}, },
{ {
name: { name: {
en: 'Arcarum: Replicard Sandbox', en: "Arcarum: Replicard Sandbox",
ja: 'アーカルム レプリカルド・サンドボックス' ja: "アーカルム レプリカルド・サンドボックス",
} },
}, },
{ {
name: { name: {
en: 'Astrals', en: "Astrals",
ja: '星の民' ja: "星の民",
} },
}, },
{ {
name: { name: {
en: '10-Star Raids', en: "Disaster",
ja: '★★★★★★★★★★' ja: "災害",
} },
}, },
{ {
name: { name: {
en: 'Super Ultimate', en: "Super Ultimate",
ja: 'スーパーアルティメット' ja: "スーパーアルティメット",
} },
} },
] ]

View file

@ -0,0 +1,12 @@
import React, { useEffect, useRef } from "react"
const useDidMountEffect = (func: any, deps: any) => {
const didMount = useRef(false)
useEffect(() => {
if (didMount.current) func()
else didMount.current = true
}, deps)
}
export default useDidMountEffect