Merge branch 'main' of github.com:jedmund/hensei-web
This commit is contained in:
commit
c05f86d012
29 changed files with 4899 additions and 4334 deletions
|
|
@ -1,211 +1,255 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import * as Switch from "@radix-ui/react-switch"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { pictureData } from '~utils/pictureData'
|
||||
import api from "~utils/api"
|
||||
import { accountState } from "~utils/accountState"
|
||||
import { pictureData } from "~utils/pictureData"
|
||||
|
||||
import Button from '~components/Button'
|
||||
import Button from "~components/Button"
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
|
||||
const AccountModal = () => {
|
||||
const { account } = useSnapshot(accountState)
|
||||
const { account } = useSnapshot(accountState)
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
// Cookies
|
||||
const [cookies, setCookies] = useCookies()
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
|
||||
const headers = (cookies.account != null) ? {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
}
|
||||
} : {}
|
||||
|
||||
// State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [picture, setPicture] = useState('')
|
||||
const [language, setLanguage] = useState('')
|
||||
const [gender, setGender] = useState(0)
|
||||
const [privateProfile, setPrivateProfile] = useState(false)
|
||||
const headers = {}
|
||||
// cookies.account != null
|
||||
// ? {
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${cookies.account.access_token}`,
|
||||
// },
|
||||
// }
|
||||
// : {}
|
||||
|
||||
// Refs
|
||||
const pictureSelect = React.createRef<HTMLSelectElement>()
|
||||
const languageSelect = React.createRef<HTMLSelectElement>()
|
||||
const genderSelect = React.createRef<HTMLSelectElement>()
|
||||
const privateSelect = React.createRef<HTMLInputElement>()
|
||||
// State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [picture, setPicture] = useState("")
|
||||
const [language, setLanguage] = useState("")
|
||||
const [gender, setGender] = useState(0)
|
||||
const [privateProfile, setPrivateProfile] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (cookies.user) setPicture(cookies.user.picture)
|
||||
if (cookies.user) setLanguage(cookies.user.language)
|
||||
if (cookies.user) setGender(cookies.user.gender)
|
||||
}, [cookies])
|
||||
// Refs
|
||||
const pictureSelect = React.createRef<HTMLSelectElement>()
|
||||
const languageSelect = React.createRef<HTMLSelectElement>()
|
||||
const genderSelect = React.createRef<HTMLSelectElement>()
|
||||
const privateSelect = React.createRef<HTMLInputElement>()
|
||||
|
||||
const pictureOptions = (
|
||||
pictureData.sort((a, b) => (a.name.en > b.name.en) ? 1 : -1).map((item, i) => {
|
||||
return (
|
||||
<option key={`picture-${i}`} value={item.filename}>{item.name[locale]}</option>
|
||||
)
|
||||
})
|
||||
)
|
||||
// useEffect(() => {
|
||||
// if (cookies.user) setPicture(cookies.user.picture)
|
||||
// if (cookies.user) setLanguage(cookies.user.language)
|
||||
// if (cookies.user) setGender(cookies.user.gender)
|
||||
// }, [cookies])
|
||||
|
||||
function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (pictureSelect.current)
|
||||
setPicture(pictureSelect.current.value)
|
||||
const pictureOptions = pictureData
|
||||
.sort((a, b) => (a.name.en > b.name.en ? 1 : -1))
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<option key={`picture-${i}`} value={item.filename}>
|
||||
{item.name[locale]}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
|
||||
function handlePictureChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (pictureSelect.current) setPicture(pictureSelect.current.value)
|
||||
}
|
||||
|
||||
function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (languageSelect.current) setLanguage(languageSelect.current.value)
|
||||
}
|
||||
|
||||
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (genderSelect.current) setGender(parseInt(genderSelect.current.value))
|
||||
}
|
||||
|
||||
function handlePrivateChange(checked: boolean) {
|
||||
setPrivateProfile(checked)
|
||||
}
|
||||
|
||||
function update(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const object = {
|
||||
user: {
|
||||
picture: picture,
|
||||
element: pictureData.find((i) => i.filename === picture)?.element,
|
||||
language: language,
|
||||
gender: gender,
|
||||
private: privateProfile,
|
||||
},
|
||||
}
|
||||
|
||||
function handleLanguageChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (languageSelect.current)
|
||||
setLanguage(languageSelect.current.value)
|
||||
}
|
||||
// api.endpoints.users
|
||||
// .update(cookies.account.user_id, object, headers)
|
||||
// .then((response) => {
|
||||
// const user = response.data.user
|
||||
|
||||
function handleGenderChange(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
if (genderSelect.current)
|
||||
setGender(parseInt(genderSelect.current.value))
|
||||
}
|
||||
// const cookieObj = {
|
||||
// picture: user.picture.picture,
|
||||
// element: user.picture.element,
|
||||
// gender: user.gender,
|
||||
// language: user.language,
|
||||
// }
|
||||
|
||||
function handlePrivateChange(checked: boolean) {
|
||||
setPrivateProfile(checked)
|
||||
}
|
||||
// setCookies("user", cookieObj, { path: "/" })
|
||||
|
||||
function update(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
// accountState.account.user = {
|
||||
// id: user.id,
|
||||
// username: user.username,
|
||||
// picture: user.picture.picture,
|
||||
// element: user.picture.element,
|
||||
// gender: user.gender,
|
||||
// }
|
||||
|
||||
const object = {
|
||||
user: {
|
||||
picture: picture,
|
||||
element: pictureData.find(i => i.filename === picture)?.element,
|
||||
language: language,
|
||||
gender: gender,
|
||||
private: privateProfile
|
||||
}
|
||||
}
|
||||
// setOpen(false)
|
||||
// changeLanguage(user.language)
|
||||
// })
|
||||
}
|
||||
|
||||
api.endpoints.users.update(cookies.account.user_id, object, headers)
|
||||
.then(response => {
|
||||
const user = response.data.user
|
||||
function changeLanguage(newLanguage: string) {
|
||||
// if (newLanguage !== router.locale) {
|
||||
// setCookies("NEXT_LOCALE", newLanguage, { path: "/" })
|
||||
// router.push(router.asPath, undefined, { locale: newLanguage })
|
||||
// }
|
||||
}
|
||||
|
||||
const cookieObj = {
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
gender: user.gender,
|
||||
language: user.language
|
||||
}
|
||||
|
||||
setCookies('user', cookieObj, { path: '/'})
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
gender: user.gender
|
||||
}
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t("menu.settings")}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className="Account Dialog"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<Dialog.Title className="SubTitle">
|
||||
{t("modals.settings.title")}
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">
|
||||
@{account.user?.username}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
setOpen(false)
|
||||
changeLanguage(user.language)
|
||||
})
|
||||
}
|
||||
<form onSubmit={update}>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.picture")}</label>
|
||||
</div>
|
||||
|
||||
function changeLanguage(newLanguage: string) {
|
||||
if (newLanguage !== router.locale) {
|
||||
setCookies('NEXT_LOCALE', newLanguage, { path: '/'})
|
||||
router.push(router.asPath, undefined, { locale: newLanguage })
|
||||
}
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.settings')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Account Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<Dialog.Title className="SubTitle">{t('modals.settings.title')}</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">@{account.user?.username}</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<form onSubmit={update}>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.picture')}</label>
|
||||
</div>
|
||||
|
||||
<div className={`preview ${pictureData.find(i => i.filename === picture)?.element}`}>
|
||||
<img
|
||||
alt="Profile preview"
|
||||
srcSet={`/profile/${picture}.png,
|
||||
<div
|
||||
className={`preview ${
|
||||
pictureData.find((i) => i.filename === picture)?.element
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
alt="Profile preview"
|
||||
srcSet={`/profile/${picture}.png,
|
||||
/profile/${picture}@2x.png 2x`}
|
||||
src={`/profile/${picture}.png`}
|
||||
/>
|
||||
</div>
|
||||
src={`/profile/${picture}.png`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select name="picture" onChange={handlePictureChange} value={picture} ref={pictureSelect}>
|
||||
{pictureOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.gender')}</label>
|
||||
</div>
|
||||
<select
|
||||
name="picture"
|
||||
onChange={handlePictureChange}
|
||||
value={picture}
|
||||
ref={pictureSelect}
|
||||
>
|
||||
{pictureOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.gender")}</label>
|
||||
</div>
|
||||
|
||||
<select name="gender" 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>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.language')}</label>
|
||||
</div>
|
||||
<select
|
||||
name="gender"
|
||||
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>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.language")}</label>
|
||||
</div>
|
||||
|
||||
<select name="language" 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>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t('modals.settings.labels.private')}</label>
|
||||
<p className={locale}>{t('modals.settings.descriptions.private')}</p>
|
||||
</div>
|
||||
<select
|
||||
name="language"
|
||||
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>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="left">
|
||||
<label>{t("modals.settings.labels.private")}</label>
|
||||
<p className={locale}>
|
||||
{t("modals.settings.descriptions.private")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Switch.Root className="Switch" onCheckedChange={handlePrivateChange} checked={privateProfile}>
|
||||
<Switch.Thumb className="Thumb" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
<Switch.Root
|
||||
className="Switch"
|
||||
onCheckedChange={handlePrivateChange}
|
||||
checked={privateProfile}
|
||||
>
|
||||
<Switch.Thumb className="Thumb" />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
|
||||
<Button>{t('modals.settings.buttons.confirm')}</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
<Button>{t("modals.settings.buttons.confirm")}</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountModal
|
||||
|
|
|
|||
|
|
@ -1,254 +1,220 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useSnapshot } from "valtio"
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { AxiosResponse } from "axios"
|
||||
import debounce from "lodash.debounce"
|
||||
|
||||
import JobSection from '~components/JobSection'
|
||||
import CharacterUnit from '~components/CharacterUnit'
|
||||
import JobSection from "~components/JobSection"
|
||||
import CharacterUnit from "~components/CharacterUnit"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
new: boolean
|
||||
slug?: string
|
||||
createParty: () => Promise<AxiosResponse<any, any>>
|
||||
pushHistory?: (path: string) => void
|
||||
new: boolean
|
||||
characters?: GridCharacter[]
|
||||
createParty: () => Promise<AxiosResponse<any, any>>
|
||||
pushHistory?: (path: string) => void
|
||||
}
|
||||
|
||||
const CharacterGrid = (props: Props) => {
|
||||
// Constants
|
||||
const numCharacters: number = 5
|
||||
// Constants
|
||||
const numCharacters: number = 5
|
||||
|
||||
// Cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = (cookies.account != null) ? {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
}
|
||||
} : {}
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { headers: { Authorization: `Bearer ${accountData.token}` } }
|
||||
: {}
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
// Set up state for view management
|
||||
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
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
||||
[key: number]: number
|
||||
}>({})
|
||||
|
||||
// Create a temporary state to store previous character uncap values
|
||||
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
|
||||
useEffect(() => {
|
||||
// If user is logged in and matches
|
||||
if (
|
||||
(accountData && party.user && accountData.userId === party.user.id) ||
|
||||
props.new
|
||||
)
|
||||
appState.party.editable = true
|
||||
else appState.party.editable = false
|
||||
}, [props.new, accountData, party])
|
||||
|
||||
// Set the editable flag only on first load
|
||||
useEffect(() => {
|
||||
if (!loading && !firstLoadComplete) {
|
||||
// If user is logged in and matches
|
||||
if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new)
|
||||
appState.party.editable = true
|
||||
else
|
||||
appState.party.editable = false
|
||||
// Initialize an array of current uncap values for each characters
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: { [key: number]: number } = {}
|
||||
Object.values(appState.grid.characters).map(
|
||||
(o) => (initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
)
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [appState.grid.characters])
|
||||
|
||||
setFirstLoadComplete(true)
|
||||
}
|
||||
}, [props.new, cookies, party, loading, firstLoadComplete])
|
||||
// Methods: Adding an object from search
|
||||
function receiveCharacterFromSearch(
|
||||
object: Character | Weapon | Summon,
|
||||
position: number
|
||||
) {
|
||||
const character = object as Character
|
||||
|
||||
// Initialize an array of current uncap values for each characters
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||
Object.values(appState.grid.characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [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
|
||||
if (!party.id) {
|
||||
props.createParty().then((response) => {
|
||||
const party = response.data.party
|
||||
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)
|
||||
setSlug(party.shortcode)
|
||||
|
||||
// Populate the weapons in state
|
||||
populateCharacters(party.characters)
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then((response) => storeGridCharacter(response.data.grid_character))
|
||||
.catch((error) => console.error(error))
|
||||
})
|
||||
} else {
|
||||
if (party.editable)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then((response) => storeGridCharacter(response.data.grid_character))
|
||||
.catch((error) => console.error(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCharacter(
|
||||
partyId: string,
|
||||
character: Character,
|
||||
position: number
|
||||
) {
|
||||
return await api.endpoints.characters.create(
|
||||
{
|
||||
character: {
|
||||
party_id: partyId,
|
||||
character_id: character.id,
|
||||
position: position,
|
||||
uncap_level: characterUncapLevel(character),
|
||||
},
|
||||
},
|
||||
headers
|
||||
)
|
||||
}
|
||||
|
||||
function storeGridCharacter(gridCharacter: GridCharacter) {
|
||||
appState.grid.characters[gridCharacter.position] = gridCharacter
|
||||
}
|
||||
|
||||
// Methods: Helpers
|
||||
function characterUncapLevel(character: Character) {
|
||||
let uncapLevel
|
||||
|
||||
if (character.special) {
|
||||
uncapLevel = 3
|
||||
if (character.uncap.ulb) uncapLevel = 5
|
||||
else if (character.uncap.flb) uncapLevel = 4
|
||||
} else {
|
||||
uncapLevel = 4
|
||||
if (character.uncap.ulb) uncapLevel = 6
|
||||
else if (character.uncap.flb) uncapLevel = 5
|
||||
}
|
||||
|
||||
function processError(error: any) {
|
||||
if (error.response != null) {
|
||||
if (error.response.status == 404) {
|
||||
setFound(false)
|
||||
setLoading(false)
|
||||
}
|
||||
} else {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
return uncapLevel
|
||||
}
|
||||
|
||||
function populateCharacters(list: Array<GridCharacter>) {
|
||||
list.forEach((object: GridCharacter) => {
|
||||
if (object.position != null)
|
||||
appState.grid.characters[object.position] = object
|
||||
// Methods: Updating uncap level
|
||||
// Note: Saves, but debouncing is not working properly
|
||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||
storePreviousUncapValue(position)
|
||||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap("character", id, uncapLevel).then((response) => {
|
||||
storeGridCharacter(response.data.grid_character)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// Revert optimistic UI
|
||||
updateUncapLevel(position, previousUncapValues[position])
|
||||
|
||||
// Remove optimistic key
|
||||
let newPreviousValues = { ...previousUncapValues }
|
||||
delete newPreviousValues[position]
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||
const character = object as Character
|
||||
function initiateUncapUpdate(
|
||||
id: string,
|
||||
position: number,
|
||||
uncapLevel: number
|
||||
) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
if (!party.id) {
|
||||
props.createParty()
|
||||
.then(response => {
|
||||
const party = response.data.party
|
||||
appState.party.id = party.id
|
||||
setSlug(party.shortcode)
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then(response => storeGridCharacter(response.data.grid_character))
|
||||
.catch(error => console.error(error))
|
||||
})
|
||||
} else {
|
||||
if (party.editable)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then(response => storeGridCharacter(response.data.grid_character))
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
},
|
||||
[props, previousUncapValues]
|
||||
)
|
||||
|
||||
const debouncedAction = useMemo(
|
||||
() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500),
|
||||
[props, saveUncap]
|
||||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
appState.grid.characters[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = { ...previousUncapValues }
|
||||
|
||||
if (grid.characters[position]) {
|
||||
newPreviousValues[position] = grid.characters[position].uncap_level
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCharacter(partyId: string, character: Character, position: number) {
|
||||
return await api.endpoints.characters.create({
|
||||
'character': {
|
||||
'party_id': partyId,
|
||||
'character_id': character.id,
|
||||
'position': position,
|
||||
'uncap_level': characterUncapLevel(character)
|
||||
}
|
||||
}, headers)
|
||||
}
|
||||
|
||||
function storeGridCharacter(gridCharacter: GridCharacter) {
|
||||
appState.grid.characters[gridCharacter.position] = gridCharacter
|
||||
}
|
||||
|
||||
// Methods: Helpers
|
||||
function characterUncapLevel(character: Character) {
|
||||
let uncapLevel
|
||||
|
||||
if (character.special) {
|
||||
uncapLevel = 3
|
||||
if (character.uncap.ulb) uncapLevel = 5
|
||||
else if (character.uncap.flb) uncapLevel = 4
|
||||
} else {
|
||||
uncapLevel = 4
|
||||
if (character.uncap.ulb) uncapLevel = 6
|
||||
else if (character.uncap.flb) uncapLevel = 5
|
||||
}
|
||||
|
||||
return uncapLevel
|
||||
}
|
||||
|
||||
// Methods: Updating uncap level
|
||||
// Note: Saves, but debouncing is not working properly
|
||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||
storePreviousUncapValue(position)
|
||||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap('character', id, uncapLevel)
|
||||
.then(response => { storeGridCharacter(response.data.grid_character) })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// Revert optimistic UI
|
||||
updateUncapLevel(position, previousUncapValues[position])
|
||||
|
||||
// Remove optimistic key
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
delete newPreviousValues[position]
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
}, [props, previousUncapValues]
|
||||
)
|
||||
|
||||
const debouncedAction = useMemo(() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500), [props, saveUncap]
|
||||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
appState.grid.characters[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
|
||||
if (grid.characters[position]) {
|
||||
newPreviousValues[position] = grid.characters[position].uncap_level
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
return (
|
||||
<div>
|
||||
<div id="CharacterGrid">
|
||||
<JobSection />
|
||||
<ul id="grid_characters">
|
||||
{Array.from(Array(numCharacters)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<CharacterUnit
|
||||
gridCharacter={grid.characters[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
updateObject={receiveCharacterFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
// Render: JSX components
|
||||
return (
|
||||
<div>
|
||||
<div id="CharacterGrid">
|
||||
<JobSection />
|
||||
<ul id="grid_characters">
|
||||
{Array.from(Array(numCharacters)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`}>
|
||||
<CharacterUnit
|
||||
gridCharacter={grid.characters[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
updateObject={receiveCharacterFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CharacterGrid
|
||||
|
|
|
|||
|
|
@ -1,187 +1,201 @@
|
|||
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 { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
import { accountState } from "~utils/accountState"
|
||||
import { formatTimeAgo } from "~utils/timeAgo"
|
||||
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { formatTimeAgo } from '~utils/timeAgo'
|
||||
import Button from "~components/Button"
|
||||
import { ButtonType } from "~utils/enums"
|
||||
|
||||
import Button from '~components/Button'
|
||||
import { ButtonType } from '~utils/enums'
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
shortcode: string
|
||||
id: string
|
||||
name: string
|
||||
raid: Raid
|
||||
grid: GridWeapon[]
|
||||
user?: User
|
||||
favorited: boolean
|
||||
createdAt: Date
|
||||
displayUser?: boolean | false
|
||||
onClick: (shortcode: string) => void
|
||||
onSave?: (partyId: string, favorited: boolean) => void
|
||||
shortcode: string
|
||||
id: string
|
||||
name: string
|
||||
raid: Raid
|
||||
grid: GridWeapon[]
|
||||
user?: User
|
||||
favorited: boolean
|
||||
createdAt: Date
|
||||
displayUser?: boolean | false
|
||||
onClick: (shortcode: string) => void
|
||||
onSave?: (partyId: string, favorited: boolean) => void
|
||||
}
|
||||
|
||||
const GridRep = (props: Props) => {
|
||||
const numWeapons: number = 9
|
||||
const numWeapons: number = 9
|
||||
|
||||
const { account } = useSnapshot(accountState)
|
||||
const { account } = useSnapshot(accountState)
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
|
||||
const [mainhand, setMainhand] = useState<Weapon>()
|
||||
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
||||
const [mainhand, setMainhand] = useState<Weapon>()
|
||||
const [weapons, setWeapons] = useState<GridArray<Weapon>>({})
|
||||
|
||||
const titleClass = classNames({
|
||||
'empty': !props.name
|
||||
})
|
||||
const titleClass = classNames({
|
||||
empty: !props.name,
|
||||
})
|
||||
|
||||
const raidClass = classNames({
|
||||
'raid': true,
|
||||
'empty': !props.raid
|
||||
})
|
||||
const raidClass = classNames({
|
||||
raid: true,
|
||||
empty: !props.raid,
|
||||
})
|
||||
|
||||
const userClass = classNames({
|
||||
'user': true,
|
||||
'empty': !props.user
|
||||
})
|
||||
const userClass = classNames({
|
||||
user: true,
|
||||
empty: !props.user,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const newWeapons = Array(numWeapons)
|
||||
useEffect(() => {
|
||||
const newWeapons = Array(numWeapons)
|
||||
|
||||
for (const [key, value] of Object.entries(props.grid)) {
|
||||
if (value.position == -1)
|
||||
setMainhand(value.object)
|
||||
else if (!value.mainhand && value.position != null)
|
||||
newWeapons[value.position] = value.object
|
||||
}
|
||||
|
||||
setWeapons(newWeapons)
|
||||
}, [props.grid])
|
||||
|
||||
function navigate() {
|
||||
props.onClick(props.shortcode)
|
||||
for (const [key, value] of Object.entries(props.grid)) {
|
||||
if (value.position == -1) setMainhand(value.object)
|
||||
else if (!value.mainhand && value.position != null)
|
||||
newWeapons[value.position] = value.object
|
||||
}
|
||||
|
||||
function generateMainhandImage() {
|
||||
let url = ''
|
||||
setWeapons(newWeapons)
|
||||
}, [props.grid])
|
||||
|
||||
if (mainhand) {
|
||||
if (mainhand.element == 0 && props.grid[0].element) {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${props.grid[0].element}.jpg`
|
||||
} else {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`
|
||||
}
|
||||
}
|
||||
function navigate() {
|
||||
props.onClick(props.shortcode)
|
||||
}
|
||||
|
||||
return (mainhand) ?
|
||||
<img alt={mainhand.name[locale]} src={url} /> : ''
|
||||
function generateMainhandImage() {
|
||||
let url = ""
|
||||
|
||||
if (mainhand) {
|
||||
if (mainhand.element == 0 && props.grid[0].element) {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}_${props.grid[0].element}.jpg`
|
||||
} else {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${mainhand.granblue_id}.jpg`
|
||||
}
|
||||
}
|
||||
|
||||
function generateGridImage(position: number) {
|
||||
let url = ''
|
||||
return mainhand && props.grid[0] ? (
|
||||
<img alt={mainhand.name[locale]} src={url} />
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
if (weapons[position]) {
|
||||
if (weapons[position].element == 0 && props.grid[position].element) {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}_${props.grid[position].element}.jpg`
|
||||
} else {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}.jpg`
|
||||
}
|
||||
}
|
||||
function generateGridImage(position: number) {
|
||||
let url = ""
|
||||
|
||||
return (weapons[position]) ?
|
||||
<img alt={weapons[position].name[locale]} src={url} /> : ''
|
||||
if (weapons[position]) {
|
||||
if (weapons[position].element == 0 && props.grid[position].element) {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}_${props.grid[position].element}.jpg`
|
||||
} else {
|
||||
url = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapons[position]?.granblue_id}.jpg`
|
||||
}
|
||||
}
|
||||
|
||||
function sendSaveData() {
|
||||
if (props.onSave)
|
||||
props.onSave(props.id, props.favorited)
|
||||
}
|
||||
return weapons[position] ? (
|
||||
<img alt={weapons[position].name[locale]} src={url} />
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
const userImage = () => {
|
||||
if (props.user)
|
||||
return (
|
||||
<img
|
||||
alt={props.user.picture.picture}
|
||||
className={`profile ${props.user.picture.element}`}
|
||||
srcSet={`/profile/${props.user.picture.picture}.png,
|
||||
function sendSaveData() {
|
||||
if (props.onSave) props.onSave(props.id, props.favorited)
|
||||
}
|
||||
|
||||
const userImage = () => {
|
||||
if (props.user)
|
||||
return (
|
||||
<img
|
||||
alt={props.user.picture.picture}
|
||||
className={`profile ${props.user.picture.element}`}
|
||||
srcSet={`/profile/${props.user.picture.picture}.png,
|
||||
/profile/${props.user.picture.picture}@2x.png 2x`}
|
||||
src={`/profile/${props.user.picture.picture}.png`}
|
||||
/>
|
||||
src={`/profile/${props.user.picture.picture}.png`}
|
||||
/>
|
||||
)
|
||||
else return <div className="no-user" />
|
||||
}
|
||||
|
||||
const details = (
|
||||
<div className="Details">
|
||||
<h2 className={titleClass} onClick={navigate}>
|
||||
{props.name ? props.name : t("no_title")}
|
||||
</h2>
|
||||
<div className="bottom">
|
||||
<div className={raidClass}>
|
||||
{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>
|
||||
)
|
||||
|
||||
const detailsWithUsername = (
|
||||
<div className="Details">
|
||||
<div className="top">
|
||||
<div className="info">
|
||||
<h2 className={titleClass} onClick={navigate}>
|
||||
{props.name ? props.name : t("no_title")}
|
||||
</h2>
|
||||
<div className={raidClass}>
|
||||
{props.raid ? props.raid.name[locale] : t("no_raid")}
|
||||
</div>
|
||||
</div>
|
||||
{account.authorized &&
|
||||
((props.user && account.user && account.user.id !== props.user.id) ||
|
||||
!props.user) ? (
|
||||
<Button
|
||||
active={props.favorited}
|
||||
icon="save"
|
||||
type={ButtonType.IconOnly}
|
||||
onClick={sendSaveData}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div className="bottom">
|
||||
<div className={userClass}>
|
||||
{userImage()}
|
||||
{props.user ? props.user.username : t("no_user")}
|
||||
</div>
|
||||
<time className="last-updated" dateTime={props.createdAt.toISOString()}>
|
||||
{formatTimeAgo(props.createdAt, locale)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="GridRep">
|
||||
{props.displayUser ? detailsWithUsername : details}
|
||||
<div className="Grid" onClick={navigate}>
|
||||
<div className="weapon grid_mainhand">{generateMainhandImage()}</div>
|
||||
|
||||
<ul className="grid_weapons">
|
||||
{Array.from(Array(numWeapons)).map((x, i) => {
|
||||
return (
|
||||
<li
|
||||
key={`${props.shortcode}-${i}`}
|
||||
className="weapon grid_weapon"
|
||||
>
|
||||
{generateGridImage(i)}
|
||||
</li>
|
||||
)
|
||||
else
|
||||
return (<div className="no-user" />)
|
||||
}
|
||||
|
||||
const details = (
|
||||
<div className="Details">
|
||||
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
|
||||
<div className="bottom">
|
||||
<div className={raidClass}>{ (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>
|
||||
)
|
||||
|
||||
const detailsWithUsername = (
|
||||
<div className="Details">
|
||||
<div className="top">
|
||||
<div className="info">
|
||||
<h2 className={titleClass} onClick={navigate}>{ (props.name) ? props.name : t('no_title') }</h2>
|
||||
<div className={raidClass}>{ (props.raid) ? props.raid.name[locale] : t('no_raid') }</div>
|
||||
</div>
|
||||
{
|
||||
(account.authorized && (
|
||||
(props.user && account.user && account.user.id !== props.user.id)
|
||||
|| (!props.user)
|
||||
)) ?
|
||||
<Button
|
||||
active={props.favorited}
|
||||
icon="save"
|
||||
type={ButtonType.IconOnly}
|
||||
onClick={sendSaveData} />
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
<div className="bottom">
|
||||
<div className={userClass}>
|
||||
{ userImage() }
|
||||
{ (props.user) ? props.user.username : t('no_user') }
|
||||
</div>
|
||||
<time className="last-updated" dateTime={props.createdAt.toISOString()}>{formatTimeAgo(props.createdAt, locale)}</time>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="GridRep">
|
||||
{ (props.displayUser) ? detailsWithUsername : details}
|
||||
<div className="Grid" onClick={navigate}>
|
||||
<div className="weapon grid_mainhand">
|
||||
{generateMainhandImage()}
|
||||
</div>
|
||||
|
||||
<ul className="grid_weapons">
|
||||
{
|
||||
Array.from(Array(numWeapons)).map((x, i) => {
|
||||
return (
|
||||
<li key={`${props.shortcode}-${i}`} className="weapon grid_weapon">
|
||||
{generateGridImage(i)}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GridRep
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
.GridRepCollection {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
margin: 0 auto;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
transition: opacity 0.14s ease-in-out;
|
||||
// width: fit-content;
|
||||
max-width: 996px;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
transition: opacity 0.14s ease-in-out;
|
||||
// width: fit-content;
|
||||
max-width: 996px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import classNames from "classnames"
|
||||
import React from "react"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const GridRepCollection = (props: Props) => {
|
||||
const classes = classNames({
|
||||
'GridRepCollection': true,
|
||||
'visible': !props.loading
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
const classes = classNames({
|
||||
GridRepCollection: true,
|
||||
})
|
||||
|
||||
return <div className={classes}>{props.children}</div>
|
||||
}
|
||||
|
||||
export default GridRepCollection
|
||||
|
|
|
|||
|
|
@ -1,129 +1,141 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { getCookie, setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import Link from 'next/link'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
import Link from "next/link"
|
||||
import * as Switch from "@radix-ui/react-switch"
|
||||
|
||||
import AboutModal from '~components/AboutModal'
|
||||
import AccountModal from '~components/AccountModal'
|
||||
import LoginModal from '~components/LoginModal'
|
||||
import SignupModal from '~components/SignupModal'
|
||||
import AboutModal from "~components/AboutModal"
|
||||
import AccountModal from "~components/AccountModal"
|
||||
import LoginModal from "~components/LoginModal"
|
||||
import SignupModal from "~components/SignupModal"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {
|
||||
authenticated: boolean,
|
||||
username?: string,
|
||||
logout?: () => void
|
||||
authenticated: boolean
|
||||
username?: string
|
||||
logout?: () => void
|
||||
}
|
||||
|
||||
const HeaderMenu = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [accountCookies] = useCookies(['account'])
|
||||
const [userCookies] = useCookies(['user'])
|
||||
const [cookies, setCookies] = useCookies()
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const [checked, setChecked] = useState(false)
|
||||
const accountCookie = getCookie("account")
|
||||
const accountData: AccountCookie = accountCookie
|
||||
? JSON.parse(accountCookie as string)
|
||||
: null
|
||||
|
||||
useEffect(() => {
|
||||
const locale = cookies['NEXT_LOCALE']
|
||||
setChecked((locale === 'ja') ? true : false)
|
||||
}, [cookies])
|
||||
const userCookie = getCookie("user")
|
||||
const userData: UserCookie = userCookie
|
||||
? JSON.parse(userCookie as string)
|
||||
: null
|
||||
|
||||
function handleCheckedChange(value: boolean) {
|
||||
const language = (value) ? 'ja' : 'en'
|
||||
setCookies('NEXT_LOCALE', language, { path: '/'})
|
||||
router.push(router.asPath, undefined, { locale: language })
|
||||
}
|
||||
const localeCookie = getCookie("NEXT_LOCALE")
|
||||
|
||||
function authItems() {
|
||||
return (
|
||||
<nav>
|
||||
<ul className="Menu auth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem profile">
|
||||
<Link href={`/${accountCookies.account.username}` || ''} passHref>
|
||||
<div>
|
||||
<span>{accountCookies.account.username}</span>
|
||||
<img
|
||||
alt={userCookies.user.picture}
|
||||
className={`profile ${userCookies.user.element}`}
|
||||
srcSet={`/profile/${userCookies.user.picture}.png,
|
||||
/profile/${userCookies.user.picture}@2x.png 2x`}
|
||||
src={`/profile/${userCookies.user.picture}.png`}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href='/teams'>{t('menu.teams')}</Link>
|
||||
</li>
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
<li className="MenuItem disabled">
|
||||
<div>
|
||||
<span>{t('menu.guides')}</span>
|
||||
<i className="tag">{t('coming_soon')}</i>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
<AccountModal />
|
||||
<li className="MenuItem" onClick={props.logout}>
|
||||
<span>{t('menu.logout')}</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
const locale = localeCookie
|
||||
setChecked(locale === "ja" ? true : false)
|
||||
}, [localeCookie])
|
||||
|
||||
function unauthItems() {
|
||||
return (
|
||||
<ul className="Menu unauth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem language">
|
||||
<span>{t('menu.language')}</span>
|
||||
<Switch.Root className="Switch" onCheckedChange={handleCheckedChange} checked={checked}>
|
||||
<Switch.Thumb className="Thumb" />
|
||||
<span className="left">JP</span>
|
||||
<span className="right">EN</span>
|
||||
</Switch.Root>
|
||||
</li>
|
||||
function handleCheckedChange(value: boolean) {
|
||||
const language = value ? "ja" : "en"
|
||||
setCookie("NEXT_LOCALE", language, { path: "/" })
|
||||
router.push(router.asPath, undefined, { locale: language })
|
||||
}
|
||||
|
||||
function authItems() {
|
||||
return (
|
||||
<nav>
|
||||
<ul className="Menu auth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem profile">
|
||||
<Link href={`/${accountData.username}` || ""} passHref>
|
||||
<div>
|
||||
<span>{accountData.username}</span>
|
||||
<img
|
||||
alt={userData.picture}
|
||||
className={`profile ${userData.element}`}
|
||||
srcSet={`/profile/${userData.picture}.png,
|
||||
/profile/${userData.picture}@2x.png 2x`}
|
||||
src={`/profile/${userData.picture}.png`}
|
||||
/>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href='/teams'>{t('menu.teams')}</Link>
|
||||
</li>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<Link href={`/saved` || ""}>{t("menu.saved")}</Link>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href="/teams">{t("menu.teams")}</Link>
|
||||
</li>
|
||||
|
||||
<li className="MenuItem disabled">
|
||||
<div>
|
||||
<span>{t('menu.guides')}</span>
|
||||
<i className="tag">{t('coming_soon')}</i>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<LoginModal />
|
||||
<SignupModal />
|
||||
</div>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
<li className="MenuItem disabled">
|
||||
<div>
|
||||
<span>{t("menu.guides")}</span>
|
||||
<i className="tag">{t("coming_soon")}</i>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
<AccountModal />
|
||||
<li className="MenuItem" onClick={props.logout}>
|
||||
<span>{t("menu.logout")}</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
return (props.authenticated) ? authItems() : unauthItems()
|
||||
function unauthItems() {
|
||||
return (
|
||||
<ul className="Menu unauth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem language">
|
||||
<span>{t("menu.language")}</span>
|
||||
<Switch.Root
|
||||
className="Switch"
|
||||
onCheckedChange={handleCheckedChange}
|
||||
checked={checked}
|
||||
>
|
||||
<Switch.Thumb className="Thumb" />
|
||||
<span className="left">JP</span>
|
||||
<span className="right">EN</span>
|
||||
</Switch.Root>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href="/teams">{t("menu.teams")}</Link>
|
||||
</li>
|
||||
|
||||
<li className="MenuItem disabled">
|
||||
<div>
|
||||
<span>{t("menu.guides")}</span>
|
||||
<i className="tag">{t("coming_soon")}</i>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<LoginModal />
|
||||
<SignupModal />
|
||||
</div>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return props.authenticated ? authItems() : unauthItems()
|
||||
}
|
||||
|
||||
export default HeaderMenu
|
||||
export default HeaderMenu
|
||||
|
|
|
|||
|
|
@ -1,213 +1,216 @@
|
|||
import React, { useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import React, { useState } from "react"
|
||||
import { setCookie } from "cookies-next"
|
||||
import Router, { useRouter } from "next/router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
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 { accountState } from '~utils/accountState'
|
||||
import api from "~utils/api"
|
||||
import { accountState } from "~utils/accountState"
|
||||
|
||||
import Button from '~components/Button'
|
||||
import Fieldset from '~components/Fieldset'
|
||||
import Button from "~components/Button"
|
||||
import Fieldset from "~components/Fieldset"
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface ErrorMap {
|
||||
[index: string]: string
|
||||
email: string
|
||||
password: string
|
||||
[index: string]: string
|
||||
email: 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 router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Set up form states and error handling
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
// Set up form states and error handling
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
email: "",
|
||||
password: "",
|
||||
})
|
||||
|
||||
// Cookies
|
||||
const [cookies, setCookies] = useCookies()
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
// Set up form refs
|
||||
const emailInput: React.RefObject<HTMLInputElement> = React.createRef()
|
||||
const passwordInput: React.RefObject<HTMLInputElement> = React.createRef()
|
||||
const form: React.RefObject<HTMLInputElement>[] = [emailInput, passwordInput]
|
||||
|
||||
// Set up form refs
|
||||
const emailInput: React.RefObject<HTMLInputElement> = React.createRef()
|
||||
const passwordInput: React.RefObject<HTMLInputElement> = React.createRef()
|
||||
const form: React.RefObject<HTMLInputElement>[] = [emailInput, passwordInput]
|
||||
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { name, value } = event.target
|
||||
let newErrors = { ...errors }
|
||||
|
||||
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { name, value } = event.target
|
||||
let newErrors = {...errors}
|
||||
switch (name) {
|
||||
case "email":
|
||||
if (value.length == 0)
|
||||
newErrors.email = t("modals.login.errors.empty_email")
|
||||
else if (!emailRegex.test(value))
|
||||
newErrors.email = t("modals.login.errors.invalid_email")
|
||||
else newErrors.email = ""
|
||||
break
|
||||
|
||||
switch(name) {
|
||||
case 'email':
|
||||
if (value.length == 0)
|
||||
newErrors.email = t('modals.login.errors.empty_email')
|
||||
else if (!emailRegex.test(value))
|
||||
newErrors.email = t('modals.login.errors.invalid_email')
|
||||
else
|
||||
newErrors.email = ''
|
||||
break
|
||||
case "password":
|
||||
newErrors.password =
|
||||
value.length == 0 ? t("modals.login.errors.empty_password") : ""
|
||||
break
|
||||
|
||||
case 'password':
|
||||
newErrors.password = value.length == 0
|
||||
? t('modals.login.errors.empty_password')
|
||||
: ''
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
setFormValid(validateForm(newErrors))
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
function validateForm(errors: ErrorMap) {
|
||||
let valid = true
|
||||
setErrors(newErrors)
|
||||
setFormValid(validateForm(newErrors))
|
||||
}
|
||||
|
||||
Object.values(form).forEach(
|
||||
(input) => input.current?.value.length == 0 && (valid = false)
|
||||
)
|
||||
function validateForm(errors: ErrorMap) {
|
||||
let valid = true
|
||||
|
||||
Object.values(errors).forEach(
|
||||
(error) => error.length > 0 && (valid = false)
|
||||
)
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
function login(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
const body = {
|
||||
email: emailInput.current?.value,
|
||||
password: passwordInput.current?.value,
|
||||
grant_type: 'password'
|
||||
}
|
||||
|
||||
if (formValid) {
|
||||
api.login(body)
|
||||
.then(response => {
|
||||
storeCookieInfo(response)
|
||||
return response.data.user.id
|
||||
})
|
||||
.then(id => fetchUserInfo(id))
|
||||
.then(infoResponse => storeUserInfo(infoResponse))
|
||||
}
|
||||
}
|
||||
|
||||
function fetchUserInfo(id: string) {
|
||||
return api.userInfo(id)
|
||||
}
|
||||
|
||||
function storeCookieInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj = {
|
||||
user_id: user.id,
|
||||
username: user.username,
|
||||
access_token: response.data.access_token
|
||||
}
|
||||
|
||||
setCookies('account', cookieObj, { path: '/' })
|
||||
}
|
||||
|
||||
function storeUserInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj = {
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
setCookies('user', cookieObj, { path: '/' })
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
accountState.account.authorized = true
|
||||
|
||||
setOpen(false)
|
||||
changeLanguage(user.language)
|
||||
}
|
||||
|
||||
function changeLanguage(newLanguage: string) {
|
||||
if (newLanguage !== router.locale) {
|
||||
setCookies('NEXT_LOCALE', newLanguage, { path: '/'})
|
||||
router.push(router.asPath, undefined, { locale: newLanguage })
|
||||
}
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
setErrors({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.login')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Login Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">{t('modals.login.title')}</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<form className="form" onSubmit={login}>
|
||||
<Fieldset
|
||||
fieldName="email"
|
||||
placeholder={t('modals.login.placeholders.email')}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
ref={emailInput}
|
||||
/>
|
||||
|
||||
<Fieldset
|
||||
fieldName="password"
|
||||
placeholder={t('modals.login.placeholders.password')}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
ref={passwordInput}
|
||||
/>
|
||||
|
||||
<Button>{t('modals.login.buttons.confirm')}</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
Object.values(form).forEach(
|
||||
(input) => input.current?.value.length == 0 && (valid = false)
|
||||
)
|
||||
|
||||
Object.values(errors).forEach(
|
||||
(error) => error.length > 0 && (valid = false)
|
||||
)
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
function login(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
const body = {
|
||||
email: emailInput.current?.value,
|
||||
password: passwordInput.current?.value,
|
||||
grant_type: "password",
|
||||
}
|
||||
|
||||
if (formValid) {
|
||||
api
|
||||
.login(body)
|
||||
.then((response) => {
|
||||
storeCookieInfo(response)
|
||||
return response.data.user.id
|
||||
})
|
||||
.then((id) => fetchUserInfo(id))
|
||||
.then((infoResponse) => storeUserInfo(infoResponse))
|
||||
}
|
||||
}
|
||||
|
||||
function fetchUserInfo(id: string) {
|
||||
return api.userInfo(id)
|
||||
}
|
||||
|
||||
function storeCookieInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj: AccountCookie = {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
token: response.data.access_token,
|
||||
}
|
||||
|
||||
setCookie("account", cookieObj, { path: "/" })
|
||||
}
|
||||
|
||||
function storeUserInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj: UserCookie = {
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender,
|
||||
}
|
||||
|
||||
setCookie("user", cookieObj, { path: "/" })
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
gender: user.gender,
|
||||
}
|
||||
|
||||
console.log("Authorizing account...")
|
||||
accountState.account.authorized = true
|
||||
|
||||
setOpen(false)
|
||||
changeLanguage(user.language)
|
||||
}
|
||||
|
||||
function changeLanguage(newLanguage: string) {
|
||||
if (newLanguage !== router.locale) {
|
||||
setCookie("NEXT_LOCALE", newLanguage, { path: "/" })
|
||||
router.push(router.asPath, undefined, { locale: newLanguage })
|
||||
}
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
setErrors({
|
||||
email: "",
|
||||
password: "",
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t("menu.login")}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className="Login Dialog"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">
|
||||
{t("modals.login.title")}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<form className="form" onSubmit={login}>
|
||||
<Fieldset
|
||||
fieldName="email"
|
||||
placeholder={t("modals.login.placeholders.email")}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
ref={emailInput}
|
||||
/>
|
||||
|
||||
<Fieldset
|
||||
fieldName="password"
|
||||
placeholder={t("modals.login.placeholders.password")}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
ref={passwordInput}
|
||||
/>
|
||||
|
||||
<Button>{t("modals.login.buttons.confirm")}</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginModal
|
||||
export default LoginModal
|
||||
|
|
|
|||
|
|
@ -1,254 +1,291 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { getCookie } from "cookies-next"
|
||||
import clonedeep from "lodash.clonedeep"
|
||||
|
||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||
import PartyDetails from '~components/PartyDetails'
|
||||
import WeaponGrid from '~components/WeaponGrid'
|
||||
import SummonGrid from '~components/SummonGrid'
|
||||
import CharacterGrid from '~components/CharacterGrid'
|
||||
import PartySegmentedControl from "~components/PartySegmentedControl"
|
||||
import PartyDetails from "~components/PartyDetails"
|
||||
import WeaponGrid from "~components/WeaponGrid"
|
||||
import SummonGrid from "~components/SummonGrid"
|
||||
import CharacterGrid from "~components/CharacterGrid"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState, initialAppState } from '~utils/appState'
|
||||
import { GridType, TeamElement } from '~utils/enums'
|
||||
import api from "~utils/api"
|
||||
import { appState, initialAppState } from "~utils/appState"
|
||||
import { GridType, TeamElement } from "~utils/enums"
|
||||
|
||||
import './index.scss'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
new?: boolean
|
||||
slug?: string
|
||||
pushHistory?: (path: string) => void
|
||||
new?: boolean
|
||||
team?: Party
|
||||
raids: Raid[][]
|
||||
pushHistory?: (path: string) => void
|
||||
}
|
||||
|
||||
const Party = (props: Props) => {
|
||||
// Cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = useMemo(() => {
|
||||
return (cookies.account != null) ? {
|
||||
headers: { 'Authorization': `Bearer ${cookies.account.access_token}` }
|
||||
} : {}
|
||||
}, [cookies.account])
|
||||
const Party = (props: Props) => {
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const headers = useMemo(() => {
|
||||
return accountData
|
||||
? { headers: { Authorization: `Bearer ${accountData.token}` } }
|
||||
: {}
|
||||
}, [accountData])
|
||||
|
||||
// Set up states
|
||||
const { party } = useSnapshot(appState)
|
||||
const jobState = party.job
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
||||
// Set up states
|
||||
const { party } = useSnapshot(appState)
|
||||
const jobState = party.job
|
||||
|
||||
// Reset state on first load
|
||||
useEffect(() => {
|
||||
const resetState = clonedeep(initialAppState)
|
||||
appState.grid = resetState.grid
|
||||
}, [])
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
||||
|
||||
useEffect(() => {
|
||||
setJob(jobState)
|
||||
}, [jobState])
|
||||
// Reset state on first load
|
||||
useEffect(() => {
|
||||
const resetState = clonedeep(initialAppState)
|
||||
appState.grid = resetState.grid
|
||||
if (props.team) storeParty(props.team)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
jobChanged()
|
||||
}, [job])
|
||||
useEffect(() => {
|
||||
setJob(jobState)
|
||||
}, [jobState])
|
||||
|
||||
// Methods: Creating a new party
|
||||
async function createParty(extra: boolean = false) {
|
||||
let body = {
|
||||
party: {
|
||||
...(cookies.account) && { user_id: cookies.account.user_id },
|
||||
extra: extra
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
jobChanged()
|
||||
}, [job])
|
||||
|
||||
return await api.endpoints.parties.create(body, headers)
|
||||
// Methods: Creating a new party
|
||||
async function createParty(extra: boolean = false) {
|
||||
let body = {
|
||||
party: {
|
||||
...(accountData && { user_id: accountData.userId }),
|
||||
extra: extra,
|
||||
},
|
||||
}
|
||||
|
||||
// Methods: Updating the party's details
|
||||
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
appState.party.extra = event.target.checked
|
||||
return await api.endpoints.parties.create(body, headers)
|
||||
}
|
||||
|
||||
if (party.id) {
|
||||
api.endpoints.parties.update(party.id, {
|
||||
'party': { 'extra': event.target.checked }
|
||||
}, headers)
|
||||
}
|
||||
// Methods: Updating the party's details
|
||||
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
appState.party.extra = event.target.checked
|
||||
|
||||
if (party.id) {
|
||||
api.endpoints.parties.update(
|
||||
party.id,
|
||||
{
|
||||
party: { extra: event.target.checked },
|
||||
},
|
||||
headers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function jobChanged() {
|
||||
if (party.id) {
|
||||
api.endpoints.parties.update(party.id, {
|
||||
'party': { 'job_id': (job) ? job.id : '' }
|
||||
}, headers)
|
||||
}
|
||||
function jobChanged() {
|
||||
if (party.id && appState.party.editable) {
|
||||
api.endpoints.parties.update(
|
||||
party.id,
|
||||
{
|
||||
party: { job_id: job ? job.id : "" },
|
||||
},
|
||||
headers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function updateDetails(name?: string, description?: string, raid?: Raid) {
|
||||
if (appState.party.name !== name ||
|
||||
appState.party.description !== description ||
|
||||
appState.party.raid?.id !== raid?.id) {
|
||||
if (appState.party.id)
|
||||
api.endpoints.parties.update(appState.party.id, {
|
||||
'party': {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'raid_id': raid?.id
|
||||
}
|
||||
}, headers)
|
||||
.then(() => {
|
||||
appState.party.name = name
|
||||
appState.party.description = description
|
||||
appState.party.raid = raid
|
||||
appState.party.updated_at = party.updated_at
|
||||
})
|
||||
}
|
||||
function updateDetails(name?: string, description?: string, raid?: Raid) {
|
||||
if (
|
||||
appState.party.name !== name ||
|
||||
appState.party.description !== description ||
|
||||
appState.party.raid?.id !== raid?.id
|
||||
) {
|
||||
if (appState.party.id)
|
||||
api.endpoints.parties
|
||||
.update(
|
||||
appState.party.id,
|
||||
{
|
||||
party: {
|
||||
name: name,
|
||||
description: description,
|
||||
raid_id: raid?.id,
|
||||
},
|
||||
},
|
||||
headers
|
||||
)
|
||||
.then(() => {
|
||||
appState.party.name = name
|
||||
appState.party.description = description
|
||||
appState.party.raid = raid
|
||||
appState.party.updated_at = party.updated_at
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting the party
|
||||
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
if (appState.party.editable && appState.party.id) {
|
||||
api.endpoints.parties.destroy({ id: appState.party.id, params: headers })
|
||||
.then(() => {
|
||||
// Push to route
|
||||
router.push('/')
|
||||
// Deleting the party
|
||||
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
if (appState.party.editable && appState.party.id) {
|
||||
api.endpoints.parties
|
||||
.destroy({ id: appState.party.id, params: headers })
|
||||
.then(() => {
|
||||
// Push to route
|
||||
router.push("/")
|
||||
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAppState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
appState[key] = resetState[key]
|
||||
})
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAppState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
appState[key] = resetState[key]
|
||||
})
|
||||
|
||||
// Set party to be editable
|
||||
appState.party.editable = true
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
// Set party to be editable
|
||||
appState.party.editable = true
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Navigating with segmented control
|
||||
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
switch(event.target.value) {
|
||||
case 'class':
|
||||
setCurrentTab(GridType.Class)
|
||||
break
|
||||
case 'characters':
|
||||
setCurrentTab(GridType.Character)
|
||||
break
|
||||
case 'weapons':
|
||||
setCurrentTab(GridType.Weapon)
|
||||
break
|
||||
case 'summons':
|
||||
setCurrentTab(GridType.Summon)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
// 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
|
||||
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
switch (event.target.value) {
|
||||
case "class":
|
||||
setCurrentTab(GridType.Class)
|
||||
break
|
||||
case "characters":
|
||||
setCurrentTab(GridType.Character)
|
||||
break
|
||||
case "weapons":
|
||||
setCurrentTab(GridType.Weapon)
|
||||
break
|
||||
case "summons":
|
||||
setCurrentTab(GridType.Summon)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Render: JSX components
|
||||
const navigation = (
|
||||
<PartySegmentedControl
|
||||
selectedTab={currentTab}
|
||||
onClick={segmentClicked}
|
||||
onCheckboxChange={checkboxChanged}
|
||||
/>
|
||||
)
|
||||
|
||||
// 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 weaponGrid = (
|
||||
<WeaponGrid
|
||||
new={props.new || false}
|
||||
weapons={props.team?.weapons}
|
||||
createParty={createParty}
|
||||
pushHistory={props.pushHistory}
|
||||
/>
|
||||
)
|
||||
|
||||
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 summonGrid = (
|
||||
<SummonGrid
|
||||
new={props.new || false}
|
||||
summons={props.team?.summons}
|
||||
createParty={createParty}
|
||||
pushHistory={props.pushHistory}
|
||||
/>
|
||||
)
|
||||
|
||||
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])
|
||||
const characterGrid = (
|
||||
<CharacterGrid
|
||||
new={props.new || false}
|
||||
characters={props.team?.characters}
|
||||
createParty={createParty}
|
||||
pushHistory={props.pushHistory}
|
||||
/>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const shortcode = (props.slug) ? props.slug : undefined
|
||||
if (shortcode) fetchDetails(shortcode)
|
||||
}, [props.slug, fetchDetails])
|
||||
const currentGrid = () => {
|
||||
switch (currentTab) {
|
||||
case GridType.Character:
|
||||
return characterGrid
|
||||
case GridType.Weapon:
|
||||
return weaponGrid
|
||||
case GridType.Summon:
|
||||
return summonGrid
|
||||
}
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
const navigation = (
|
||||
<PartySegmentedControl
|
||||
selectedTab={currentTab}
|
||||
onClick={segmentClicked}
|
||||
onCheckboxChange={checkboxChanged}
|
||||
return (
|
||||
<div>
|
||||
{navigation}
|
||||
<section id="Party">{currentGrid()}</section>
|
||||
{
|
||||
<PartyDetails
|
||||
editable={party.editable}
|
||||
updateCallback={updateDetails}
|
||||
deleteCallback={deleteTeam}
|
||||
/>
|
||||
)
|
||||
|
||||
const weaponGrid = (
|
||||
<WeaponGrid
|
||||
new={props.new || false}
|
||||
slug={props.slug}
|
||||
createParty={createParty}
|
||||
pushHistory={props.pushHistory}
|
||||
/>
|
||||
)
|
||||
|
||||
const summonGrid = (
|
||||
<SummonGrid
|
||||
new={props.new || false}
|
||||
slug={props.slug}
|
||||
createParty={createParty}
|
||||
pushHistory={props.pushHistory}
|
||||
/>
|
||||
)
|
||||
|
||||
const characterGrid = (
|
||||
<CharacterGrid
|
||||
new={props.new || false}
|
||||
slug={props.slug}
|
||||
createParty={createParty}
|
||||
pushHistory={props.pushHistory}
|
||||
/>
|
||||
)
|
||||
|
||||
const currentGrid = () => {
|
||||
switch(currentTab) {
|
||||
case GridType.Character:
|
||||
return characterGrid
|
||||
case GridType.Weapon:
|
||||
return weaponGrid
|
||||
case GridType.Summon:
|
||||
return summonGrid
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ navigation }
|
||||
<section id="Party">
|
||||
{ currentGrid() }
|
||||
</section>
|
||||
{ <PartyDetails
|
||||
editable={party.editable}
|
||||
updateCallback={updateDetails}
|
||||
deleteCallback={deleteTeam}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Party
|
||||
|
|
|
|||
|
|
@ -1,317 +1,350 @@
|
|||
import React, { useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React, { useState } from "react"
|
||||
import Head from "next/head"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import Linkify from 'react-linkify'
|
||||
import classNames from 'classnames'
|
||||
import Linkify from "react-linkify"
|
||||
import classNames from "classnames"
|
||||
|
||||
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog"
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
|
||||
import Button from '~components/Button'
|
||||
import CharLimitedFieldset from '~components/CharLimitedFieldset'
|
||||
import RaidDropdown from '~components/RaidDropdown'
|
||||
import TextFieldset from '~components/TextFieldset'
|
||||
import Button from "~components/Button"
|
||||
import CharLimitedFieldset from "~components/CharLimitedFieldset"
|
||||
import RaidDropdown from "~components/RaidDropdown"
|
||||
import TextFieldset from "~components/TextFieldset"
|
||||
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { appState } from '~utils/appState'
|
||||
import { accountState } from "~utils/accountState"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import './index.scss'
|
||||
import Link from 'next/link'
|
||||
import { formatTimeAgo } from '~utils/timeAgo'
|
||||
import "./index.scss"
|
||||
import Link from "next/link"
|
||||
import { formatTimeAgo } from "~utils/timeAgo"
|
||||
|
||||
const emptyRaid: Raid = {
|
||||
id: '',
|
||||
name: {
|
||||
en: '',
|
||||
ja: ''
|
||||
},
|
||||
slug: '',
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0
|
||||
id: "",
|
||||
name: {
|
||||
en: "",
|
||||
ja: "",
|
||||
},
|
||||
slug: "",
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
editable: boolean
|
||||
updateCallback: (name?: string, description?: string, raid?: Raid) => void
|
||||
deleteCallback: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
editable: boolean
|
||||
updateCallback: (name?: string, description?: string, raid?: Raid) => void
|
||||
deleteCallback: (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => void
|
||||
}
|
||||
|
||||
const PartyDetails = (props: Props) => {
|
||||
const { party, raids } = useSnapshot(appState)
|
||||
const { account } = useSnapshot(accountState)
|
||||
const { party, raids } = useSnapshot(appState)
|
||||
const { account } = useSnapshot(accountState)
|
||||
|
||||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
const locale = router.locale || 'en'
|
||||
const { t } = useTranslation("common")
|
||||
const router = useRouter()
|
||||
const locale = router.locale || "en"
|
||||
|
||||
const nameInput = React.createRef<HTMLInputElement>()
|
||||
const descriptionInput = React.createRef<HTMLTextAreaElement>()
|
||||
const raidSelect = React.createRef<HTMLSelectElement>()
|
||||
const nameInput = React.createRef<HTMLInputElement>()
|
||||
const descriptionInput = React.createRef<HTMLTextAreaElement>()
|
||||
const raidSelect = React.createRef<HTMLSelectElement>()
|
||||
|
||||
const readOnlyClasses = classNames({
|
||||
'PartyDetails': true,
|
||||
'ReadOnly': true,
|
||||
'Visible': !party.detailsVisible
|
||||
})
|
||||
const readOnlyClasses = classNames({
|
||||
PartyDetails: true,
|
||||
ReadOnly: true,
|
||||
Visible: !party.detailsVisible,
|
||||
})
|
||||
|
||||
const editableClasses = classNames({
|
||||
'PartyDetails': true,
|
||||
'Editable': true,
|
||||
'Visible': party.detailsVisible
|
||||
})
|
||||
const editableClasses = classNames({
|
||||
PartyDetails: true,
|
||||
Editable: true,
|
||||
Visible: party.detailsVisible,
|
||||
})
|
||||
|
||||
const emptyClasses = classNames({
|
||||
'EmptyDetails': true,
|
||||
'Visible': !party.detailsVisible
|
||||
})
|
||||
const emptyClasses = classNames({
|
||||
EmptyDetails: true,
|
||||
Visible: !party.detailsVisible,
|
||||
})
|
||||
|
||||
const userClass = classNames({
|
||||
'user': true,
|
||||
'empty': !party.user
|
||||
})
|
||||
const userClass = classNames({
|
||||
user: true,
|
||||
empty: !party.user,
|
||||
})
|
||||
|
||||
const linkClass = classNames({
|
||||
'wind': party && party.element == 1,
|
||||
'fire': party && party.element == 2,
|
||||
'water': party && party.element == 3,
|
||||
'earth': party && party.element == 4,
|
||||
'dark': party && party.element == 5,
|
||||
'light': party && party.element == 6
|
||||
})
|
||||
const linkClass = classNames({
|
||||
wind: party && party.element == 1,
|
||||
fire: party && party.element == 2,
|
||||
water: party && party.element == 3,
|
||||
earth: party && party.element == 4,
|
||||
dark: party && party.element == 5,
|
||||
light: party && party.element == 6,
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
||||
name: '',
|
||||
description: ''
|
||||
})
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
||||
name: "",
|
||||
description: "",
|
||||
})
|
||||
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const { name, value } = event.target
|
||||
let newErrors = errors
|
||||
const { name, value } = event.target
|
||||
let newErrors = errors
|
||||
|
||||
setErrors(newErrors)
|
||||
}
|
||||
setErrors(newErrors)
|
||||
}
|
||||
|
||||
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
event.preventDefault()
|
||||
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const { name, value } = event.target
|
||||
let newErrors = errors
|
||||
const { name, value } = event.target
|
||||
let newErrors = errors
|
||||
|
||||
setErrors(newErrors)
|
||||
}
|
||||
setErrors(newErrors)
|
||||
}
|
||||
|
||||
function toggleDetails() {
|
||||
appState.party.detailsVisible = !appState.party.detailsVisible
|
||||
}
|
||||
function toggleDetails() {
|
||||
appState.party.detailsVisible = !appState.party.detailsVisible
|
||||
}
|
||||
|
||||
function updateDetails(event: React.MouseEvent) {
|
||||
const nameValue = nameInput.current?.value
|
||||
const descriptionValue = descriptionInput.current?.value
|
||||
const raid = raids.find(raid => raid.slug === raidSelect.current?.value)
|
||||
function updateDetails(event: React.MouseEvent) {
|
||||
const nameValue = nameInput.current?.value
|
||||
const descriptionValue = descriptionInput.current?.value
|
||||
const raid = raids.find((raid) => raid.slug === raidSelect.current?.value)
|
||||
|
||||
props.updateCallback(nameValue, descriptionValue, raid)
|
||||
toggleDetails()
|
||||
}
|
||||
props.updateCallback(nameValue, descriptionValue, raid)
|
||||
toggleDetails()
|
||||
}
|
||||
|
||||
const userImage = () => {
|
||||
if (party.user)
|
||||
return (
|
||||
<img
|
||||
alt={party.user.picture.picture}
|
||||
className={`profile ${party.user.picture.element}`}
|
||||
srcSet={`/profile/${party.user.picture.picture}.png,
|
||||
const userImage = () => {
|
||||
if (party.user)
|
||||
return (
|
||||
<img
|
||||
alt={party.user.picture.picture}
|
||||
className={`profile ${party.user.picture.element}`}
|
||||
srcSet={`/profile/${party.user.picture.picture}.png,
|
||||
/profile/${party.user.picture.picture}@2x.png 2x`}
|
||||
src={`/profile/${party.user.picture.picture}.png`}
|
||||
/>
|
||||
)
|
||||
else
|
||||
return (<div className="no-user" />)
|
||||
}
|
||||
|
||||
const userBlock = () => {
|
||||
return (
|
||||
<div className={userClass}>
|
||||
{ userImage() }
|
||||
{ (party.user) ? party.user.username : t('no_user') }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const linkedUserBlock = (user: User) => {
|
||||
return (
|
||||
<div>
|
||||
<Link href={`/${user.username}`} passHref>
|
||||
<a className={linkClass}>{userBlock()}</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const linkedRaidBlock = (raid: Raid) => {
|
||||
return (
|
||||
<div>
|
||||
<Link href={`/teams?raid=${raid.slug}`} passHref>
|
||||
<a className={`Raid ${linkClass}`}>
|
||||
{raid.name[locale]}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const deleteButton = () => {
|
||||
if (party.editable) {
|
||||
return (
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger className="Button destructive">
|
||||
<span className='icon'>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
<span className="text">{t('buttons.delete')}</span>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className="Overlay" />
|
||||
<AlertDialog.Content className="Dialog">
|
||||
<AlertDialog.Title className="DialogTitle">
|
||||
{t('modals.delete_team.title')}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description className="DialogDescription">
|
||||
{t('modals.delete_team.description')}
|
||||
</AlertDialog.Description>
|
||||
<div className="actions">
|
||||
<AlertDialog.Cancel className="Button modal">{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>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
)
|
||||
} else {
|
||||
return ('')
|
||||
}
|
||||
}
|
||||
|
||||
const editable = (
|
||||
<section className={editableClasses}>
|
||||
<CharLimitedFieldset
|
||||
fieldName="name"
|
||||
placeholder="Name your team"
|
||||
value={party.name}
|
||||
limit={50}
|
||||
onChange={handleInputChange}
|
||||
error={errors.name}
|
||||
ref={nameInput}
|
||||
/>
|
||||
<RaidDropdown
|
||||
showAllRaidsOption={false}
|
||||
currentRaid={party.raid?.slug || ''}
|
||||
ref={raidSelect}
|
||||
/>
|
||||
<TextFieldset
|
||||
fieldName="name"
|
||||
placeholder={"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediel’s 1 first\nGood luck with RNG!"}
|
||||
value={party.description}
|
||||
onChange={handleTextAreaChange}
|
||||
error={errors.description}
|
||||
ref={descriptionInput}
|
||||
/>
|
||||
|
||||
<div className="bottom">
|
||||
<div className="left">
|
||||
{ (router.pathname !== '/new') ? deleteButton() : '' }
|
||||
</div>
|
||||
<div className="right">
|
||||
<Button
|
||||
active={true}
|
||||
onClick={toggleDetails}>
|
||||
{t('buttons.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
active={true}
|
||||
icon="check"
|
||||
onClick={updateDetails}>
|
||||
{t('buttons.save_info')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const readOnly = (
|
||||
<section className={readOnlyClasses}>
|
||||
<div className="info">
|
||||
<div className="left">
|
||||
{ (party.name) ? <h1>{party.name}</h1> : '' }
|
||||
<div className="attribution">
|
||||
{ (party.user) ? linkedUserBlock(party.user) : userBlock() }
|
||||
{ (party.raid) ? linkedRaidBlock(party.raid) : '' }
|
||||
{ (party.created_at != undefined)
|
||||
? <time
|
||||
className="last-updated"
|
||||
dateTime={new Date(party.created_at).toString()}>
|
||||
{formatTimeAgo(new Date(party.created_at), locale)}
|
||||
</time>
|
||||
: '' }
|
||||
</div>
|
||||
</div>
|
||||
<div className="right">
|
||||
{ (party.editable)
|
||||
? <Button active={true} icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button>
|
||||
: <div /> }
|
||||
</div>
|
||||
</div>
|
||||
{ (party.description) ? <p><Linkify>{party.description}</Linkify></p> : '' }
|
||||
</section>
|
||||
)
|
||||
|
||||
const emptyDetails = (
|
||||
<div className={emptyClasses}>
|
||||
<Button active={true} icon="edit" onClick={toggleDetails}>{t('buttons.show_info')}</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const generateTitle = () => {
|
||||
let title = ''
|
||||
|
||||
const username = (party.user != null) ? `@${party.user?.username}` : 'Anonymous'
|
||||
|
||||
if (party.name != null)
|
||||
title = `${party.name} by ${username}`
|
||||
else if (party.name == null && party.editable && router.route === '/new')
|
||||
title = "New Team"
|
||||
else
|
||||
title = `Untitled team by ${username}`
|
||||
|
||||
return title
|
||||
}
|
||||
src={`/profile/${party.user.picture.picture}.png`}
|
||||
/>
|
||||
)
|
||||
else return <div className="no-user" />
|
||||
}
|
||||
|
||||
const userBlock = () => {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{generateTitle()}</title>
|
||||
|
||||
<meta property="og:title" content={generateTitle()} />
|
||||
<meta property="og:description" content={ (party.description) ? party.description : '' } />
|
||||
<meta property="og:url" content="https://app.granblue.team" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content={generateTitle()} />
|
||||
<meta name="twitter:description" content={ (party.description) ? party.description : '' } />
|
||||
</Head>
|
||||
{ (editable && (party.name || party.description || party.raid)) ? readOnly : emptyDetails}
|
||||
{editable}
|
||||
</div>
|
||||
<div className={userClass}>
|
||||
{userImage()}
|
||||
{party.user ? party.user.username : t("no_user")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const linkedUserBlock = (user: User) => {
|
||||
return (
|
||||
<div>
|
||||
<Link href={`/${user.username}`} passHref>
|
||||
<a className={linkClass}>{userBlock()}</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const linkedRaidBlock = (raid: Raid) => {
|
||||
return (
|
||||
<div>
|
||||
<Link href={`/teams?raid=${raid.slug}`} passHref>
|
||||
<a className={`Raid ${linkClass}`}>{raid.name[locale]}</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const deleteButton = () => {
|
||||
if (party.editable) {
|
||||
return (
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger className="Button destructive">
|
||||
<span className="icon">
|
||||
<CrossIcon />
|
||||
</span>
|
||||
<span className="text">{t("buttons.delete")}</span>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className="Overlay" />
|
||||
<AlertDialog.Content className="Dialog">
|
||||
<AlertDialog.Title className="DialogTitle">
|
||||
{t("modals.delete_team.title")}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description className="DialogDescription">
|
||||
{t("modals.delete_team.description")}
|
||||
</AlertDialog.Description>
|
||||
<div className="actions">
|
||||
<AlertDialog.Cancel className="Button modal">
|
||||
{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>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
const editable = (
|
||||
<section className={editableClasses}>
|
||||
<CharLimitedFieldset
|
||||
fieldName="name"
|
||||
placeholder="Name your team"
|
||||
value={party.name}
|
||||
limit={50}
|
||||
onChange={handleInputChange}
|
||||
error={errors.name}
|
||||
ref={nameInput}
|
||||
/>
|
||||
<RaidDropdown
|
||||
showAllRaidsOption={false}
|
||||
currentRaid={party.raid?.slug || ""}
|
||||
ref={raidSelect}
|
||||
/>
|
||||
<TextFieldset
|
||||
fieldName="name"
|
||||
placeholder={
|
||||
"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediel’s 1 first\nGood luck with RNG!"
|
||||
}
|
||||
value={party.description}
|
||||
onChange={handleTextAreaChange}
|
||||
error={errors.description}
|
||||
ref={descriptionInput}
|
||||
/>
|
||||
|
||||
<div className="bottom">
|
||||
<div className="left">
|
||||
{router.pathname !== "/new" ? deleteButton() : ""}
|
||||
</div>
|
||||
<div className="right">
|
||||
<Button active={true} onClick={toggleDetails}>
|
||||
{t("buttons.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button active={true} icon="check" onClick={updateDetails}>
|
||||
{t("buttons.save_info")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
const readOnly = (
|
||||
<section className={readOnlyClasses}>
|
||||
<div className="info">
|
||||
<div className="left">
|
||||
{party.name ? <h1>{party.name}</h1> : ""}
|
||||
<div className="attribution">
|
||||
{party.user ? linkedUserBlock(party.user) : userBlock()}
|
||||
{party.raid ? linkedRaidBlock(party.raid) : ""}
|
||||
{party.created_at != undefined ? (
|
||||
<time
|
||||
className="last-updated"
|
||||
dateTime={new Date(party.created_at).toString()}
|
||||
>
|
||||
{formatTimeAgo(new Date(party.created_at), locale)}
|
||||
</time>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="right">
|
||||
{party.editable ? (
|
||||
<Button active={true} icon="edit" onClick={toggleDetails}>
|
||||
{t("buttons.show_info")}
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{party.description ? (
|
||||
<p>
|
||||
<Linkify>{party.description}</Linkify>
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
const emptyDetails = (
|
||||
<div className={emptyClasses}>
|
||||
{party.editable ? (
|
||||
<Button active={true} icon="edit" onClick={toggleDetails}>
|
||||
{t("buttons.show_info")}
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const generateTitle = () => {
|
||||
let title = party.raid ? `[${party.raid?.name[locale]}] ` : ""
|
||||
|
||||
const username =
|
||||
party.user != null ? `@${party.user?.username}` : t("header.anonymous")
|
||||
|
||||
if (party.name != null)
|
||||
title += t("header.byline", { partyName: party.name, username: username })
|
||||
else if (party.name == null && party.editable && router.route === "/new")
|
||||
title = t("header.new_team")
|
||||
else
|
||||
title += t("header.untitled_team", {
|
||||
username: username,
|
||||
})
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{generateTitle()}</title>
|
||||
|
||||
<meta property="og:title" content={generateTitle()} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={party.description ? party.description : ""}
|
||||
/>
|
||||
<meta property="og:url" content="https://app.granblue.team" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content={generateTitle()} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={party.description ? party.description : ""}
|
||||
/>
|
||||
</Head>
|
||||
{editable && (party.name || party.description || party.raid)
|
||||
? readOnly
|
||||
: emptyDetails}
|
||||
{editable}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartyDetails
|
||||
|
|
|
|||
|
|
@ -1,311 +1,348 @@
|
|||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
import { getCookie, setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import InfiniteScroll from "react-infinite-scroll-component"
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
import api from '~utils/api'
|
||||
import { appState } from "~utils/appState"
|
||||
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 WeaponSearchFilterBar from '~components/WeaponSearchFilterBar'
|
||||
import SummonSearchFilterBar from '~components/SummonSearchFilterBar'
|
||||
import CharacterSearchFilterBar from "~components/CharacterSearchFilterBar"
|
||||
import WeaponSearchFilterBar from "~components/WeaponSearchFilterBar"
|
||||
import SummonSearchFilterBar from "~components/SummonSearchFilterBar"
|
||||
|
||||
import CharacterResult from '~components/CharacterResult'
|
||||
import WeaponResult from '~components/WeaponResult'
|
||||
import SummonResult from '~components/SummonResult'
|
||||
import CharacterResult from "~components/CharacterResult"
|
||||
import WeaponResult from "~components/WeaponResult"
|
||||
import SummonResult from "~components/SummonResult"
|
||||
|
||||
import './index.scss'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import "./index.scss"
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import cloneDeep from "lodash.clonedeep"
|
||||
|
||||
interface Props {
|
||||
send: (object: Character | Weapon | Summon, position: number) => any
|
||||
placeholderText: string
|
||||
fromPosition: number
|
||||
object: 'weapons' | 'characters' | 'summons',
|
||||
children: React.ReactNode
|
||||
send: (object: Character | Weapon | Summon, position: number) => any
|
||||
placeholderText: string
|
||||
fromPosition: number
|
||||
object: "weapons" | "characters" | "summons"
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SearchModal = (props: Props) => {
|
||||
// Set up snapshot of app state
|
||||
let { grid, search } = useSnapshot(appState)
|
||||
// Set up snapshot of app state
|
||||
let { grid, search } = useSnapshot(appState)
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const locale = router.locale
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const locale = router.locale
|
||||
|
||||
// Set up translation
|
||||
const { t } = useTranslation('common')
|
||||
// Set up translation
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Set up cookies
|
||||
const [cookies, setCookies] = useCookies()
|
||||
let searchInput = React.createRef<HTMLInputElement>()
|
||||
let scrollContainer = React.createRef<HTMLDivElement>()
|
||||
|
||||
let searchInput = React.createRef<HTMLInputElement>()
|
||||
let scrollContainer = React.createRef<HTMLDivElement>()
|
||||
const [firstLoad, setFirstLoad] = useState(true)
|
||||
const [objects, setObjects] = useState<{
|
||||
[id: number]: GridCharacter | GridWeapon | GridSummon
|
||||
}>()
|
||||
const [filters, setFilters] = useState<{ [key: string]: number[] }>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState("")
|
||||
const [results, setResults] = useState<(Weapon | Summon | Character)[]>([])
|
||||
|
||||
const [firstLoad, setFirstLoad] = useState(true)
|
||||
const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>()
|
||||
const [filters, setFilters] = useState<{ [key: string]: number[] }>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<(Weapon | Summon | Character)[]>([])
|
||||
// Pagination states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Pagination states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
useEffect(() => {
|
||||
setObjects(grid[props.object])
|
||||
}, [grid, props.object])
|
||||
|
||||
useEffect(() => {
|
||||
setObjects(grid[props.object])
|
||||
}, [grid, props.object])
|
||||
useEffect(() => {
|
||||
if (searchInput.current) searchInput.current.focus()
|
||||
}, [searchInput])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInput.current)
|
||||
searchInput.current.focus()
|
||||
}, [searchInput])
|
||||
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const text = event.target.value
|
||||
if (text.length) {
|
||||
setQuery(text)
|
||||
} else {
|
||||
setQuery("")
|
||||
}
|
||||
}
|
||||
|
||||
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const text = event.target.value
|
||||
if (text.length) {
|
||||
setQuery(text)
|
||||
function fetchResults({ replace = false }: { replace?: boolean }) {
|
||||
api
|
||||
.search({
|
||||
object: props.object,
|
||||
query: query,
|
||||
filters: filters,
|
||||
locale: locale,
|
||||
page: currentPage,
|
||||
})
|
||||
.then((response) => {
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
|
||||
if (replace) {
|
||||
replaceResults(response.data.count, response.data.results)
|
||||
} else {
|
||||
setQuery('')
|
||||
appendResults(response.data.results)
|
||||
}
|
||||
}
|
||||
|
||||
function fetchResults({ replace = false }: { replace?: boolean }) {
|
||||
api.search({
|
||||
object: props.object,
|
||||
query: query,
|
||||
filters: filters,
|
||||
locale: locale,
|
||||
page: currentPage
|
||||
}).then(response => {
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
if (replace) {
|
||||
replaceResults(response.data.count, response.data.results)
|
||||
} else {
|
||||
appendResults(response.data.results)
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
function replaceResults(
|
||||
count: number,
|
||||
list: Weapon[] | Summon[] | Character[]
|
||||
) {
|
||||
if (count > 0) {
|
||||
setResults(list)
|
||||
} else {
|
||||
setResults([])
|
||||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Weapon[] | Summon[] | Character[]) {
|
||||
setResults([...results, ...list])
|
||||
}
|
||||
|
||||
function storeRecentResult(result: Character | Weapon | Summon) {
|
||||
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[] = []
|
||||
|
||||
if (props.object === "weapons") {
|
||||
recents = cloneDeep(cookieObj as Weapon[]) || []
|
||||
if (!recents.find((item) => item.granblue_id === result.granblue_id)) {
|
||||
recents.unshift(result as Weapon)
|
||||
}
|
||||
} else if (props.object === "summons") {
|
||||
recents = cloneDeep(cookieObj as Summon[]) || []
|
||||
if (!recents.find((item) => item.granblue_id === result.granblue_id)) {
|
||||
recents.unshift(result as Summon)
|
||||
}
|
||||
}
|
||||
|
||||
function replaceResults(count: number, list: Weapon[] | Summon[] | Character[]) {
|
||||
if (count > 0) {
|
||||
setResults(list)
|
||||
} else {
|
||||
setResults([])
|
||||
}
|
||||
if (recents && recents.length > 5) recents.pop()
|
||||
setCookie(`recent_${props.object}`, recents, { path: "/" })
|
||||
sendData(result)
|
||||
}
|
||||
|
||||
function sendData(result: Character | Weapon | Summon) {
|
||||
props.send(result, props.fromPosition)
|
||||
openChange()
|
||||
}
|
||||
|
||||
function receiveFilters(filters: { [key: string]: number[] }) {
|
||||
setCurrentPage(1)
|
||||
setResults([])
|
||||
setFilters(filters)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Current page changed
|
||||
if (open && currentPage > 1) {
|
||||
fetchResults({ replace: false })
|
||||
} else if (open && currentPage == 1) {
|
||||
fetchResults({ replace: true })
|
||||
}
|
||||
}, [currentPage])
|
||||
|
||||
function appendResults(list: Weapon[] | Summon[] | Character[]) {
|
||||
setResults([...results, ...list])
|
||||
}
|
||||
useEffect(() => {
|
||||
// Filters changed
|
||||
const key = `recent_${props.object}`
|
||||
const cookie = getCookie(key)
|
||||
const cookieObj: Weapon[] | Summon[] | Character[] = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: []
|
||||
|
||||
function storeRecentResult(result: Character | Weapon | Summon) {
|
||||
const key = `recent_${props.object}`
|
||||
let recents: Character[] | Weapon[] | Summon[] = []
|
||||
|
||||
if (props.object === "weapons") {
|
||||
recents = cloneDeep(cookies[key] as Weapon[]) || []
|
||||
if (!recents.find(item => item.granblue_id === result.granblue_id)) {
|
||||
recents.unshift(result as Weapon)
|
||||
}
|
||||
} else if (props.object === "summons") {
|
||||
recents = cloneDeep(cookies[key] as Summon[]) || []
|
||||
if (!recents.find(item => item.granblue_id === result.granblue_id)) {
|
||||
recents.unshift(result as Summon)
|
||||
}
|
||||
}
|
||||
|
||||
if (recents && recents.length > 5) recents.pop()
|
||||
setCookies(`recent_${props.object}`, recents, { path: '/' })
|
||||
sendData(result)
|
||||
}
|
||||
|
||||
function sendData(result: Character | Weapon | Summon) {
|
||||
props.send(result, props.fromPosition)
|
||||
openChange()
|
||||
}
|
||||
|
||||
function receiveFilters(filters: { [key: string]: number[] }) {
|
||||
if (open) {
|
||||
if (firstLoad && cookieObj && cookieObj.length > 0) {
|
||||
setResults(cookieObj)
|
||||
setRecordCount(cookieObj.length)
|
||||
setFirstLoad(false)
|
||||
} else {
|
||||
setCurrentPage(1)
|
||||
setResults([])
|
||||
setFilters(filters)
|
||||
fetchResults({ replace: true })
|
||||
}
|
||||
}
|
||||
}, [filters])
|
||||
|
||||
useEffect(() => {
|
||||
// Current page changed
|
||||
if (open && currentPage > 1) {
|
||||
fetchResults({ replace: false })
|
||||
} else if (open && currentPage == 1) {
|
||||
fetchResults({ replace: true })
|
||||
}
|
||||
}, [currentPage])
|
||||
|
||||
useEffect(() => {
|
||||
// Filters changed
|
||||
const key = `recent_${props.object}`
|
||||
|
||||
if (open) {
|
||||
if (firstLoad && cookies[key] && cookies[key].length > 0) {
|
||||
setResults(cookies[key])
|
||||
setRecordCount(cookies[key].length)
|
||||
setFirstLoad(false)
|
||||
} else {
|
||||
setCurrentPage(1)
|
||||
fetchResults({ replace: true })
|
||||
}
|
||||
}
|
||||
}, [filters])
|
||||
|
||||
useEffect(() => {
|
||||
// Query changed
|
||||
if (open && query.length != 1) {
|
||||
setCurrentPage(1)
|
||||
fetchResults({ replace: true })
|
||||
}
|
||||
}, [query])
|
||||
|
||||
function renderResults() {
|
||||
let jsx
|
||||
|
||||
switch(props.object) {
|
||||
case 'weapons':
|
||||
jsx = renderWeaponSearchResults()
|
||||
break
|
||||
case 'summons':
|
||||
jsx = renderSummonSearchResults(results)
|
||||
break
|
||||
case 'characters':
|
||||
jsx = renderCharacterSearchResults(results)
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
dataLength={ (results && results.length > 0) ? results.length : 0}
|
||||
next={ () => setCurrentPage(currentPage + 1) }
|
||||
hasMore={totalPages > currentPage}
|
||||
scrollableTarget="Results"
|
||||
loader={<div className="footer">Loading...</div>}>
|
||||
{jsx}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
useEffect(() => {
|
||||
// Query changed
|
||||
if (open && query.length != 1) {
|
||||
setCurrentPage(1)
|
||||
fetchResults({ replace: true })
|
||||
}
|
||||
}, [query])
|
||||
|
||||
function renderWeaponSearchResults() {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: Weapon[] = results as Weapon[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: Weapon) => {
|
||||
return <WeaponResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { storeRecentResult(result) }}
|
||||
/>
|
||||
})
|
||||
}
|
||||
function renderResults() {
|
||||
let jsx
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function renderSummonSearchResults(results: { [key: string]: any }) {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: Summon[] = results as Summon[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: Summon) => {
|
||||
return <SummonResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { storeRecentResult(result) }}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function renderCharacterSearchResults(results: { [key: string]: any }) {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: Character[] = results as Character[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: Character) => {
|
||||
return <CharacterResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { storeRecentResult(result) }}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setQuery('')
|
||||
setFirstLoad(true)
|
||||
setResults([])
|
||||
setRecordCount(0)
|
||||
setCurrentPage(1)
|
||||
setOpen(false)
|
||||
} else {
|
||||
setOpen(true)
|
||||
}
|
||||
switch (props.object) {
|
||||
case "weapons":
|
||||
jsx = renderWeaponSearchResults()
|
||||
break
|
||||
case "summons":
|
||||
jsx = renderSummonSearchResults(results)
|
||||
break
|
||||
case "characters":
|
||||
jsx = renderCharacterSearchResults(results)
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
{props.children}
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Search Dialog">
|
||||
<div id="Header">
|
||||
<div id="Bar">
|
||||
<label className="search_label" htmlFor="search_input">
|
||||
<input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
name="query"
|
||||
className="Input"
|
||||
id="search_input"
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
placeholder={props.placeholderText}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
</label>
|
||||
<Dialog.Close className="DialogClose" onClick={openChange}>
|
||||
<CrossIcon />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
{ (props.object === 'characters') ? <CharacterSearchFilterBar sendFilters={receiveFilters} /> : '' }
|
||||
{ (props.object === 'weapons') ? <WeaponSearchFilterBar sendFilters={receiveFilters} /> : '' }
|
||||
{ (props.object === 'summons') ? <SummonSearchFilterBar sendFilters={receiveFilters} /> : '' }
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
<h5 className="total">{t('search.result_count', { "record_count": recordCount })}</h5>
|
||||
{ (open) ? renderResults() : ''}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<InfiniteScroll
|
||||
dataLength={results && results.length > 0 ? results.length : 0}
|
||||
next={() => setCurrentPage(currentPage + 1)}
|
||||
hasMore={totalPages > currentPage}
|
||||
scrollableTarget="Results"
|
||||
loader={<div className="footer">Loading...</div>}
|
||||
>
|
||||
{jsx}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
||||
|
||||
function renderWeaponSearchResults() {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: Weapon[] = results as Weapon[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: Weapon) => {
|
||||
return (
|
||||
<WeaponResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => {
|
||||
storeRecentResult(result)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function renderSummonSearchResults(results: { [key: string]: any }) {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: Summon[] = results as Summon[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: Summon) => {
|
||||
return (
|
||||
<SummonResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => {
|
||||
storeRecentResult(result)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function renderCharacterSearchResults(results: { [key: string]: any }) {
|
||||
let jsx: React.ReactNode
|
||||
|
||||
const castResults: Character[] = results as Character[]
|
||||
if (castResults && Object.keys(castResults).length > 0) {
|
||||
jsx = castResults.map((result: Character) => {
|
||||
return (
|
||||
<CharacterResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => {
|
||||
storeRecentResult(result)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return jsx
|
||||
}
|
||||
|
||||
function openChange() {
|
||||
if (open) {
|
||||
setQuery("")
|
||||
setFirstLoad(true)
|
||||
setResults([])
|
||||
setRecordCount(0)
|
||||
setCurrentPage(1)
|
||||
setOpen(false)
|
||||
} else {
|
||||
setOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Search Dialog">
|
||||
<div id="Header">
|
||||
<div id="Bar">
|
||||
<label className="search_label" htmlFor="search_input">
|
||||
<input
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
name="query"
|
||||
className="Input"
|
||||
id="search_input"
|
||||
ref={searchInput}
|
||||
value={query}
|
||||
placeholder={props.placeholderText}
|
||||
onChange={inputChanged}
|
||||
/>
|
||||
</label>
|
||||
<Dialog.Close className="DialogClose" onClick={openChange}>
|
||||
<CrossIcon />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
{props.object === "characters" ? (
|
||||
<CharacterSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "weapons" ? (
|
||||
<WeaponSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.object === "summons" ? (
|
||||
<SummonSearchFilterBar sendFilters={receiveFilters} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div id="Results" ref={scrollContainer}>
|
||||
<h5 className="total">
|
||||
{t("search.result_count", { record_count: recordCount })}
|
||||
</h5>
|
||||
{open ? renderResults() : ""}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchModal
|
||||
export default SearchModal
|
||||
|
|
|
|||
|
|
@ -1,306 +1,324 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Trans, useTranslation } from 'next-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import React, { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { setCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { Trans, useTranslation } from "next-i18next"
|
||||
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 { accountState } from '~utils/accountState'
|
||||
import api from "~utils/api"
|
||||
import { accountState } from "~utils/accountState"
|
||||
|
||||
import Button from '~components/Button'
|
||||
import Fieldset from '~components/Fieldset'
|
||||
import Button from "~components/Button"
|
||||
import Fieldset from "~components/Fieldset"
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface ErrorMap {
|
||||
[index: string]: string
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
passwordConfirmation: string
|
||||
[index: string]: string
|
||||
username: string
|
||||
email: string
|
||||
password: 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 router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up form states and error handling
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: ''
|
||||
})
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Cookies
|
||||
const [cookies, setCookies] = useCookies()
|
||||
// Set up form states and error handling
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [errors, setErrors] = useState<ErrorMap>({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
})
|
||||
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Set up form refs
|
||||
const usernameInput = React.createRef<HTMLInputElement>()
|
||||
const emailInput = React.createRef<HTMLInputElement>()
|
||||
const passwordInput = React.createRef<HTMLInputElement>()
|
||||
const passwordConfirmationInput = React.createRef<HTMLInputElement>()
|
||||
const form = [usernameInput, emailInput, passwordInput, passwordConfirmationInput]
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
function register(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
// Set up form refs
|
||||
const usernameInput = React.createRef<HTMLInputElement>()
|
||||
const emailInput = React.createRef<HTMLInputElement>()
|
||||
const passwordInput = React.createRef<HTMLInputElement>()
|
||||
const passwordConfirmationInput = React.createRef<HTMLInputElement>()
|
||||
const form = [
|
||||
usernameInput,
|
||||
emailInput,
|
||||
passwordInput,
|
||||
passwordConfirmationInput,
|
||||
]
|
||||
|
||||
const body = {
|
||||
user: {
|
||||
username: usernameInput.current?.value,
|
||||
email: emailInput.current?.value,
|
||||
password: passwordInput.current?.value,
|
||||
password_confirmation: passwordConfirmationInput.current?.value,
|
||||
language: router.locale
|
||||
}
|
||||
}
|
||||
function register(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
if (formValid)
|
||||
api.endpoints.users.create(body)
|
||||
.then(response => {
|
||||
storeCookieInfo(response)
|
||||
return response.data.user.user_id
|
||||
})
|
||||
.then(id => fetchUserInfo(id))
|
||||
.then(infoResponse => storeUserInfo(infoResponse))
|
||||
const body = {
|
||||
user: {
|
||||
username: usernameInput.current?.value,
|
||||
email: emailInput.current?.value,
|
||||
password: passwordInput.current?.value,
|
||||
password_confirmation: passwordConfirmationInput.current?.value,
|
||||
language: router.locale,
|
||||
},
|
||||
}
|
||||
|
||||
function storeCookieInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj = {
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
access_token: user.token
|
||||
}
|
||||
|
||||
setCookies('account', cookieObj, { path: '/'})
|
||||
}
|
||||
|
||||
function fetchUserInfo(id: string) {
|
||||
return api.userInfo(id)
|
||||
}
|
||||
|
||||
function storeUserInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj = {
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
// TODO: Set language
|
||||
setCookies('user', cookieObj, { path: '/'})
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
gender: user.gender
|
||||
}
|
||||
|
||||
accountState.account.authorized = true
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
function handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const fieldName = event.target.name
|
||||
const value = event.target.value
|
||||
|
||||
if (value.length >= 3) {
|
||||
api.check(fieldName, value)
|
||||
.then((response) => {
|
||||
processNameCheck(fieldName, value, response.data.available)
|
||||
}, (error) => {
|
||||
console.error(error)
|
||||
})
|
||||
} else {
|
||||
validateName(fieldName, value)
|
||||
}
|
||||
}
|
||||
|
||||
function processNameCheck(fieldName: string, value: string, available: boolean) {
|
||||
const newErrors = {...errors}
|
||||
|
||||
if (available) {
|
||||
// Continue checking for errors
|
||||
newErrors[fieldName] = ''
|
||||
setErrors(newErrors)
|
||||
setFormValid(true)
|
||||
|
||||
validateName(fieldName, value)
|
||||
} else {
|
||||
newErrors[fieldName] = t('modals.signup.errors.field_in_use', { field: fieldName})
|
||||
setErrors(newErrors)
|
||||
setFormValid(false)
|
||||
}
|
||||
}
|
||||
|
||||
function validateName(fieldName: string, value: string) {
|
||||
let newErrors = {...errors}
|
||||
|
||||
switch(fieldName) {
|
||||
case 'username':
|
||||
if (value.length < 3)
|
||||
newErrors.username = t('modals.signup.errors.username_too_short')
|
||||
else if (value.length > 20)
|
||||
newErrors.username = t('modals.signup.errors.username_too_long')
|
||||
else
|
||||
newErrors.username = ''
|
||||
|
||||
break
|
||||
|
||||
case 'email':
|
||||
newErrors.email = emailRegex.test(value)
|
||||
? ''
|
||||
: t('modals.signup.errors.invalid_email')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
setFormValid(validateForm(newErrors))
|
||||
}
|
||||
|
||||
function handlePasswordChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const { name, value } = event.target
|
||||
let newErrors = {...errors}
|
||||
|
||||
switch(name) {
|
||||
case 'password':
|
||||
newErrors.password = passwordInput.current?.value.includes(usernameInput.current?.value!)
|
||||
? t('modals.signup.errors.password_contains_username')
|
||||
: ''
|
||||
break
|
||||
|
||||
case 'password':
|
||||
newErrors.password = value.length < 8
|
||||
? t('modals.signup.errors.password_too_short')
|
||||
: ''
|
||||
break
|
||||
|
||||
case 'confirm_password':
|
||||
newErrors.passwordConfirmation = passwordInput.current?.value === passwordConfirmationInput.current?.value
|
||||
? ''
|
||||
: t('modals.signup.errors.passwords_dont_match')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
setFormValid(validateForm(newErrors))
|
||||
}
|
||||
|
||||
function validateForm(errors: ErrorMap) {
|
||||
let valid = true
|
||||
|
||||
Object.values(form).forEach(
|
||||
(input) => input.current?.value.length == 0 && (valid = false)
|
||||
)
|
||||
|
||||
Object.values(errors).forEach(
|
||||
(error) => error.length > 0 && (valid = false)
|
||||
)
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
setErrors({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: ''
|
||||
if (formValid)
|
||||
api.endpoints.users
|
||||
.create(body)
|
||||
.then((response) => {
|
||||
storeCookieInfo(response)
|
||||
return response.data.user.user_id
|
||||
})
|
||||
.then((id) => fetchUserInfo(id))
|
||||
.then((infoResponse) => storeUserInfo(infoResponse))
|
||||
}
|
||||
|
||||
function storeCookieInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
const cookieObj: AccountCookie = {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
token: user.token,
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.signup')}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Signup Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">{t('modals.signup.title')}</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
setCookie("account", cookieObj, { path: "/" })
|
||||
}
|
||||
|
||||
<form className="form" onSubmit={register}>
|
||||
<Fieldset
|
||||
fieldName="username"
|
||||
placeholder={t('modals.signup.placeholders.username')}
|
||||
onChange={handleNameChange}
|
||||
error={errors.username}
|
||||
ref={usernameInput}
|
||||
/>
|
||||
function fetchUserInfo(id: string) {
|
||||
return api.userInfo(id)
|
||||
}
|
||||
|
||||
<Fieldset
|
||||
fieldName="email"
|
||||
placeholder={t('modals.signup.placeholders.email')}
|
||||
onChange={handleNameChange}
|
||||
error={errors.email}
|
||||
ref={emailInput}
|
||||
/>
|
||||
function storeUserInfo(response: AxiosResponse) {
|
||||
const user = response.data.user
|
||||
|
||||
<Fieldset
|
||||
fieldName="password"
|
||||
placeholder={t('modals.signup.placeholders.password')}
|
||||
onChange={handlePasswordChange}
|
||||
error={errors.password}
|
||||
ref={passwordInput}
|
||||
/>
|
||||
const cookieObj: UserCookie = {
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
language: user.language,
|
||||
gender: user.gender,
|
||||
}
|
||||
|
||||
<Fieldset
|
||||
fieldName="confirm_password"
|
||||
placeholder={t('modals.signup.placeholders.password_confirm')}
|
||||
onChange={handlePasswordChange}
|
||||
error={errors.passwordConfirmation}
|
||||
ref={passwordConfirmationInput}
|
||||
/>
|
||||
// TODO: Set language
|
||||
setCookie("user", cookieObj, { path: "/" })
|
||||
|
||||
<Button>{t('modals.signup.buttons.confirm')}</Button>
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.picture.picture,
|
||||
element: user.picture.element,
|
||||
gender: user.gender,
|
||||
}
|
||||
|
||||
<Dialog.Description className="terms">
|
||||
{/* <Trans i18nKey="modals.signup.agreement">
|
||||
accountState.account.authorized = true
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
function handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const fieldName = event.target.name
|
||||
const value = event.target.value
|
||||
|
||||
if (value.length >= 3) {
|
||||
api.check(fieldName, value).then(
|
||||
(response) => {
|
||||
processNameCheck(fieldName, value, response.data.available)
|
||||
},
|
||||
(error) => {
|
||||
console.error(error)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
validateName(fieldName, value)
|
||||
}
|
||||
}
|
||||
|
||||
function processNameCheck(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
available: boolean
|
||||
) {
|
||||
const newErrors = { ...errors }
|
||||
|
||||
if (available) {
|
||||
// Continue checking for errors
|
||||
newErrors[fieldName] = ""
|
||||
setErrors(newErrors)
|
||||
setFormValid(true)
|
||||
|
||||
validateName(fieldName, value)
|
||||
} else {
|
||||
newErrors[fieldName] = t("modals.signup.errors.field_in_use", {
|
||||
field: fieldName,
|
||||
})
|
||||
setErrors(newErrors)
|
||||
setFormValid(false)
|
||||
}
|
||||
}
|
||||
|
||||
function validateName(fieldName: string, value: string) {
|
||||
let newErrors = { ...errors }
|
||||
|
||||
switch (fieldName) {
|
||||
case "username":
|
||||
if (value.length < 3)
|
||||
newErrors.username = t("modals.signup.errors.username_too_short")
|
||||
else if (value.length > 20)
|
||||
newErrors.username = t("modals.signup.errors.username_too_long")
|
||||
else newErrors.username = ""
|
||||
|
||||
break
|
||||
|
||||
case "email":
|
||||
newErrors.email = emailRegex.test(value)
|
||||
? ""
|
||||
: t("modals.signup.errors.invalid_email")
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
setFormValid(validateForm(newErrors))
|
||||
}
|
||||
|
||||
function handlePasswordChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const { name, value } = event.target
|
||||
let newErrors = { ...errors }
|
||||
|
||||
switch (name) {
|
||||
case "password":
|
||||
newErrors.password = passwordInput.current?.value.includes(
|
||||
usernameInput.current?.value!
|
||||
)
|
||||
? t("modals.signup.errors.password_contains_username")
|
||||
: ""
|
||||
break
|
||||
|
||||
case "password":
|
||||
newErrors.password =
|
||||
value.length < 8 ? t("modals.signup.errors.password_too_short") : ""
|
||||
break
|
||||
|
||||
case "confirm_password":
|
||||
newErrors.passwordConfirmation =
|
||||
passwordInput.current?.value ===
|
||||
passwordConfirmationInput.current?.value
|
||||
? ""
|
||||
: t("modals.signup.errors.passwords_dont_match")
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
setFormValid(validateForm(newErrors))
|
||||
}
|
||||
|
||||
function validateForm(errors: ErrorMap) {
|
||||
let valid = true
|
||||
|
||||
Object.values(form).forEach(
|
||||
(input) => input.current?.value.length == 0 && (valid = false)
|
||||
)
|
||||
|
||||
Object.values(errors).forEach(
|
||||
(error) => error.length > 0 && (valid = false)
|
||||
)
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
setErrors({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t("menu.signup")}</span>
|
||||
</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className="Signup Dialog"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">
|
||||
{t("modals.signup.title")}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<form className="form" onSubmit={register}>
|
||||
<Fieldset
|
||||
fieldName="username"
|
||||
placeholder={t("modals.signup.placeholders.username")}
|
||||
onChange={handleNameChange}
|
||||
error={errors.username}
|
||||
ref={usernameInput}
|
||||
/>
|
||||
|
||||
<Fieldset
|
||||
fieldName="email"
|
||||
placeholder={t("modals.signup.placeholders.email")}
|
||||
onChange={handleNameChange}
|
||||
error={errors.email}
|
||||
ref={emailInput}
|
||||
/>
|
||||
|
||||
<Fieldset
|
||||
fieldName="password"
|
||||
placeholder={t("modals.signup.placeholders.password")}
|
||||
onChange={handlePasswordChange}
|
||||
error={errors.password}
|
||||
ref={passwordInput}
|
||||
/>
|
||||
|
||||
<Fieldset
|
||||
fieldName="confirm_password"
|
||||
placeholder={t("modals.signup.placeholders.password_confirm")}
|
||||
onChange={handlePasswordChange}
|
||||
error={errors.passwordConfirmation}
|
||||
ref={passwordConfirmationInput}
|
||||
/>
|
||||
|
||||
<Button>{t("modals.signup.buttons.confirm")}</Button>
|
||||
|
||||
<Dialog.Description className="terms">
|
||||
{/* <Trans i18nKey="modals.signup.agreement">
|
||||
By signing up, I agree to the <Link href="/privacy"><span>Privacy Policy</span></Link><Link href="/usage"><span>Usage Guidelines</span></Link>.
|
||||
</Trans> */}
|
||||
</Dialog.Description>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
</Dialog.Description>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default SignupModal
|
||||
export default SignupModal
|
||||
|
|
|
|||
|
|
@ -1,316 +1,286 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { AxiosResponse } from "axios"
|
||||
import debounce from "lodash.debounce"
|
||||
|
||||
import SummonUnit from '~components/SummonUnit'
|
||||
import ExtraSummons from '~components/ExtraSummons'
|
||||
import SummonUnit from "~components/SummonUnit"
|
||||
import ExtraSummons from "~components/ExtraSummons"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
new: boolean
|
||||
slug?: string
|
||||
createParty: () => Promise<AxiosResponse<any, any>>
|
||||
pushHistory?: (path: string) => void
|
||||
new: boolean
|
||||
summons?: GridSummon[]
|
||||
createParty: () => Promise<AxiosResponse<any, any>>
|
||||
pushHistory?: (path: string) => void
|
||||
}
|
||||
|
||||
const SummonGrid = (props: Props) => {
|
||||
// Constants
|
||||
const numSummons: number = 4
|
||||
// Constants
|
||||
const numSummons: number = 4
|
||||
|
||||
const { t } = useTranslation('common')
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { headers: { Authorization: `Bearer ${accountData.token}` } }
|
||||
: {}
|
||||
|
||||
// Cookies
|
||||
const [cookies, _] = useCookies(['account'])
|
||||
const headers = (cookies.account != null) ? {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
}
|
||||
} : {}
|
||||
// Localization
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
// Set up state for view management
|
||||
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
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
||||
[key: number]: number
|
||||
}>({})
|
||||
|
||||
// Create a temporary state to store previous weapon uncap value
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
||||
// Set the editable flag only on first load
|
||||
useEffect(() => {
|
||||
// If user is logged in and matches
|
||||
if (
|
||||
(accountData && party.user && accountData.userId === party.user.id) ||
|
||||
props.new
|
||||
)
|
||||
appState.party.editable = true
|
||||
else appState.party.editable = false
|
||||
}, [props.new, accountData, party])
|
||||
|
||||
// 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])
|
||||
// Initialize an array of current uncap values for each summon
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: { [key: number]: number } = {}
|
||||
|
||||
// Set the editable flag only on first load
|
||||
useEffect(() => {
|
||||
if (!loading && !firstLoadComplete) {
|
||||
// If user is logged in and matches
|
||||
if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new)
|
||||
appState.party.editable = true
|
||||
else
|
||||
appState.party.editable = false
|
||||
if (appState.grid.summons.mainSummon)
|
||||
initialPreviousUncapValues[-1] =
|
||||
appState.grid.summons.mainSummon.uncap_level
|
||||
|
||||
setFirstLoadComplete(true)
|
||||
}
|
||||
}, [props.new, cookies, party, loading, firstLoadComplete])
|
||||
if (appState.grid.summons.friendSummon)
|
||||
initialPreviousUncapValues[6] =
|
||||
appState.grid.summons.friendSummon.uncap_level
|
||||
|
||||
// Initialize an array of current uncap values for each summon
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||
Object.values(appState.grid.summons.allSummons).map(
|
||||
(o) => (initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
)
|
||||
|
||||
if (appState.grid.summons.mainSummon)
|
||||
initialPreviousUncapValues[-1] = appState.grid.summons.mainSummon.uncap_level
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [
|
||||
appState.grid.summons.mainSummon,
|
||||
appState.grid.summons.friendSummon,
|
||||
appState.grid.summons.allSummons,
|
||||
])
|
||||
|
||||
if (appState.grid.summons.friendSummon)
|
||||
initialPreviousUncapValues[6] = appState.grid.summons.friendSummon.uncap_level
|
||||
// Methods: Adding an object from search
|
||||
function receiveSummonFromSearch(
|
||||
object: Character | Weapon | Summon,
|
||||
position: number
|
||||
) {
|
||||
const summon = object as Summon
|
||||
|
||||
Object.values(appState.grid.summons.allSummons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [appState.grid.summons.mainSummon, appState.grid.summons.friendSummon, appState.grid.summons.allSummons])
|
||||
|
||||
|
||||
// Methods: Fetching an object from the server
|
||||
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
|
||||
if (!party.id) {
|
||||
props.createParty().then((response) => {
|
||||
const party = response.data.party
|
||||
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)
|
||||
setSlug(party.shortcode)
|
||||
|
||||
// Populate the weapons in state
|
||||
populateSummons(party.summons)
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
|
||||
saveSummon(party.id, summon, position).then((response) =>
|
||||
storeGridSummon(response.data.grid_summon)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
if (party.editable)
|
||||
saveSummon(party.id, summon, position).then((response) =>
|
||||
storeGridSummon(response.data.grid_summon)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function processError(error: any) {
|
||||
if (error.response != null) {
|
||||
if (error.response.status == 404) {
|
||||
setFound(false)
|
||||
setLoading(false)
|
||||
}
|
||||
} else {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
async function saveSummon(partyId: string, summon: Summon, position: number) {
|
||||
let uncapLevel = 3
|
||||
if (summon.uncap.ulb) uncapLevel = 5
|
||||
else if (summon.uncap.flb) uncapLevel = 4
|
||||
|
||||
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
|
||||
return await api.endpoints.summons.create(
|
||||
{
|
||||
summon: {
|
||||
party_id: partyId,
|
||||
summon_id: summon.id,
|
||||
position: position,
|
||||
main: position == -1,
|
||||
friend: position == 6,
|
||||
uncap_level: uncapLevel,
|
||||
},
|
||||
},
|
||||
headers
|
||||
)
|
||||
}
|
||||
|
||||
function storeGridSummon(gridSummon: GridSummon) {
|
||||
if (gridSummon.position == -1) appState.grid.summons.mainSummon = gridSummon
|
||||
else if (gridSummon.position == 6)
|
||||
appState.grid.summons.friendSummon = gridSummon
|
||||
else appState.grid.summons.allSummons[gridSummon.position] = gridSummon
|
||||
}
|
||||
|
||||
// Methods: Updating uncap level
|
||||
// Note: Saves, but debouncing is not working properly
|
||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||
storePreviousUncapValue(position)
|
||||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap("summon", id, uncapLevel).then((response) => {
|
||||
storeGridSummon(response.data.grid_summon)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// Revert optimistic UI
|
||||
updateUncapLevel(position, previousUncapValues[position])
|
||||
|
||||
// Remove optimistic key
|
||||
let newPreviousValues = { ...previousUncapValues }
|
||||
delete newPreviousValues[position]
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||
const summon = object as Summon
|
||||
function initiateUncapUpdate(
|
||||
id: string,
|
||||
position: number,
|
||||
uncapLevel: number
|
||||
) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
if (!party.id) {
|
||||
props.createParty()
|
||||
.then(response => {
|
||||
const party = response.data.party
|
||||
appState.party.id = party.id
|
||||
setSlug(party.shortcode)
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
},
|
||||
[props, previousUncapValues]
|
||||
)
|
||||
|
||||
saveSummon(party.id, summon, position)
|
||||
.then(response => storeGridSummon(response.data.grid_summon))
|
||||
})
|
||||
} else {
|
||||
if (party.editable)
|
||||
saveSummon(party.id, summon, position)
|
||||
.then(response => storeGridSummon(response.data.grid_summon))
|
||||
}
|
||||
}
|
||||
const debouncedAction = useMemo(
|
||||
() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500),
|
||||
[props, saveUncap]
|
||||
)
|
||||
|
||||
async function saveSummon(partyId: string, summon: Summon, position: number) {
|
||||
let uncapLevel = 3
|
||||
if (summon.uncap.ulb) uncapLevel = 5
|
||||
else if (summon.uncap.flb) uncapLevel = 4
|
||||
|
||||
return await api.endpoints.summons.create({
|
||||
'summon': {
|
||||
'party_id': partyId,
|
||||
'summon_id': summon.id,
|
||||
'position': position,
|
||||
'main': (position == -1),
|
||||
'friend': (position == 6),
|
||||
'uncap_level': uncapLevel
|
||||
}
|
||||
}, headers)
|
||||
}
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
if (appState.grid.summons.mainSummon && position == -1)
|
||||
appState.grid.summons.mainSummon.uncap_level = uncapLevel
|
||||
else if (appState.grid.summons.friendSummon && position == 6)
|
||||
appState.grid.summons.friendSummon.uncap_level = uncapLevel
|
||||
else appState.grid.summons.allSummons[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storeGridSummon(gridSummon: GridSummon) {
|
||||
if (gridSummon.position == -1)
|
||||
appState.grid.summons.mainSummon = gridSummon
|
||||
else if (gridSummon.position == 6)
|
||||
appState.grid.summons.friendSummon = gridSummon
|
||||
else
|
||||
appState.grid.summons.allSummons[gridSummon.position] = gridSummon
|
||||
}
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = { ...previousUncapValues }
|
||||
|
||||
// Methods: Updating uncap level
|
||||
// Note: Saves, but debouncing is not working properly
|
||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||
storePreviousUncapValue(position)
|
||||
if (appState.grid.summons.mainSummon && position == -1)
|
||||
newPreviousValues[position] = appState.grid.summons.mainSummon.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
|
||||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap('summon', id, uncapLevel)
|
||||
.then(response => { storeGridSummon(response.data.grid_summon) })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
|
||||
// Revert optimistic UI
|
||||
updateUncapLevel(position, previousUncapValues[position])
|
||||
// Render: JSX components
|
||||
const mainSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t("summons.main")}</div>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.mainSummon}
|
||||
editable={party.editable}
|
||||
key="grid_main_summon"
|
||||
position={-1}
|
||||
unitType={0}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Remove optimistic key
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
delete newPreviousValues[position]
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
}, [props, previousUncapValues]
|
||||
)
|
||||
|
||||
const debouncedAction = useMemo(() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500), [props, saveUncap]
|
||||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
if (appState.grid.summons.mainSummon && position == -1)
|
||||
appState.grid.summons.mainSummon.uncap_level = uncapLevel
|
||||
else if (appState.grid.summons.friendSummon && position == 6)
|
||||
appState.grid.summons.friendSummon.uncap_level = uncapLevel
|
||||
else
|
||||
appState.grid.summons.allSummons[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
|
||||
if (appState.grid.summons.mainSummon && position == -1) newPreviousValues[position] = appState.grid.summons.mainSummon.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)
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
const mainSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t('summons.main')}</div>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.mainSummon}
|
||||
const friendSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t("summons.friend")}</div>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.friendSummon}
|
||||
editable={party.editable}
|
||||
key="grid_friend_summon"
|
||||
position={6}
|
||||
unitType={2}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
const summonGridElement = (
|
||||
<div id="LabeledGrid">
|
||||
<div className="Label">{t("summons.summons")}</div>
|
||||
<ul id="grid_summons">
|
||||
{Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`}>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.allSummons[i]}
|
||||
editable={party.editable}
|
||||
key="grid_main_summon"
|
||||
position={-1}
|
||||
unitType={0}
|
||||
position={i}
|
||||
unitType={1}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
const subAuraSummonElement = (
|
||||
<ExtraSummons
|
||||
grid={grid.summons.allSummons}
|
||||
editable={party.editable}
|
||||
exists={false}
|
||||
offset={numSummons}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<div id="SummonGrid">
|
||||
{mainSummonElement}
|
||||
{friendSummonElement}
|
||||
{summonGridElement}
|
||||
</div>
|
||||
|
||||
const friendSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t('summons.friend')}</div>
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.friendSummon}
|
||||
editable={party.editable}
|
||||
key="grid_friend_summon"
|
||||
position={6}
|
||||
unitType={2}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
const summonGridElement = (
|
||||
<div id="LabeledGrid">
|
||||
<div className="Label">{t('summons.summons')}</div>
|
||||
<ul id="grid_summons">
|
||||
{Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (<li key={`grid_unit_${i}`} >
|
||||
<SummonUnit
|
||||
gridSummon={grid.summons.allSummons[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
unitType={1}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
const subAuraSummonElement = (
|
||||
<ExtraSummons
|
||||
grid={grid.summons.allSummons}
|
||||
editable={party.editable}
|
||||
exists={false}
|
||||
offset={numSummons}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<div id="SummonGrid">
|
||||
{ mainSummonElement }
|
||||
{ friendSummonElement }
|
||||
{ summonGridElement }
|
||||
</div>
|
||||
|
||||
{ subAuraSummonElement }
|
||||
</div>
|
||||
)
|
||||
{subAuraSummonElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummonGrid
|
||||
|
|
|
|||
|
|
@ -1,147 +1,154 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React from "react"
|
||||
import { useSnapshot } from "valtio"
|
||||
import { getCookie, deleteCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import clonedeep from "lodash.clonedeep"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { accountState, initialAccountState } from '~utils/accountState'
|
||||
import { appState, initialAppState } from '~utils/appState'
|
||||
import api from "~utils/api"
|
||||
import { accountState, initialAccountState } from "~utils/accountState"
|
||||
import { appState, initialAppState } from "~utils/appState"
|
||||
|
||||
import Header from '~components/Header'
|
||||
import Button from '~components/Button'
|
||||
import HeaderMenu from '~components/HeaderMenu'
|
||||
import Header from "~components/Header"
|
||||
import Button from "~components/Button"
|
||||
import HeaderMenu from "~components/HeaderMenu"
|
||||
|
||||
const TopHeader = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Cookies
|
||||
const [accountCookies, setAccountCookie, removeAccountCookie] = useCookies(['account'])
|
||||
const [userCookies, setUserCookies, removeUserCookie] = useCookies(['user'])
|
||||
|
||||
const headers = (accountCookies.account != null) ? {
|
||||
'Authorization': `Bearer ${accountCookies.account.access_token}`
|
||||
} : {}
|
||||
// Cookies
|
||||
const accountCookie = getCookie("account")
|
||||
const userCookie = getCookie("user")
|
||||
|
||||
const { account } = useSnapshot(accountState)
|
||||
const { party } = useSnapshot(appState)
|
||||
const router = useRouter()
|
||||
const headers = {}
|
||||
// accountCookies.account != null
|
||||
// ? {
|
||||
// Authorization: `Bearer ${accountCookies.account.access_token}`,
|
||||
// }
|
||||
// : {}
|
||||
|
||||
function copyToClipboard() {
|
||||
const el = document.createElement('input')
|
||||
el.value = window.location.href
|
||||
el.id = 'url-input'
|
||||
document.body.appendChild(el)
|
||||
const { account } = useSnapshot(accountState)
|
||||
const { party } = useSnapshot(appState)
|
||||
const router = useRouter()
|
||||
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
el.remove()
|
||||
}
|
||||
function copyToClipboard() {
|
||||
const el = document.createElement("input")
|
||||
el.value = window.location.href
|
||||
el.id = "url-input"
|
||||
document.body.appendChild(el)
|
||||
|
||||
function newParty() {
|
||||
// Push the root URL
|
||||
router.push('/')
|
||||
el.select()
|
||||
document.execCommand("copy")
|
||||
el.remove()
|
||||
}
|
||||
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAppState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
appState[key] = resetState[key]
|
||||
})
|
||||
function newParty() {
|
||||
// Push the root URL
|
||||
router.push("/")
|
||||
|
||||
// Set party to be editable
|
||||
appState.party.editable = true
|
||||
}
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAppState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
appState[key] = resetState[key]
|
||||
})
|
||||
|
||||
function logout() {
|
||||
removeAccountCookie('account')
|
||||
removeUserCookie('user')
|
||||
// Set party to be editable
|
||||
appState.party.editable = true
|
||||
}
|
||||
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAccountState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
if (key !== 'language')
|
||||
accountState[key] = resetState[key]
|
||||
})
|
||||
|
||||
if (router.route != '/new')
|
||||
appState.party.editable = false
|
||||
function logout() {
|
||||
deleteCookie("account")
|
||||
deleteCookie("user")
|
||||
|
||||
router.push('/')
|
||||
return false
|
||||
}
|
||||
// Clean state
|
||||
const resetState = clonedeep(initialAccountState)
|
||||
Object.keys(resetState).forEach((key) => {
|
||||
if (key !== "language") accountState[key] = resetState[key]
|
||||
})
|
||||
|
||||
function toggleFavorite() {
|
||||
if (party.favorited)
|
||||
unsaveFavorite()
|
||||
else
|
||||
saveFavorite()
|
||||
}
|
||||
if (router.route != "/new") appState.party.editable = false
|
||||
|
||||
function saveFavorite() {
|
||||
if (party.id)
|
||||
api.saveTeam({ id: party.id, params: headers })
|
||||
.then((response) => {
|
||||
if (response.status == 201)
|
||||
appState.party.favorited = true
|
||||
})
|
||||
else
|
||||
console.error("Failed to save team: No party ID")
|
||||
}
|
||||
router.push("/")
|
||||
return false
|
||||
}
|
||||
|
||||
function unsaveFavorite() {
|
||||
if (party.id)
|
||||
api.unsaveTeam({ id: party.id, params: headers })
|
||||
.then((response) => {
|
||||
if (response.status == 200)
|
||||
appState.party.favorited = false
|
||||
})
|
||||
else
|
||||
console.error("Failed to unsave team: No party ID")
|
||||
}
|
||||
function toggleFavorite() {
|
||||
if (party.favorited) unsaveFavorite()
|
||||
else saveFavorite()
|
||||
}
|
||||
|
||||
const leftNav = () => {
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<Button icon="menu">{t('buttons.menu')}</Button>
|
||||
{ (account.user) ?
|
||||
<HeaderMenu authenticated={account.authorized} username={account.user.username} logout={logout} /> :
|
||||
<HeaderMenu authenticated={account.authorized} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
function saveFavorite() {
|
||||
if (party.id)
|
||||
api.saveTeam({ id: party.id, params: headers }).then((response) => {
|
||||
if (response.status == 201) appState.party.favorited = true
|
||||
})
|
||||
else console.error("Failed to save team: No party ID")
|
||||
}
|
||||
|
||||
const saveButton = () => {
|
||||
if (party.favorited)
|
||||
return (<Button icon="save" active={true} onClick={toggleFavorite}>Saved</Button>)
|
||||
else
|
||||
return (<Button icon="save" onClick={toggleFavorite}>Save</Button>)
|
||||
}
|
||||
function unsaveFavorite() {
|
||||
if (party.id)
|
||||
api.unsaveTeam({ id: party.id, params: headers }).then((response) => {
|
||||
if (response.status == 200) appState.party.favorited = false
|
||||
})
|
||||
else console.error("Failed to unsave team: No party ID")
|
||||
}
|
||||
|
||||
const rightNav = () => {
|
||||
return (
|
||||
<div>
|
||||
{ (router.route === '/p/[party]' && account.user && (!party.user || party.user.id !== account.user.id)) ?
|
||||
saveButton() : ''
|
||||
}
|
||||
{ (router.route === '/p/[party]') ?
|
||||
<Button icon="link" onClick={copyToClipboard}>{t('buttons.copy')}</Button> : ''
|
||||
}
|
||||
<Button icon="new" onClick={newParty}>{t('buttons.new')}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const leftNav = () => {
|
||||
return (
|
||||
<Header
|
||||
position="top"
|
||||
left={ leftNav() }
|
||||
right={ rightNav() }
|
||||
/>
|
||||
<div className="dropdown">
|
||||
<Button icon="menu">{t("buttons.menu")}</Button>
|
||||
{account.user ? (
|
||||
<HeaderMenu
|
||||
authenticated={account.authorized}
|
||||
username={account.user.username}
|
||||
logout={logout}
|
||||
/>
|
||||
) : (
|
||||
<HeaderMenu authenticated={account.authorized} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const saveButton = () => {
|
||||
if (party.favorited)
|
||||
return (
|
||||
<Button icon="save" active={true} onClick={toggleFavorite}>
|
||||
Saved
|
||||
</Button>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<Button icon="save" onClick={toggleFavorite}>
|
||||
Save
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const rightNav = () => {
|
||||
return (
|
||||
<div>
|
||||
{router.route === "/p/[party]" &&
|
||||
account.user &&
|
||||
(!party.user || party.user.id !== account.user.id)
|
||||
? saveButton()
|
||||
: ""}
|
||||
{router.route === "/p/[party]" ? (
|
||||
<Button icon="link" onClick={copyToClipboard}>
|
||||
{t("buttons.copy")}
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Button icon="new" onClick={newParty}>
|
||||
{t("buttons.new")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Header position="top" left={leftNav()} right={rightNav()} />
|
||||
}
|
||||
|
||||
export default TopHeader
|
||||
export default TopHeader
|
||||
|
|
|
|||
|
|
@ -1,286 +1,246 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useSnapshot } from "valtio"
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { AxiosResponse } from "axios"
|
||||
import debounce from "lodash.debounce"
|
||||
|
||||
import WeaponUnit from '~components/WeaponUnit'
|
||||
import ExtraWeapons from '~components/ExtraWeapons'
|
||||
import WeaponUnit from "~components/WeaponUnit"
|
||||
import ExtraWeapons from "~components/ExtraWeapons"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import './index.scss'
|
||||
import "./index.scss"
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
new: boolean
|
||||
slug?: string
|
||||
createParty: (extra: boolean) => Promise<AxiosResponse<any, any>>
|
||||
pushHistory?: (path: string) => void
|
||||
new: boolean
|
||||
weapons?: GridWeapon[]
|
||||
createParty: (extra: boolean) => Promise<AxiosResponse<any, any>>
|
||||
pushHistory?: (path: string) => void
|
||||
}
|
||||
|
||||
const WeaponGrid = (props: Props) => {
|
||||
// Constants
|
||||
const numWeapons: number = 9
|
||||
// Constants
|
||||
const numWeapons: number = 9
|
||||
|
||||
// Cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = (cookies.account != null) ? {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
}
|
||||
} : {}
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { headers: { Authorization: `Bearer ${accountData.token}` } }
|
||||
: {}
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
// Set up state for view management
|
||||
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
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
||||
[key: number]: number
|
||||
}>({})
|
||||
|
||||
// Create a temporary state to store previous weapon uncap values
|
||||
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
||||
// Set the editable flag only on first load
|
||||
useEffect(() => {
|
||||
// If user is logged in and matches
|
||||
if (
|
||||
(accountData && party.user && accountData.userId === party.user.id) ||
|
||||
props.new
|
||||
)
|
||||
appState.party.editable = true
|
||||
else appState.party.editable = false
|
||||
}, [props.new, accountData, party])
|
||||
|
||||
// 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])
|
||||
// Initialize an array of current uncap values for each weapon
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: { [key: number]: number } = {}
|
||||
|
||||
// Set the editable flag only on first load
|
||||
useEffect(() => {
|
||||
if (!loading && !firstLoadComplete) {
|
||||
// If user is logged in and matches
|
||||
if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new)
|
||||
appState.party.editable = true
|
||||
else
|
||||
appState.party.editable = false
|
||||
if (appState.grid.weapons.mainWeapon)
|
||||
initialPreviousUncapValues[-1] =
|
||||
appState.grid.weapons.mainWeapon.uncap_level
|
||||
|
||||
setFirstLoadComplete(true)
|
||||
}
|
||||
}, [props.new, cookies, party, loading, firstLoadComplete])
|
||||
Object.values(appState.grid.weapons.allWeapons).map(
|
||||
(o) => (initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
)
|
||||
|
||||
// Initialize an array of current uncap values for each weapon
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons])
|
||||
|
||||
if (appState.grid.weapons.mainWeapon)
|
||||
initialPreviousUncapValues[-1] = appState.grid.weapons.mainWeapon.uncap_level
|
||||
// Methods: Adding an object from search
|
||||
function receiveWeaponFromSearch(
|
||||
object: Character | Weapon | Summon,
|
||||
position: number
|
||||
) {
|
||||
const weapon = object as Weapon
|
||||
if (position == 1) appState.party.element = weapon.element
|
||||
|
||||
Object.values(appState.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [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
|
||||
if (!party.id) {
|
||||
props.createParty(party.extra).then((response) => {
|
||||
const party = response.data.party
|
||||
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
|
||||
setSlug(party.shortcode)
|
||||
|
||||
setFound(true)
|
||||
setLoading(false)
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
|
||||
// Populate the weapons in state
|
||||
populateWeapons(party.weapons)
|
||||
saveWeapon(party.id, weapon, position).then((response) =>
|
||||
storeGridWeapon(response.data.grid_weapon)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
saveWeapon(party.id, weapon, position).then((response) =>
|
||||
storeGridWeapon(response.data.grid_weapon)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function processError(error: any) {
|
||||
if (error.response != null) {
|
||||
if (error.response.status == 404) {
|
||||
setFound(false)
|
||||
setLoading(false)
|
||||
}
|
||||
} else {
|
||||
console.error(error)
|
||||
}
|
||||
async function saveWeapon(partyId: string, weapon: Weapon, position: number) {
|
||||
let uncapLevel = 3
|
||||
if (weapon.uncap.ulb) uncapLevel = 5
|
||||
else if (weapon.uncap.flb) uncapLevel = 4
|
||||
|
||||
return await api.endpoints.weapons.create(
|
||||
{
|
||||
weapon: {
|
||||
party_id: partyId,
|
||||
weapon_id: weapon.id,
|
||||
position: position,
|
||||
mainhand: position == -1,
|
||||
uncap_level: uncapLevel,
|
||||
},
|
||||
},
|
||||
headers
|
||||
)
|
||||
}
|
||||
|
||||
function storeGridWeapon(gridWeapon: GridWeapon) {
|
||||
if (gridWeapon.position == -1) {
|
||||
appState.grid.weapons.mainWeapon = gridWeapon
|
||||
appState.party.element = gridWeapon.object.element
|
||||
} else {
|
||||
// Store the grid unit at the correct position
|
||||
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||
}
|
||||
}
|
||||
|
||||
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: Updating uncap level
|
||||
// Note: Saves, but debouncing is not working properly
|
||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||
storePreviousUncapValue(position)
|
||||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap("weapon", id, uncapLevel).then((response) => {
|
||||
storeGridWeapon(response.data.grid_weapon)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// Revert optimistic UI
|
||||
updateUncapLevel(position, previousUncapValues[position])
|
||||
|
||||
// Remove optimistic key
|
||||
let newPreviousValues = { ...previousUncapValues }
|
||||
delete newPreviousValues[position]
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||
const weapon = object as Weapon
|
||||
if (position == 1)
|
||||
appState.party.element = weapon.element
|
||||
}
|
||||
|
||||
if (!party.id) {
|
||||
props.createParty(party.extra)
|
||||
.then(response => {
|
||||
const party = response.data.party
|
||||
appState.party.id = party.id
|
||||
setSlug(party.shortcode)
|
||||
function initiateUncapUpdate(
|
||||
id: string,
|
||||
position: number,
|
||||
uncapLevel: number
|
||||
) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
saveWeapon(party.id, weapon, position)
|
||||
.then(response => storeGridWeapon(response.data.grid_weapon))
|
||||
})
|
||||
} else {
|
||||
saveWeapon(party.id, weapon, position)
|
||||
.then(response => storeGridWeapon(response.data.grid_weapon))
|
||||
}
|
||||
}
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
},
|
||||
[props, previousUncapValues]
|
||||
)
|
||||
|
||||
async function saveWeapon(partyId: string, weapon: Weapon, position: number) {
|
||||
let uncapLevel = 3
|
||||
if (weapon.uncap.ulb) uncapLevel = 5
|
||||
else if (weapon.uncap.flb) uncapLevel = 4
|
||||
|
||||
return await api.endpoints.weapons.create({
|
||||
'weapon': {
|
||||
'party_id': partyId,
|
||||
'weapon_id': weapon.id,
|
||||
'position': position,
|
||||
'mainhand': (position == -1),
|
||||
'uncap_level': uncapLevel
|
||||
}
|
||||
}, headers)
|
||||
}
|
||||
const debouncedAction = useMemo(
|
||||
() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500),
|
||||
[props, saveUncap]
|
||||
)
|
||||
|
||||
function storeGridWeapon(gridWeapon: GridWeapon) {
|
||||
if (gridWeapon.position == -1) {
|
||||
appState.grid.weapons.mainWeapon = gridWeapon
|
||||
appState.party.element = gridWeapon.object.element
|
||||
} else {
|
||||
// Store the grid unit at the correct position
|
||||
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||
}
|
||||
}
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
if (appState.grid.weapons.mainWeapon && position == -1)
|
||||
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel
|
||||
else appState.grid.weapons.allWeapons[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
// Methods: Updating uncap level
|
||||
// Note: Saves, but debouncing is not working properly
|
||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||
storePreviousUncapValue(position)
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = { ...previousUncapValues }
|
||||
newPreviousValues[position] =
|
||||
appState.grid.weapons.mainWeapon && position == -1
|
||||
? appState.grid.weapons.mainWeapon.uncap_level
|
||||
: appState.grid.weapons.allWeapons[position].uncap_level
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
|
||||
try {
|
||||
if (uncapLevel != previousUncapValues[position])
|
||||
await api.updateUncap('weapon', id, uncapLevel)
|
||||
.then(response => { storeGridWeapon(response.data.grid_weapon) })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// Render: JSX components
|
||||
const mainhandElement = (
|
||||
<WeaponUnit
|
||||
gridWeapon={appState.grid.weapons.mainWeapon}
|
||||
editable={party.editable}
|
||||
key="grid_mainhand"
|
||||
position={-1}
|
||||
unitType={0}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
||||
// Revert optimistic UI
|
||||
updateUncapLevel(position, previousUncapValues[position])
|
||||
|
||||
// Remove optimistic key
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
delete newPreviousValues[position]
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
}, [props, previousUncapValues]
|
||||
)
|
||||
|
||||
const debouncedAction = useMemo(() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500), [props, saveUncap]
|
||||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
if (appState.grid.weapons.mainWeapon && position == -1)
|
||||
appState.grid.weapons.mainWeapon.uncap_level = uncapLevel
|
||||
else
|
||||
appState.grid.weapons.allWeapons[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
newPreviousValues[position] = (appState.grid.weapons.mainWeapon && position == -1) ?
|
||||
appState.grid.weapons.mainWeapon.uncap_level : appState.grid.weapons.allWeapons[position].uncap_level
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
const mainhandElement = (
|
||||
<WeaponUnit
|
||||
gridWeapon={appState.grid.weapons.mainWeapon}
|
||||
editable={party.editable}
|
||||
key="grid_mainhand"
|
||||
position={-1}
|
||||
unitType={0}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
||||
const weaponGridElement = (
|
||||
Array.from(Array(numWeapons)).map((x, i) => {
|
||||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<WeaponUnit
|
||||
gridWeapon={appState.grid.weapons.allWeapons[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
unitType={1}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const extraGridElement = (
|
||||
<ExtraWeapons
|
||||
grid={appState.grid.weapons.allWeapons}
|
||||
editable={party.editable}
|
||||
offset={numWeapons}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
||||
const weaponGridElement = Array.from(Array(numWeapons)).map((x, i) => {
|
||||
return (
|
||||
<div id="WeaponGrid">
|
||||
<div id="MainGrid">
|
||||
{ mainhandElement }
|
||||
<ul className="grid_weapons">{ weaponGridElement }</ul>
|
||||
</div>
|
||||
|
||||
{ (() => { return (party.extra) ? extraGridElement : '' })() }
|
||||
</div>
|
||||
<li key={`grid_unit_${i}`}>
|
||||
<WeaponUnit
|
||||
gridWeapon={appState.grid.weapons.allWeapons[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
unitType={1}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
|
||||
const extraGridElement = (
|
||||
<ExtraWeapons
|
||||
grid={appState.grid.weapons.allWeapons}
|
||||
editable={party.editable}
|
||||
offset={numWeapons}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div id="WeaponGrid">
|
||||
<div id="MainGrid">
|
||||
{mainhandElement}
|
||||
<ul className="grid_weapons">{weaponGridElement}</ul>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
return party.extra ? extraGridElement : ""
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WeaponGrid
|
||||
|
|
|
|||
|
|
@ -1,223 +1,261 @@
|
|||
import React, { useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import React, { useState } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
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 ElementToggle from '~components/ElementToggle'
|
||||
import WeaponKeyDropdown from '~components/WeaponKeyDropdown'
|
||||
import Button from '~components/Button'
|
||||
import AXSelect from "~components/AxSelect"
|
||||
import ElementToggle from "~components/ElementToggle"
|
||||
import WeaponKeyDropdown from "~components/WeaponKeyDropdown"
|
||||
import Button from "~components/Button"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import api from "~utils/api"
|
||||
import { appState } from "~utils/appState"
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
import CrossIcon from "~public/icons/Cross.svg"
|
||||
import "./index.scss"
|
||||
|
||||
interface GridWeaponObject {
|
||||
weapon: {
|
||||
element?: number
|
||||
weapon_key1_id?: string
|
||||
weapon_key2_id?: string
|
||||
weapon_key3_id?: string
|
||||
ax_modifier1?: number
|
||||
ax_modifier2?: number
|
||||
ax_strength1?: number
|
||||
ax_strength2?: number
|
||||
}
|
||||
weapon: {
|
||||
element?: number
|
||||
weapon_key1_id?: string
|
||||
weapon_key2_id?: string
|
||||
weapon_key3_id?: string
|
||||
ax_modifier1?: number
|
||||
ax_modifier2?: number
|
||||
ax_strength1?: number
|
||||
ax_strength2?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
gridWeapon: GridWeapon
|
||||
children: React.ReactNode
|
||||
gridWeapon: GridWeapon
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const WeaponModal = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const locale = (router.locale && ['en', 'ja'].includes(router.locale)) ? router.locale : 'en'
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = (cookies.account != null) ? {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
}
|
||||
} : {}
|
||||
|
||||
// Refs
|
||||
const weaponKey1Select = React.createRef<HTMLSelectElement>()
|
||||
const weaponKey2Select = React.createRef<HTMLSelectElement>()
|
||||
const weaponKey3Select = React.createRef<HTMLSelectElement>()
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ["en", "ja"].includes(router.locale) ? router.locale : "en"
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
// Cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { Authorization: `Bearer ${accountData.token}` }
|
||||
: {}
|
||||
|
||||
const [element, setElement] = useState(-1)
|
||||
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
|
||||
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
|
||||
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
|
||||
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
|
||||
|
||||
function receiveAxValues(primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) {
|
||||
setPrimaryAxModifier(primaryAxModifier)
|
||||
setSecondaryAxModifier(secondaryAxModifier)
|
||||
// Refs
|
||||
const weaponKey1Select = React.createRef<HTMLSelectElement>()
|
||||
const weaponKey2Select = React.createRef<HTMLSelectElement>()
|
||||
const weaponKey3Select = React.createRef<HTMLSelectElement>()
|
||||
|
||||
setPrimaryAxValue(primaryAxValue)
|
||||
setSecondaryAxValue(secondaryAxValue)
|
||||
// State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
|
||||
const [element, setElement] = useState(-1)
|
||||
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
|
||||
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
|
||||
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
|
||||
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
|
||||
|
||||
function receiveAxValues(
|
||||
primaryAxModifier: number,
|
||||
primaryAxValue: number,
|
||||
secondaryAxModifier: number,
|
||||
secondaryAxValue: number
|
||||
) {
|
||||
setPrimaryAxModifier(primaryAxModifier)
|
||||
setSecondaryAxModifier(secondaryAxModifier)
|
||||
|
||||
setPrimaryAxValue(primaryAxValue)
|
||||
setSecondaryAxValue(secondaryAxValue)
|
||||
}
|
||||
|
||||
function receiveAxValidity(isValid: boolean) {
|
||||
setFormValid(isValid)
|
||||
}
|
||||
|
||||
function receiveElementValue(element: string) {
|
||||
setElement(parseInt(element))
|
||||
}
|
||||
|
||||
function prepareObject() {
|
||||
let object: GridWeaponObject = { weapon: {} }
|
||||
|
||||
if (props.gridWeapon.object.element == 0) object.weapon.element = element
|
||||
|
||||
if ([2, 3, 17, 24].includes(props.gridWeapon.object.series))
|
||||
object.weapon.weapon_key1_id = weaponKey1Select.current?.value
|
||||
|
||||
if ([2, 3, 17].includes(props.gridWeapon.object.series))
|
||||
object.weapon.weapon_key2_id = weaponKey2Select.current?.value
|
||||
|
||||
if (props.gridWeapon.object.series == 17)
|
||||
object.weapon.weapon_key3_id = weaponKey3Select.current?.value
|
||||
|
||||
if (props.gridWeapon.object.ax > 0) {
|
||||
object.weapon.ax_modifier1 = primaryAxModifier
|
||||
object.weapon.ax_modifier2 = secondaryAxModifier
|
||||
object.weapon.ax_strength1 = primaryAxValue
|
||||
object.weapon.ax_strength2 = secondaryAxValue
|
||||
}
|
||||
|
||||
function receiveAxValidity(isValid: boolean) {
|
||||
setFormValid(isValid)
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
function receiveElementValue(element: string) {
|
||||
setElement(parseInt(element))
|
||||
}
|
||||
async function updateWeapon() {
|
||||
const updateObject = prepareObject()
|
||||
return await api.endpoints.grid_weapons
|
||||
.update(props.gridWeapon.id, updateObject, headers)
|
||||
.then((response) => processResult(response))
|
||||
.catch((error) => processError(error))
|
||||
}
|
||||
|
||||
function prepareObject() {
|
||||
let object: GridWeaponObject = { weapon: {} }
|
||||
function processResult(response: AxiosResponse) {
|
||||
const gridWeapon: GridWeapon = response.data.grid_weapon
|
||||
|
||||
if (props.gridWeapon.object.element == 0)
|
||||
object.weapon.element = element
|
||||
if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon
|
||||
else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||
|
||||
if ([2, 3, 17, 24].includes(props.gridWeapon.object.series))
|
||||
object.weapon.weapon_key1_id = weaponKey1Select.current?.value
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
if ([2, 3, 17].includes(props.gridWeapon.object.series))
|
||||
object.weapon.weapon_key2_id = weaponKey2Select.current?.value
|
||||
function processError(error: any) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (props.gridWeapon.object.series == 17)
|
||||
object.weapon.weapon_key3_id = weaponKey3Select.current?.value
|
||||
|
||||
if (props.gridWeapon.object.ax > 0) {
|
||||
object.weapon.ax_modifier1 = primaryAxModifier
|
||||
object.weapon.ax_modifier2 = secondaryAxModifier
|
||||
object.weapon.ax_strength1 = primaryAxValue
|
||||
object.weapon.ax_strength2 = secondaryAxValue
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
async function updateWeapon() {
|
||||
const updateObject = prepareObject()
|
||||
return await api.endpoints.grid_weapons.update(props.gridWeapon.id, updateObject, headers)
|
||||
.then(response => processResult(response))
|
||||
.catch(error => processError(error))
|
||||
}
|
||||
|
||||
function processResult(response: AxiosResponse) {
|
||||
const gridWeapon: GridWeapon = response.data.grid_weapon
|
||||
|
||||
if (gridWeapon.mainhand)
|
||||
appState.grid.weapons.mainWeapon = gridWeapon
|
||||
else
|
||||
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
function processError(error: any) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
const elementSelect = () => {
|
||||
return (
|
||||
<section>
|
||||
<h3>{t('modals.weapon.subtitles.element')}</h3>
|
||||
<ElementToggle
|
||||
currentElement={props.gridWeapon.element}
|
||||
sendValue={receiveElementValue}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const keySelect = () => {
|
||||
return (
|
||||
<section>
|
||||
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
|
||||
{ ([2, 3, 17, 22].includes(props.gridWeapon.object.series)) ?
|
||||
<WeaponKeyDropdown
|
||||
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[0] : undefined }
|
||||
series={props.gridWeapon.object.series}
|
||||
slot={0}
|
||||
ref={weaponKey1Select} />
|
||||
: ''}
|
||||
|
||||
{ ([2, 3, 17].includes(props.gridWeapon.object.series)) ?
|
||||
<WeaponKeyDropdown
|
||||
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[1] : undefined }
|
||||
series={props.gridWeapon.object.series}
|
||||
slot={1}
|
||||
ref={weaponKey2Select} />
|
||||
: ''}
|
||||
|
||||
{ (props.gridWeapon.object.series == 17) ?
|
||||
<WeaponKeyDropdown
|
||||
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[2] : undefined }
|
||||
series={props.gridWeapon.object.series}
|
||||
slot={2}
|
||||
ref={weaponKey3Select} />
|
||||
: ''}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const axSelect = () => {
|
||||
return (
|
||||
<section>
|
||||
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
|
||||
<AXSelect
|
||||
axType={props.gridWeapon.object.ax}
|
||||
currentSkills={props.gridWeapon.ax}
|
||||
sendValidity={receiveAxValidity}
|
||||
sendValues={receiveAxValues}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setFormValid(false)
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const elementSelect = () => {
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>
|
||||
{ props.children }
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="Weapon Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<Dialog.Title className="SubTitle">{t('modals.weapon.title')}</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">{props.gridWeapon.object.name[locale]}</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<div className="mods">
|
||||
{ (props.gridWeapon.object.element == 0) ? elementSelect() : '' }
|
||||
{ ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) ? keySelect() : '' }
|
||||
{ (props.gridWeapon.object.ax > 0) ? axSelect() : '' }
|
||||
<Button onClick={updateWeapon} disabled={props.gridWeapon.object.ax > 0 && !formValid}>{t('modals.weapon.buttons.confirm')}</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<section>
|
||||
<h3>{t("modals.weapon.subtitles.element")}</h3>
|
||||
<ElementToggle
|
||||
currentElement={props.gridWeapon.element}
|
||||
sendValue={receiveElementValue}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const keySelect = () => {
|
||||
return (
|
||||
<section>
|
||||
<h3>{t("modals.weapon.subtitles.weapon_keys")}</h3>
|
||||
{[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? (
|
||||
<WeaponKeyDropdown
|
||||
currentValue={
|
||||
props.gridWeapon.weapon_keys
|
||||
? props.gridWeapon.weapon_keys[0]
|
||||
: undefined
|
||||
}
|
||||
series={props.gridWeapon.object.series}
|
||||
slot={0}
|
||||
ref={weaponKey1Select}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
{[2, 3, 17].includes(props.gridWeapon.object.series) ? (
|
||||
<WeaponKeyDropdown
|
||||
currentValue={
|
||||
props.gridWeapon.weapon_keys
|
||||
? props.gridWeapon.weapon_keys[1]
|
||||
: undefined
|
||||
}
|
||||
series={props.gridWeapon.object.series}
|
||||
slot={1}
|
||||
ref={weaponKey2Select}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
{props.gridWeapon.object.series == 17 ? (
|
||||
<WeaponKeyDropdown
|
||||
currentValue={
|
||||
props.gridWeapon.weapon_keys
|
||||
? props.gridWeapon.weapon_keys[2]
|
||||
: undefined
|
||||
}
|
||||
series={props.gridWeapon.object.series}
|
||||
slot={2}
|
||||
ref={weaponKey3Select}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const axSelect = () => {
|
||||
return (
|
||||
<section>
|
||||
<h3>{t("modals.weapon.subtitles.ax_skills")}</h3>
|
||||
<AXSelect
|
||||
axType={props.gridWeapon.object.ax}
|
||||
currentSkills={props.gridWeapon.ax}
|
||||
sendValidity={receiveAxValidity}
|
||||
sendValues={receiveAxValues}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function openChange(open: boolean) {
|
||||
setFormValid(false)
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||
<Dialog.Trigger asChild>{props.children}</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className="Weapon Dialog"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
<div className="DialogHeader">
|
||||
<div className="DialogTop">
|
||||
<Dialog.Title className="SubTitle">
|
||||
{t("modals.weapon.title")}
|
||||
</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">
|
||||
{props.gridWeapon.object.name[locale]}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<div className="mods">
|
||||
{props.gridWeapon.object.element == 0 ? elementSelect() : ""}
|
||||
{[2, 3, 17, 24].includes(props.gridWeapon.object.series)
|
||||
? keySelect()
|
||||
: ""}
|
||||
{props.gridWeapon.object.ax > 0 ? axSelect() : ""}
|
||||
<Button
|
||||
onClick={updateWeapon}
|
||||
disabled={props.gridWeapon.object.ax > 0 && !formValid}
|
||||
>
|
||||
{t("modals.weapon.buttons.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
<Dialog.Overlay className="Overlay" />
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default WeaponModal
|
||||
export default WeaponModal
|
||||
|
|
|
|||
95
package-lock.json
generated
95
package-lock.json
generated
|
|
@ -15,6 +15,7 @@
|
|||
"@svgr/webpack": "^6.2.0",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies-next": "^2.1.1",
|
||||
"i18next": "^21.6.13",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"i18next-http-backend": "^1.3.2",
|
||||
|
|
@ -25,7 +26,6 @@
|
|||
"next-i18next": "^10.5.0",
|
||||
"next-usequerystate": "^1.7.0",
|
||||
"react": "17.0.2",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.5",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
|
|
@ -3353,11 +3353,6 @@
|
|||
"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": {
|
||||
"version": "3.3.1",
|
||||
"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_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": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz",
|
||||
|
|
@ -6333,19 +6348,6 @@
|
|||
"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": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
|
|
@ -7184,15 +7186,6 @@
|
|||
"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": {
|
||||
"version": "4.4.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.21.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz",
|
||||
|
|
@ -11886,16 +11896,6 @@
|
|||
"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": {
|
||||
"version": "17.0.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@svgr/webpack": "^6.2.0",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies-next": "^2.1.1",
|
||||
"i18next": "^21.6.13",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"i18next-http-backend": "^1.3.2",
|
||||
|
|
@ -30,7 +31,6 @@
|
|||
"next-i18next": "^10.5.0",
|
||||
"next-usequerystate": "^1.7.0",
|
||||
"react": "17.0.2",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.5",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
|
|
|
|||
|
|
@ -1,310 +1,431 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import Head from "next/head"
|
||||
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from "cookies-next"
|
||||
import { queryTypes, useQueryState } from "next-usequerystate"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import InfiniteScroll from "react-infinite-scroll-component"
|
||||
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { elements, allElement } from '~utils/Element'
|
||||
import api from "~utils/api"
|
||||
import useDidMountEffect from "~utils/useDidMountEffect"
|
||||
import { elements, allElement } from "~utils/Element"
|
||||
|
||||
import GridRep from '~components/GridRep'
|
||||
import GridRepCollection from '~components/GridRepCollection'
|
||||
import FilterBar from '~components/FilterBar'
|
||||
import GridRep from "~components/GridRep"
|
||||
import GridRepCollection from "~components/GridRepCollection"
|
||||
import FilterBar from "~components/FilterBar"
|
||||
|
||||
const emptyUser = {
|
||||
id: '',
|
||||
username: '',
|
||||
granblueId: 0,
|
||||
picture: {
|
||||
picture: '',
|
||||
element: ''
|
||||
},
|
||||
private: false,
|
||||
gender: 0
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
interface Props {
|
||||
user?: User
|
||||
teams?: { count: number; total_pages: number; results: Party[] }
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
}
|
||||
|
||||
const ProfileRoute: React.FC = () => {
|
||||
// Set up cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = (cookies.account) ? {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
} : {}
|
||||
const ProfileRoute: React.FC<Props> = (props: Props) => {
|
||||
// Set up cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { Authorization: `Bearer ${accountData.token}` }
|
||||
: {}
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const { username } = router.query
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
const { username } = router.query
|
||||
|
||||
// Import translations
|
||||
const { t } = useTranslation('common')
|
||||
// Import translations
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Set up app-specific states
|
||||
const [found, setFound] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
// Set up app-specific states
|
||||
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
// Set up page-specific states
|
||||
const [parties, setParties] = useState<Party[]>([])
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [raid, setRaid] = useState<Raid>()
|
||||
const [user, setUser] = useState<User>(emptyUser)
|
||||
// Set up page-specific states
|
||||
const [parties, setParties] = useState<Party[]>([])
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [raid, setRaid] = useState<Raid>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Set up filter-specific query states
|
||||
// Recency is in seconds
|
||||
const [element, setElement] = useQueryState("element", {
|
||||
defaultValue: -1,
|
||||
parse: (query: string) => parseElement(query),
|
||||
serialize: value => serializeElement(value)
|
||||
})
|
||||
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1))
|
||||
// Set up filter-specific query states
|
||||
// Recency is in seconds
|
||||
const [element, setElement] = useQueryState("element", {
|
||||
defaultValue: -1,
|
||||
parse: (query: string) => parseElement(query),
|
||||
serialize: (value) => serializeElement(value),
|
||||
})
|
||||
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||
const [recency, setRecency] = useQueryState(
|
||||
"recency",
|
||||
queryTypes.integer.withDefault(-1)
|
||||
)
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
let element: TeamElement | undefined =
|
||||
(query === 'all') ?
|
||||
allElement : elements.find(element => element.name.en.toLowerCase() === query)
|
||||
return (element) ? element.id : -1
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
let element: TeamElement | undefined =
|
||||
query === "all"
|
||||
? allElement
|
||||
: elements.find((element) => element.name.en.toLowerCase() === query)
|
||||
return element ? element.id : -1
|
||||
}
|
||||
|
||||
function serializeElement(value: number | undefined) {
|
||||
let name = ""
|
||||
|
||||
if (value != undefined) {
|
||||
if (value == -1) name = allElement.name.en.toLowerCase()
|
||||
else name = elements[value].name.en.toLowerCase()
|
||||
}
|
||||
|
||||
function serializeElement(value: number | undefined) {
|
||||
let name = ''
|
||||
return name
|
||||
}
|
||||
|
||||
if (value != undefined) {
|
||||
if (value == -1)
|
||||
name = allElement.name.en.toLowerCase()
|
||||
else
|
||||
name = elements[value].name.en.toLowerCase()
|
||||
}
|
||||
|
||||
return name
|
||||
// Set the initial parties from props
|
||||
useEffect(() => {
|
||||
if (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
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [])
|
||||
// Add scroll event listener for shadow on FilterBar on mount
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
console.error(error)
|
||||
} else {
|
||||
console.error("There was an error.")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchProfile = useCallback(({ replace }: { replace: boolean }) => {
|
||||
const filters = {
|
||||
params: {
|
||||
element: (element != -1) ? element : undefined,
|
||||
raid: (raid) ? raid.id : undefined,
|
||||
recency: (recency != -1) ? recency : undefined,
|
||||
page: currentPage
|
||||
}
|
||||
}
|
||||
|
||||
if (username && !Array.isArray(username))
|
||||
api.endpoints.users.getOne({ id: username , params: {...filters, ...{ headers: headers }}})
|
||||
.then(response => {
|
||||
setUser({
|
||||
id: response.data.user.id,
|
||||
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
|
||||
})
|
||||
|
||||
setTotalPages(response.data.parties.total_pages)
|
||||
setRecordCount(response.data.parties.count)
|
||||
|
||||
if (replace)
|
||||
replaceResults(response.data.parties.count, response.data.parties.results)
|
||||
else
|
||||
appendResults(response.data.parties.results)
|
||||
})
|
||||
.then(() => {
|
||||
setFound(true)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(error => handleError(error))
|
||||
}, [currentPage, parties, element, raid, recency])
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
if (count > 0) {
|
||||
setParties(list.sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
||||
} else {
|
||||
setParties([])
|
||||
}
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
console.error(error)
|
||||
} else {
|
||||
console.error("There was an error.")
|
||||
}
|
||||
}, [])
|
||||
|
||||
function appendResults(list: Party[]) {
|
||||
setParties([...parties, ...list])
|
||||
}
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll()
|
||||
.then(response => {
|
||||
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||
setRaids(cleanRaids)
|
||||
|
||||
setRaidsLoading(false)
|
||||
|
||||
const raid = cleanRaids.find(r => r.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
|
||||
return raid
|
||||
})
|
||||
}, [setRaids])
|
||||
|
||||
// When the element, raid or recency filter changes,
|
||||
// fetch all teams again.
|
||||
useEffect(() => {
|
||||
if (!raidsLoading) {
|
||||
setCurrentPage(1)
|
||||
fetchProfile({ replace: true })
|
||||
}
|
||||
}, [element, raid, recency])
|
||||
|
||||
useEffect(() => {
|
||||
// Current page changed
|
||||
if (currentPage > 1)
|
||||
fetchProfile({ replace: false })
|
||||
else if (currentPage == 1)
|
||||
fetchProfile({ replace: true })
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) {
|
||||
if (element == 0)
|
||||
setElement(0)
|
||||
else if (element)
|
||||
setElement(element)
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find(raid => raid.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug)
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency)
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90)
|
||||
setScrolled(true)
|
||||
else
|
||||
setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
}
|
||||
|
||||
// TODO: Add save functions
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return <GridRep
|
||||
id={party.id}
|
||||
shortcode={party.shortcode}
|
||||
name={party.name}
|
||||
createdAt={new Date(party.created_at)}
|
||||
raid={party.raid}
|
||||
grid={party.weapons}
|
||||
favorited={party.favorited}
|
||||
key={`party-${i}`}
|
||||
onClick={goTo}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Profile">
|
||||
<Head>
|
||||
<title>@{user.username}'s Teams</title>
|
||||
|
||||
<meta property="og:title" content={`@${user.username}\'s Teams`} />
|
||||
<meta property="og:description" content={`Browse @${user.username}\'s Teams and filter raid, element or recency`} />
|
||||
<meta property="og:url" content={`https://app.granblue.team/${user.username}`} />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content={`@${user.username}\'s Teams`} />
|
||||
<meta name="twitter:description" content={`Browse @${user.username}\''s Teams and filter raid, element or recency`} />
|
||||
</Head>
|
||||
<FilterBar
|
||||
onFilter={receiveFilters}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raidSlug={ (raidSlug) ? raidSlug : undefined }
|
||||
recency={recency}>
|
||||
<div className="UserInfo">
|
||||
<img
|
||||
alt={user.picture.picture}
|
||||
className={`profile ${user.picture.element}`}
|
||||
srcSet={`/profile/${user.picture.picture}.png,
|
||||
/profile/${user.picture.picture}@2x.png 2x`}
|
||||
src={`/profile/${user.picture.picture}.png`}
|
||||
/>
|
||||
<h1>{user.username}</h1>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<section>
|
||||
<InfiniteScroll
|
||||
dataLength={ (parties && parties.length > 0) ? parties.length : 0}
|
||||
next={ () => setCurrentPage(currentPage + 1) }
|
||||
hasMore={totalPages > currentPage}
|
||||
loader={ <div id="NotFound"><h2>Loading...</h2></div> }>
|
||||
<GridRepCollection loading={loading}>
|
||||
{ renderParties() }
|
||||
</GridRepCollection>
|
||||
</InfiniteScroll>
|
||||
|
||||
{ (parties.length == 0) ?
|
||||
<div id="NotFound">
|
||||
<h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2>
|
||||
</div>
|
||||
: '' }
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
// Object variant:
|
||||
{ params: { username: 'string' } },
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
// Will be passed to the page component as props
|
||||
const fetchProfile = useCallback(
|
||||
({ replace }: { replace: boolean }) => {
|
||||
const filters = {
|
||||
params: {
|
||||
element: element != -1 ? element : undefined,
|
||||
raid: raid ? raid.id : undefined,
|
||||
recency: recency != -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
},
|
||||
}
|
||||
|
||||
if (username && !Array.isArray(username)) {
|
||||
api.endpoints.users
|
||||
.getOne({
|
||||
id: username,
|
||||
params: { ...filters, ...{ headers: headers } },
|
||||
})
|
||||
.then((response) => {
|
||||
setTotalPages(response.data.parties.total_pages)
|
||||
setRecordCount(response.data.parties.count)
|
||||
|
||||
if (replace)
|
||||
replaceResults(
|
||||
response.data.parties.count,
|
||||
response.data.parties.results
|
||||
)
|
||||
else appendResults(response.data.parties.results)
|
||||
})
|
||||
.catch((error) => handleError(error))
|
||||
}
|
||||
},
|
||||
[currentPage, parties, element, raid, recency]
|
||||
)
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
if (count > 0) {
|
||||
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
|
||||
} else {
|
||||
setParties([])
|
||||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Party[]) {
|
||||
setParties([...parties, ...list])
|
||||
}
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll().then((response) => {
|
||||
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||
setRaids(cleanRaids)
|
||||
|
||||
setRaidsLoading(false)
|
||||
|
||||
const raid = cleanRaids.find((r) => r.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
|
||||
return raid
|
||||
})
|
||||
}, [setRaids])
|
||||
|
||||
// When the element, raid or recency filter changes,
|
||||
// fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
setCurrentPage(1)
|
||||
fetchProfile({ replace: true })
|
||||
}, [element, raid, recency])
|
||||
|
||||
// When the page changes, fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
// Current page changed
|
||||
if (currentPage > 1) fetchProfile({ replace: false })
|
||||
else if (currentPage == 1) fetchProfile({ replace: true })
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) {
|
||||
if (element == 0) setElement(0)
|
||||
else if (element) setElement(element)
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug)
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency)
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90) setScrolled(true)
|
||||
else setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
}
|
||||
|
||||
// TODO: Add save functions
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return (
|
||||
<GridRep
|
||||
id={party.id}
|
||||
shortcode={party.shortcode}
|
||||
name={party.name}
|
||||
createdAt={new Date(party.created_at)}
|
||||
raid={party.raid}
|
||||
grid={party.weapons}
|
||||
favorited={party.favorited}
|
||||
key={`party-${i}`}
|
||||
onClick={goTo}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Profile">
|
||||
<Head>
|
||||
<title>@{props.user?.username}'s Teams</title>
|
||||
|
||||
<meta
|
||||
property="og:title"
|
||||
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 name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta
|
||||
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>
|
||||
<FilterBar
|
||||
onFilter={receiveFilters}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raidSlug={raidSlug ? raidSlug : undefined}
|
||||
recency={recency}
|
||||
>
|
||||
<div className="UserInfo">
|
||||
<img
|
||||
alt={props.user?.picture.picture}
|
||||
className={`profile ${props.user?.picture.element}`}
|
||||
srcSet={`/profile/${props.user?.picture.picture}.png,
|
||||
/profile/${props.user?.picture.picture}@2x.png 2x`}
|
||||
src={`/profile/${props.user?.picture.picture}.png`}
|
||||
/>
|
||||
<h1>{props.user?.username}</h1>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<section>
|
||||
<InfiniteScroll
|
||||
dataLength={parties && parties.length > 0 ? parties.length : 0}
|
||||
next={() => setCurrentPage(currentPage + 1)}
|
||||
hasMore={totalPages > currentPage}
|
||||
loader={
|
||||
<div id="NotFound">
|
||||
<h2>Loading...</h2>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GridRepCollection>{renderParties()}</GridRepCollection>
|
||||
</InfiniteScroll>
|
||||
|
||||
{parties.length == 0 ? (
|
||||
<div id="NotFound">
|
||||
<h2>{t("teams.not_found")}</h2>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
props: {
|
||||
user: user,
|
||||
teams: teams,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const organizeRaids = (raids: Raid[]) => {
|
||||
// Set up empty raid for "All raids"
|
||||
const all = {
|
||||
id: "0",
|
||||
name: {
|
||||
en: "All raids",
|
||||
ja: "全て",
|
||||
},
|
||||
slug: "all",
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
}
|
||||
|
||||
const numGroups = Math.max.apply(
|
||||
Math,
|
||||
raids.map((raid) => raid.group)
|
||||
)
|
||||
let groupedRaids = []
|
||||
|
||||
for (let i = 0; i <= numGroups; i++) {
|
||||
groupedRaids[i] = raids.filter((raid) => raid.group == i)
|
||||
}
|
||||
|
||||
return {
|
||||
raids: raids,
|
||||
sortedRaids: groupedRaids,
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileRoute
|
||||
|
|
|
|||
|
|
@ -1,41 +1,40 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useCookies, CookiesProvider } from 'react-cookie'
|
||||
import { appWithTranslation } from 'next-i18next'
|
||||
import { useEffect } from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { appWithTranslation } from "next-i18next"
|
||||
|
||||
import type { AppProps } from 'next/app'
|
||||
import Layout from '~components/Layout'
|
||||
import type { AppProps } from "next/app"
|
||||
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) {
|
||||
const [cookies] = useCookies(['account'])
|
||||
const cookie = getCookie("account")
|
||||
const cookieData: AccountCookie = cookie ? JSON.parse(cookie as string) : null
|
||||
|
||||
useEffect(() => {
|
||||
if (cookies.account) {
|
||||
console.log(`Logged in as user "${cookies.account.username}"`)
|
||||
useEffect(() => {
|
||||
if (cookie) {
|
||||
console.log(`Logged in as user "${cookieData.username}"`)
|
||||
|
||||
accountState.account.authorized = true
|
||||
accountState.account.user = {
|
||||
id: cookies.account.user_id,
|
||||
username: cookies.account.username,
|
||||
picture: '',
|
||||
element: '',
|
||||
gender: 0
|
||||
}
|
||||
} else {
|
||||
console.log(`You are not currently logged in.`)
|
||||
}
|
||||
}, [cookies.account])
|
||||
accountState.account.authorized = true
|
||||
accountState.account.user = {
|
||||
id: cookieData.userId,
|
||||
username: cookieData.username,
|
||||
picture: "",
|
||||
element: "",
|
||||
gender: 0,
|
||||
}
|
||||
} else {
|
||||
console.log(`You are not currently logged in.`)
|
||||
}
|
||||
}, [cookieData])
|
||||
|
||||
return (
|
||||
<CookiesProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</CookiesProvider>
|
||||
)
|
||||
return (
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default appWithTranslation(MyApp)
|
||||
|
|
|
|||
|
|
@ -1,53 +1,100 @@
|
|||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import React from "react"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
|
||||
import Party from '~components/Party'
|
||||
import Party from "~components/Party"
|
||||
|
||||
const PartyRoute: React.FC = () => {
|
||||
const { party: slug } = useRouter().query
|
||||
import api from "~utils/api"
|
||||
|
||||
return (
|
||||
<div id="Content">
|
||||
<Party slug={slug as string} />
|
||||
</div>
|
||||
)
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
// function renderNotFound() {
|
||||
// return (
|
||||
// <div id="NotFound">
|
||||
// <h2>There's no grid here.</h2>
|
||||
// <Button type="new">New grid</Button>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (!found && !loading) {
|
||||
// return renderNotFound()
|
||||
// } else if (found && !loading) {
|
||||
// return render()
|
||||
// } else {
|
||||
// return (<div />)
|
||||
// }
|
||||
interface Props {
|
||||
party: Party
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return {
|
||||
paths: [
|
||||
// Object variant:
|
||||
{ params: { party: 'string' } },
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
const PartyRoute: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div id="Content">
|
||||
<Party team={props.party} raids={props.sortedRaids} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
export const getServerSidePaths = async () => {
|
||||
return {
|
||||
paths: [
|
||||
// Object variant:
|
||||
{ params: { party: "string" } },
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
export default PartyRoute
|
||||
// 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 {
|
||||
props: {
|
||||
party: party,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const organizeRaids = (raids: Raid[]) => {
|
||||
// Set up empty raid for "All raids"
|
||||
const all = {
|
||||
id: "0",
|
||||
name: {
|
||||
en: "All raids",
|
||||
ja: "全て",
|
||||
},
|
||||
slug: "all",
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
}
|
||||
|
||||
const numGroups = Math.max.apply(
|
||||
Math,
|
||||
raids.map((raid) => raid.group)
|
||||
)
|
||||
let groupedRaids = []
|
||||
|
||||
for (let i = 0; i <= numGroups; i++) {
|
||||
groupedRaids[i] = raids.filter((raid) => raid.group == i)
|
||||
}
|
||||
|
||||
return {
|
||||
raids: raids,
|
||||
sortedRaids: groupedRaids,
|
||||
}
|
||||
}
|
||||
|
||||
export default PartyRoute
|
||||
|
|
|
|||
707
pages/saved.tsx
707
pages/saved.tsx
|
|
@ -1,306 +1,425 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import Head from "next/head"
|
||||
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from "cookies-next"
|
||||
import { queryTypes, useQueryState } from "next-usequerystate"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import InfiniteScroll from "react-infinite-scroll-component"
|
||||
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
import clonedeep from "lodash.clonedeep"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { elements, allElement } from '~utils/Element'
|
||||
import api from "~utils/api"
|
||||
import useDidMountEffect from "~utils/useDidMountEffect"
|
||||
import { elements, allElement } from "~utils/Element"
|
||||
|
||||
import GridRep from '~components/GridRep'
|
||||
import GridRepCollection from '~components/GridRepCollection'
|
||||
import FilterBar from '~components/FilterBar'
|
||||
import GridRep from "~components/GridRep"
|
||||
import GridRepCollection from "~components/GridRepCollection"
|
||||
import FilterBar from "~components/FilterBar"
|
||||
|
||||
const SavedRoute: React.FC = () => {
|
||||
// Set up cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = (cookies.account) ? {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
} : {}
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
// Import translations
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up app-specific states
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
// Set up page-specific states
|
||||
const [parties, setParties] = useState<Party[]>([])
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [raid, setRaid] = useState<Raid>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Set up filter-specific query states
|
||||
// Recency is in seconds
|
||||
const [element, setElement] = useQueryState("element", {
|
||||
defaultValue: -1,
|
||||
parse: (query: string) => parseElement(query),
|
||||
serialize: value => serializeElement(value)
|
||||
})
|
||||
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1))
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
let element: TeamElement | undefined =
|
||||
(query === 'all') ?
|
||||
allElement : elements.find(element => element.name.en.toLowerCase() === query)
|
||||
return (element) ? element.id : -1
|
||||
}
|
||||
|
||||
function serializeElement(value: number | undefined) {
|
||||
let name = ''
|
||||
|
||||
if (value != undefined) {
|
||||
if (value == -1)
|
||||
name = allElement.name.en.toLowerCase()
|
||||
else
|
||||
name = elements[value].name.en.toLowerCase()
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// Add scroll event listener for shadow on FilterBar on mount
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
console.error(error)
|
||||
} else {
|
||||
console.error("There was an error.")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchTeams = useCallback(({ replace }: { replace: boolean }) => {
|
||||
const filters = {
|
||||
params: {
|
||||
element: (element != -1) ? element : undefined,
|
||||
raid: (raid) ? raid.id : undefined,
|
||||
recency: (recency != -1) ? recency : undefined,
|
||||
page: currentPage
|
||||
}
|
||||
}
|
||||
|
||||
api.savedTeams({...filters, ...{ headers: headers }})
|
||||
.then(response => {
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
|
||||
if (replace)
|
||||
replaceResults(response.data.count, response.data.results)
|
||||
else
|
||||
appendResults(response.data.results)
|
||||
})
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(error => handleError(error))
|
||||
}, [currentPage, parties, element, raid, recency])
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
if (count > 0) {
|
||||
setParties(list)
|
||||
} else {
|
||||
setParties([])
|
||||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Party[]) {
|
||||
setParties([...parties, ...list])
|
||||
}
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll()
|
||||
.then(response => {
|
||||
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||
setRaids(cleanRaids)
|
||||
|
||||
setRaidsLoading(false)
|
||||
|
||||
const raid = cleanRaids.find(r => r.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
|
||||
return raid
|
||||
})
|
||||
}, [setRaids])
|
||||
|
||||
// When the element, raid or recency filter changes,
|
||||
// fetch all teams again.
|
||||
useEffect(() => {
|
||||
if (!raidsLoading) {
|
||||
setCurrentPage(1)
|
||||
fetchTeams({ replace: true })
|
||||
}
|
||||
}, [element, raid, recency])
|
||||
|
||||
useEffect(() => {
|
||||
// Current page changed
|
||||
if (currentPage > 1)
|
||||
fetchTeams({ replace: false })
|
||||
else if (currentPage == 1)
|
||||
fetchTeams({ replace: true })
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) {
|
||||
if (element == 0)
|
||||
setElement(0)
|
||||
else if (element)
|
||||
setElement(element)
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find(raid => raid.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug)
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency)
|
||||
}
|
||||
|
||||
// Methods: Favorites
|
||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||
if (favorited)
|
||||
unsaveFavorite(teamId)
|
||||
else
|
||||
saveFavorite(teamId)
|
||||
}
|
||||
|
||||
function saveFavorite(teamId: string) {
|
||||
api.saveTeam({ id: teamId, params: headers })
|
||||
.then((response) => {
|
||||
if (response.status == 201) {
|
||||
const index = parties.findIndex(p => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = true
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties[index] = party
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function unsaveFavorite(teamId: string) {
|
||||
api.unsaveTeam({ id: teamId, params: headers })
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
const index = parties.findIndex(p => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = false
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties.splice(index, 1)
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90)
|
||||
setScrolled(true)
|
||||
else
|
||||
setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
}
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return <GridRep
|
||||
id={party.id}
|
||||
shortcode={party.shortcode}
|
||||
name={party.name}
|
||||
createdAt={new Date(party.created_at)}
|
||||
raid={party.raid}
|
||||
grid={party.weapons}
|
||||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
onSave={toggleFavorite} />
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Teams">
|
||||
<Head>
|
||||
<title>{t('saved.title')}</title>
|
||||
|
||||
<meta property="og:title" content="Your saved Teams" />
|
||||
<meta property="og:url" content="https://app.granblue.team/saved" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content="Your saved Teams" />
|
||||
</Head>
|
||||
|
||||
<FilterBar
|
||||
onFilter={receiveFilters}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raidSlug={ (raidSlug) ? raidSlug : undefined }
|
||||
recency={recency}>
|
||||
<h1>{t('saved.title')}</h1>
|
||||
</FilterBar>
|
||||
|
||||
<section>
|
||||
<InfiniteScroll
|
||||
dataLength={ (parties && parties.length > 0) ? parties.length : 0}
|
||||
next={ () => setCurrentPage(currentPage + 1) }
|
||||
hasMore={totalPages > currentPage}
|
||||
loader={ <div id="NotFound"><h2>Loading...</h2></div> }>
|
||||
<GridRepCollection loading={loading}>
|
||||
{ renderParties() }
|
||||
</GridRepCollection>
|
||||
</InfiniteScroll>
|
||||
|
||||
{ (parties.length == 0) ?
|
||||
<div id="NotFound">
|
||||
<h2>{ (loading) ? t('saved.loading') : t('saved.not_found') }</h2>
|
||||
</div>
|
||||
: '' }
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
interface Props {
|
||||
teams?: { count: number; total_pages: number; results: Party[] }
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
}
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
// Will be passed to the page component as props
|
||||
const SavedRoute: React.FC<Props> = (props: Props) => {
|
||||
// Set up cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { Authorization: `Bearer ${accountData.token}` }
|
||||
: {}
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
// Import translations
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Set up app-specific states
|
||||
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
// Set up page-specific states
|
||||
const [parties, setParties] = useState<Party[]>([])
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [raid, setRaid] = useState<Raid>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Set up filter-specific query states
|
||||
// Recency is in seconds
|
||||
const [element, setElement] = useQueryState("element", {
|
||||
defaultValue: -1,
|
||||
parse: (query: string) => parseElement(query),
|
||||
serialize: (value) => serializeElement(value),
|
||||
})
|
||||
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||
const [recency, setRecency] = useQueryState(
|
||||
"recency",
|
||||
queryTypes.integer.withDefault(-1)
|
||||
)
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
let element: TeamElement | undefined =
|
||||
query === "all"
|
||||
? allElement
|
||||
: elements.find((element) => element.name.en.toLowerCase() === query)
|
||||
return element ? element.id : -1
|
||||
}
|
||||
|
||||
function serializeElement(value: number | undefined) {
|
||||
let name = ""
|
||||
|
||||
if (value != undefined) {
|
||||
if (value == -1) name = allElement.name.en.toLowerCase()
|
||||
else name = elements[value].name.en.toLowerCase()
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
console.error(error)
|
||||
} else {
|
||||
console.error("There was an error.")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchTeams = useCallback(
|
||||
({ replace }: { replace: boolean }) => {
|
||||
const filters = {
|
||||
params: {
|
||||
element: element != -1 ? element : undefined,
|
||||
raid: raid ? raid.id : undefined,
|
||||
recency: recency != -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
},
|
||||
}
|
||||
|
||||
api
|
||||
.savedTeams({ ...filters, ...{ headers: headers } })
|
||||
.then((response) => {
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
|
||||
if (replace)
|
||||
replaceResults(response.data.count, response.data.results)
|
||||
else appendResults(response.data.results)
|
||||
})
|
||||
.catch((error) => handleError(error))
|
||||
},
|
||||
[currentPage, parties, element, raid, recency]
|
||||
)
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
if (count > 0) {
|
||||
setParties(list)
|
||||
} else {
|
||||
setParties([])
|
||||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Party[]) {
|
||||
setParties([...parties, ...list])
|
||||
}
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll().then((response) => {
|
||||
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||
setRaids(cleanRaids)
|
||||
|
||||
setRaidsLoading(false)
|
||||
|
||||
const raid = cleanRaids.find((r) => r.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
|
||||
return raid
|
||||
})
|
||||
}, [setRaids])
|
||||
|
||||
// When the element, raid or recency filter changes,
|
||||
// fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
setCurrentPage(1)
|
||||
fetchTeams({ replace: true })
|
||||
}, [element, raid, recency])
|
||||
|
||||
// When the page changes, fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
// Current page changed
|
||||
if (currentPage > 1) fetchTeams({ replace: false })
|
||||
else if (currentPage == 1) fetchTeams({ replace: true })
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) {
|
||||
if (element == 0) setElement(0)
|
||||
else if (element) setElement(element)
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug)
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency)
|
||||
}
|
||||
|
||||
// Methods: Favorites
|
||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||
if (favorited) unsaveFavorite(teamId)
|
||||
else saveFavorite(teamId)
|
||||
}
|
||||
|
||||
function saveFavorite(teamId: string) {
|
||||
api.saveTeam({ id: teamId, params: headers }).then((response) => {
|
||||
if (response.status == 201) {
|
||||
const index = parties.findIndex((p) => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = true
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties[index] = party
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function unsaveFavorite(teamId: string) {
|
||||
api.unsaveTeam({ id: teamId, params: headers }).then((response) => {
|
||||
if (response.status == 200) {
|
||||
const index = parties.findIndex((p) => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = false
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties.splice(index, 1)
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90) setScrolled(true)
|
||||
else setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
}
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return (
|
||||
<GridRep
|
||||
id={party.id}
|
||||
shortcode={party.shortcode}
|
||||
name={party.name}
|
||||
createdAt={new Date(party.created_at)}
|
||||
raid={party.raid}
|
||||
grid={party.weapons}
|
||||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
onSave={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Teams">
|
||||
<Head>
|
||||
<title>{t("saved.title")}</title>
|
||||
|
||||
<meta property="og:title" content="Your saved Teams" />
|
||||
<meta property="og:url" content="https://app.granblue.team/saved" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content="Your saved Teams" />
|
||||
</Head>
|
||||
|
||||
<FilterBar
|
||||
onFilter={receiveFilters}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raidSlug={raidSlug ? raidSlug : undefined}
|
||||
recency={recency}
|
||||
>
|
||||
<h1>{t("saved.title")}</h1>
|
||||
</FilterBar>
|
||||
|
||||
<section>
|
||||
<InfiniteScroll
|
||||
dataLength={parties && parties.length > 0 ? parties.length : 0}
|
||||
next={() => setCurrentPage(currentPage + 1)}
|
||||
hasMore={totalPages > currentPage}
|
||||
loader={
|
||||
<div id="NotFound">
|
||||
<h2>Loading...</h2>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GridRepCollection>{renderParties()}</GridRepCollection>
|
||||
</InfiniteScroll>
|
||||
|
||||
{parties.length == 0 ? (
|
||||
<div id="NotFound">
|
||||
<h2>{t("saved.not_found")}</h2>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SavedRoute
|
||||
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 {
|
||||
props: {
|
||||
teams: response.data,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const organizeRaids = (raids: Raid[]) => {
|
||||
// Set up empty raid for "All raids"
|
||||
const all = {
|
||||
id: "0",
|
||||
name: {
|
||||
en: "All raids",
|
||||
ja: "全て",
|
||||
},
|
||||
slug: "all",
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
}
|
||||
|
||||
const numGroups = Math.max.apply(
|
||||
Math,
|
||||
raids.map((raid) => raid.group)
|
||||
)
|
||||
let groupedRaids = []
|
||||
|
||||
for (let i = 0; i <= numGroups; i++) {
|
||||
groupedRaids[i] = raids.filter((raid) => raid.group == i)
|
||||
}
|
||||
|
||||
return {
|
||||
raids: raids,
|
||||
sortedRaids: groupedRaids,
|
||||
}
|
||||
}
|
||||
|
||||
export default SavedRoute
|
||||
|
|
|
|||
717
pages/teams.tsx
717
pages/teams.tsx
|
|
@ -1,308 +1,433 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import Head from "next/head"
|
||||
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { queryTypes, useQueryState } from 'next-usequerystate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { getCookie } from "cookies-next"
|
||||
import { queryTypes, useQueryState } from "next-usequerystate"
|
||||
import { useRouter } from "next/router"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import InfiniteScroll from "react-infinite-scroll-component"
|
||||
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
import clonedeep from "lodash.clonedeep"
|
||||
|
||||
import api from '~utils/api'
|
||||
import { elements, allElement } from '~utils/Element'
|
||||
import api from "~utils/api"
|
||||
import useDidMountEffect from "~utils/useDidMountEffect"
|
||||
import { elements, allElement } from "~utils/Element"
|
||||
|
||||
import GridRep from '~components/GridRep'
|
||||
import GridRepCollection from '~components/GridRepCollection'
|
||||
import FilterBar from '~components/FilterBar'
|
||||
import GridRep from "~components/GridRep"
|
||||
import GridRepCollection from "~components/GridRepCollection"
|
||||
import FilterBar from "~components/FilterBar"
|
||||
|
||||
const TeamsRoute: React.FC = () => {
|
||||
// Set up cookies
|
||||
const [cookies] = useCookies(['account'])
|
||||
const headers = (cookies.account) ? {
|
||||
'Authorization': `Bearer ${cookies.account.access_token}`
|
||||
} : {}
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
// Import translations
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up app-specific states
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
// Set up page-specific states
|
||||
const [parties, setParties] = useState<Party[]>([])
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [raid, setRaid] = useState<Raid>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Set up filter-specific query states
|
||||
// Recency is in seconds
|
||||
const [element, setElement] = useQueryState("element", {
|
||||
defaultValue: -1,
|
||||
parse: (query: string) => parseElement(query),
|
||||
serialize: value => serializeElement(value)
|
||||
})
|
||||
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||
const [recency, setRecency] = useQueryState("recency", queryTypes.integer.withDefault(-1))
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
let element: TeamElement | undefined =
|
||||
(query === 'all') ?
|
||||
allElement : elements.find(element => element.name.en.toLowerCase() === query)
|
||||
return (element) ? element.id : -1
|
||||
}
|
||||
|
||||
function serializeElement(value: number | undefined) {
|
||||
let name = ''
|
||||
|
||||
if (value != undefined) {
|
||||
if (value == -1)
|
||||
name = allElement.name.en.toLowerCase()
|
||||
else
|
||||
name = elements[value].name.en.toLowerCase()
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// Add scroll event listener for shadow on FilterBar on mount
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
console.error(error)
|
||||
} else {
|
||||
console.error("There was an error.")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchTeams = useCallback(({ replace }: { replace: boolean }) => {
|
||||
const filters = {
|
||||
params: {
|
||||
element: (element != -1) ? element : undefined,
|
||||
raid: (raid) ? raid.id : undefined,
|
||||
recency: (recency != -1) ? recency : undefined,
|
||||
page: currentPage
|
||||
}
|
||||
}
|
||||
|
||||
api.endpoints.parties.getAll({...filters, ...{ headers: headers }})
|
||||
.then(response => {
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
|
||||
if (replace)
|
||||
replaceResults(response.data.count, response.data.results)
|
||||
else
|
||||
appendResults(response.data.results)
|
||||
})
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(error => handleError(error))
|
||||
}, [currentPage, parties, element, raid, recency])
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
if (count > 0) {
|
||||
setParties(list.sort((a, b) => (a.created_at > b.created_at) ? -1 : 1))
|
||||
} else {
|
||||
setParties([])
|
||||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Party[]) {
|
||||
setParties([...parties, ...list])
|
||||
}
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll()
|
||||
.then(response => {
|
||||
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||
setRaids(cleanRaids)
|
||||
|
||||
setRaidsLoading(false)
|
||||
|
||||
const raid = cleanRaids.find(r => r.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
|
||||
return raid
|
||||
})
|
||||
}, [setRaids])
|
||||
|
||||
// When the element, raid or recency filter changes,
|
||||
// fetch all teams again.
|
||||
useEffect(() => {
|
||||
if (!raidsLoading) {
|
||||
setCurrentPage(1)
|
||||
fetchTeams({ replace: true })
|
||||
}
|
||||
}, [element, raid, recency])
|
||||
|
||||
useEffect(() => {
|
||||
// Current page changed
|
||||
if (currentPage > 1)
|
||||
fetchTeams({ replace: false })
|
||||
else if (currentPage == 1)
|
||||
fetchTeams({ replace: true })
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({ element, raidSlug, recency }: {element?: number, raidSlug?: string, recency?: number}) {
|
||||
if (element == 0)
|
||||
setElement(0)
|
||||
else if (element)
|
||||
setElement(element)
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find(raid => raid.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug)
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency)
|
||||
}
|
||||
|
||||
// Methods: Favorites
|
||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||
if (favorited)
|
||||
unsaveFavorite(teamId)
|
||||
else
|
||||
saveFavorite(teamId)
|
||||
}
|
||||
|
||||
function saveFavorite(teamId: string) {
|
||||
api.saveTeam({ id: teamId, params: headers })
|
||||
.then((response) => {
|
||||
if (response.status == 201) {
|
||||
const index = parties.findIndex(p => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = true
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties[index] = party
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function unsaveFavorite(teamId: string) {
|
||||
api.unsaveTeam({ id: teamId, params: headers })
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
const index = parties.findIndex(p => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = false
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties[index] = party
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90)
|
||||
setScrolled(true)
|
||||
else
|
||||
setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
}
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return <GridRep
|
||||
id={party.id}
|
||||
shortcode={party.shortcode}
|
||||
name={party.name}
|
||||
createdAt={new Date(party.created_at)}
|
||||
raid={party.raid}
|
||||
grid={party.weapons}
|
||||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
onSave={toggleFavorite} />
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Teams">
|
||||
<Head>
|
||||
<title>{ t('teams.title') }</title>
|
||||
|
||||
<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:url" content="https://app.granblue.team/teams" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content="Discover Teams" />
|
||||
<meta name="twitter:description" content="Find different Granblue Fantasy teams by raid, element or recency" />
|
||||
</Head>
|
||||
|
||||
<FilterBar
|
||||
onFilter={receiveFilters}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raidSlug={ (raidSlug) ? raidSlug : undefined }
|
||||
recency={recency}>
|
||||
<h1>{t('teams.title')}</h1>
|
||||
</FilterBar>
|
||||
|
||||
<section>
|
||||
<InfiniteScroll
|
||||
dataLength={ (parties && parties.length > 0) ? parties.length : 0}
|
||||
next={ () => setCurrentPage(currentPage + 1) }
|
||||
hasMore={totalPages > currentPage}
|
||||
loader={ <div id="NotFound"><h2>Loading...</h2></div> }>
|
||||
<GridRepCollection loading={loading}>
|
||||
{ renderParties() }
|
||||
</GridRepCollection>
|
||||
</InfiniteScroll>
|
||||
|
||||
{ (parties.length == 0) ?
|
||||
<div id="NotFound">
|
||||
<h2>{ (loading) ? t('teams.loading') : t('teams.not_found') }</h2>
|
||||
</div>
|
||||
: '' }
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
interface Props {
|
||||
teams?: { count: number; total_pages: number; results: Party[] }
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
}
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
// Will be passed to the page component as props
|
||||
const TeamsRoute: React.FC<Props> = (props: Props) => {
|
||||
// Set up cookies
|
||||
const cookie = getCookie("account")
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
const headers = accountData
|
||||
? { Authorization: `Bearer ${accountData.token}` }
|
||||
: {}
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
// Import translations
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// Set up app-specific states
|
||||
const [raidsLoading, setRaidsLoading] = useState(true)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
|
||||
// Set up page-specific states
|
||||
const [parties, setParties] = useState<Party[]>([])
|
||||
const [raids, setRaids] = useState<Raid[]>()
|
||||
const [raid, setRaid] = useState<Raid>()
|
||||
|
||||
// Set up infinite scrolling-related states
|
||||
const [recordCount, setRecordCount] = useState(0)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Set up filter-specific query states
|
||||
// Recency is in seconds
|
||||
const [element, setElement] = useQueryState("element", {
|
||||
defaultValue: -1,
|
||||
parse: (query: string) => parseElement(query),
|
||||
serialize: (value) => serializeElement(value),
|
||||
})
|
||||
const [raidSlug, setRaidSlug] = useQueryState("raid", { defaultValue: "all" })
|
||||
const [recency, setRecency] = useQueryState(
|
||||
"recency",
|
||||
queryTypes.integer.withDefault(-1)
|
||||
)
|
||||
|
||||
// Define transformers for element
|
||||
function parseElement(query: string) {
|
||||
let element: TeamElement | undefined =
|
||||
query === "all"
|
||||
? allElement
|
||||
: elements.find((element) => element.name.en.toLowerCase() === query)
|
||||
return element ? element.id : -1
|
||||
}
|
||||
|
||||
function serializeElement(value: number | undefined) {
|
||||
let name = ""
|
||||
|
||||
if (value != undefined) {
|
||||
if (value == -1) name = allElement.name.en.toLowerCase()
|
||||
else name = elements[value].name.en.toLowerCase()
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
// Handle errors
|
||||
const handleError = useCallback((error: any) => {
|
||||
if (error.response != null) {
|
||||
console.error(error)
|
||||
} else {
|
||||
console.error("There was an error.")
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchTeams = useCallback(
|
||||
({ replace }: { replace: boolean }) => {
|
||||
const filters = {
|
||||
params: {
|
||||
element: element != -1 ? element : undefined,
|
||||
raid: raid ? raid.id : undefined,
|
||||
recency: recency != -1 ? recency : undefined,
|
||||
page: currentPage,
|
||||
},
|
||||
}
|
||||
|
||||
api.endpoints.parties
|
||||
.getAll({ ...filters, ...{ headers: headers } })
|
||||
.then((response) => {
|
||||
setTotalPages(response.data.total_pages)
|
||||
setRecordCount(response.data.count)
|
||||
|
||||
if (replace)
|
||||
replaceResults(response.data.count, response.data.results)
|
||||
else appendResults(response.data.results)
|
||||
})
|
||||
.catch((error) => handleError(error))
|
||||
},
|
||||
[currentPage, parties, element, raid, recency]
|
||||
)
|
||||
|
||||
function replaceResults(count: number, list: Party[]) {
|
||||
if (count > 0) {
|
||||
setParties(list.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)))
|
||||
} else {
|
||||
setParties([])
|
||||
}
|
||||
}
|
||||
|
||||
function appendResults(list: Party[]) {
|
||||
setParties([...parties, ...list])
|
||||
}
|
||||
|
||||
// Fetch all raids on mount, then find the raid in the URL if present
|
||||
useEffect(() => {
|
||||
api.endpoints.raids.getAll().then((response) => {
|
||||
const cleanRaids: Raid[] = response.data.map((r: any) => r.raid)
|
||||
setRaids(cleanRaids)
|
||||
|
||||
setRaidsLoading(false)
|
||||
|
||||
const raid = cleanRaids.find((r) => r.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
|
||||
return raid
|
||||
})
|
||||
}, [setRaids])
|
||||
|
||||
// When the element, raid or recency filter changes,
|
||||
// fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
setCurrentPage(1)
|
||||
fetchTeams({ replace: true })
|
||||
}, [element, raid, recency])
|
||||
|
||||
// When the page changes, fetch all teams again.
|
||||
useDidMountEffect(() => {
|
||||
// Current page changed
|
||||
if (currentPage > 1) fetchTeams({ replace: false })
|
||||
else if (currentPage == 1) fetchTeams({ replace: true })
|
||||
}, [currentPage])
|
||||
|
||||
// Receive filters from the filter bar
|
||||
function receiveFilters({
|
||||
element,
|
||||
raidSlug,
|
||||
recency,
|
||||
}: {
|
||||
element?: number
|
||||
raidSlug?: string
|
||||
recency?: number
|
||||
}) {
|
||||
if (element == 0) setElement(0)
|
||||
else if (element) setElement(element)
|
||||
|
||||
if (raids && raidSlug) {
|
||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||
setRaid(raid)
|
||||
setRaidSlug(raidSlug)
|
||||
}
|
||||
|
||||
if (recency) setRecency(recency)
|
||||
}
|
||||
|
||||
// Methods: Favorites
|
||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||
if (favorited) unsaveFavorite(teamId)
|
||||
else saveFavorite(teamId)
|
||||
}
|
||||
|
||||
function saveFavorite(teamId: string) {
|
||||
api.saveTeam({ id: teamId, params: headers }).then((response) => {
|
||||
if (response.status == 201) {
|
||||
const index = parties.findIndex((p) => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = true
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties[index] = party
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function unsaveFavorite(teamId: string) {
|
||||
api.unsaveTeam({ id: teamId, params: headers }).then((response) => {
|
||||
if (response.status == 200) {
|
||||
const index = parties.findIndex((p) => p.id === teamId)
|
||||
const party = parties[index]
|
||||
|
||||
party.favorited = false
|
||||
|
||||
let clonedParties = clonedeep(parties)
|
||||
clonedParties[index] = party
|
||||
|
||||
setParties(clonedParties)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Methods: Navigation
|
||||
function handleScroll() {
|
||||
if (window.pageYOffset > 90) setScrolled(true)
|
||||
else setScrolled(false)
|
||||
}
|
||||
|
||||
function goTo(shortcode: string) {
|
||||
router.push(`/p/${shortcode}`)
|
||||
}
|
||||
|
||||
function renderParties() {
|
||||
return parties.map((party, i) => {
|
||||
return (
|
||||
<GridRep
|
||||
id={party.id}
|
||||
shortcode={party.shortcode}
|
||||
name={party.name}
|
||||
createdAt={new Date(party.created_at)}
|
||||
raid={party.raid}
|
||||
grid={party.weapons}
|
||||
user={party.user}
|
||||
favorited={party.favorited}
|
||||
key={`party-${i}`}
|
||||
displayUser={true}
|
||||
onClick={goTo}
|
||||
onSave={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Teams">
|
||||
<Head>
|
||||
<title>{t("teams.title")}</title>
|
||||
|
||||
<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:url" content="https://app.granblue.team/teams" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content="Discover Teams" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Find different Granblue Fantasy teams by raid, element or recency"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<FilterBar
|
||||
onFilter={receiveFilters}
|
||||
scrolled={scrolled}
|
||||
element={element}
|
||||
raidSlug={raidSlug ? raidSlug : undefined}
|
||||
recency={recency}
|
||||
>
|
||||
<h1>{t("teams.title")}</h1>
|
||||
</FilterBar>
|
||||
|
||||
<section>
|
||||
<InfiniteScroll
|
||||
dataLength={parties && parties.length > 0 ? parties.length : 0}
|
||||
next={() => setCurrentPage(currentPage + 1)}
|
||||
hasMore={totalPages > currentPage}
|
||||
loader={
|
||||
<div id="NotFound">
|
||||
<h2>Loading...</h2>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GridRepCollection>{renderParties()}</GridRepCollection>
|
||||
</InfiniteScroll>
|
||||
|
||||
{parties.length == 0 ? (
|
||||
<div id="NotFound">
|
||||
<h2>{t("teams.not_found")}</h2>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TeamsRoute
|
||||
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 {
|
||||
props: {
|
||||
teams: response.data,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const organizeRaids = (raids: Raid[]) => {
|
||||
// Set up empty raid for "All raids"
|
||||
const all = {
|
||||
id: "0",
|
||||
name: {
|
||||
en: "All raids",
|
||||
ja: "全て",
|
||||
},
|
||||
slug: "all",
|
||||
level: 0,
|
||||
group: 0,
|
||||
element: 0,
|
||||
}
|
||||
|
||||
const numGroups = Math.max.apply(
|
||||
Math,
|
||||
raids.map((raid) => raid.group)
|
||||
)
|
||||
let groupedRaids = []
|
||||
|
||||
for (let i = 0; i <= numGroups; i++) {
|
||||
groupedRaids[i] = raids.filter((raid) => raid.group == i)
|
||||
}
|
||||
|
||||
return {
|
||||
raids: raids,
|
||||
sortedRaids: groupedRaids,
|
||||
}
|
||||
}
|
||||
|
||||
export default TeamsRoute
|
||||
|
|
|
|||
|
|
@ -1,242 +1,248 @@
|
|||
{
|
||||
"ax": {
|
||||
"no_skill": "No AX Skill",
|
||||
"errors": {
|
||||
"value_too_low": "{{name}} must be at least {{minValue}}{{suffix}}",
|
||||
"value_too_high": "{{name}} cannot be greater than {{maxValue}}{{suffix}}",
|
||||
"value_not_whole": "{{name}} must be a whole number",
|
||||
"value_empty": "{{name}} must have a value"
|
||||
}
|
||||
"ax": {
|
||||
"no_skill": "No AX Skill",
|
||||
"errors": {
|
||||
"value_too_low": "{{name}} must be at least {{minValue}}{{suffix}}",
|
||||
"value_too_high": "{{name}} cannot be greater than {{maxValue}}{{suffix}}",
|
||||
"value_not_whole": "{{name}} must be a whole number",
|
||||
"value_empty": "{{name}} must have a value"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Cancel",
|
||||
"copy": "Copy link",
|
||||
"delete": "Delete team",
|
||||
"show_info": "Edit info",
|
||||
"hide_info": "Hide info",
|
||||
"save_info": "Save info",
|
||||
"menu": "Menu",
|
||||
"new": "New",
|
||||
"wiki": "View more on gbf.wiki"
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "Element",
|
||||
"series": "Series",
|
||||
"proficiency": "Proficiency",
|
||||
"rarity": "Rarity"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"anonymous": "Anonymous",
|
||||
"untitled_team": "Untitled team by {{username}}",
|
||||
"new_team": "New team",
|
||||
"byline": "{{partyName}} by {{username}}"
|
||||
},
|
||||
"rarities": {
|
||||
"sr": "SR",
|
||||
"ssr": "SSR"
|
||||
},
|
||||
"elements": {
|
||||
"null": "Null",
|
||||
"wind": "Wind",
|
||||
"fire": "Fire",
|
||||
"water": "Water",
|
||||
"earth": "Earth",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"full": {
|
||||
"all": "All elements",
|
||||
"null": "Null",
|
||||
"wind": "Wind",
|
||||
"fire": "Fire",
|
||||
"water": "Water",
|
||||
"earth": "Earth",
|
||||
"dark": "Dark",
|
||||
"light": "Light"
|
||||
}
|
||||
},
|
||||
"proficiencies": {
|
||||
"sabre": "Sabre",
|
||||
"dagger": "Dagger",
|
||||
"spear": "Spear",
|
||||
"axe": "Axe",
|
||||
"staff": "Staff",
|
||||
"gun": "Gun",
|
||||
"melee": "Melee",
|
||||
"bow": "Bow",
|
||||
"harp": "Harp",
|
||||
"katana": "Katana"
|
||||
},
|
||||
"series": {
|
||||
"seraphic": "Seraphic",
|
||||
"grand": "Grand",
|
||||
"opus": "Dark Opus",
|
||||
"draconic": "Draconic",
|
||||
"primal": "Primal",
|
||||
"olden_primal": "Olden Primal",
|
||||
"beast": "Beast",
|
||||
"omega": "Omega",
|
||||
"militis": "Militis",
|
||||
"xeno": "Xeno",
|
||||
"astral": "Astral",
|
||||
"rose": "Rose",
|
||||
"hollowsky": "Hollowsky",
|
||||
"ultima": "Ultima",
|
||||
"bahamut": "Bahamut",
|
||||
"epic": "Epic",
|
||||
"ennead": "Ennead",
|
||||
"cosmos": "Cosmos",
|
||||
"ancestral": "Ancestral",
|
||||
"superlative": "Superlative",
|
||||
"vintage": "Vintage",
|
||||
"class_champion": "Class Champion",
|
||||
"sephira": "Sephira",
|
||||
"new_world": "New World Foundation"
|
||||
},
|
||||
"recency": {
|
||||
"all_time": "All time",
|
||||
"last_day": "Last day",
|
||||
"last_week": "Last week",
|
||||
"last_month": "Last month",
|
||||
"last_3_months": "Last 3 months",
|
||||
"last_6_months": "Last 6 months",
|
||||
"last_year": "Last year"
|
||||
},
|
||||
"summons": {
|
||||
"main": "Main Summon",
|
||||
"friend": "Friend Summon",
|
||||
"summons": "Summons",
|
||||
"subaura": "Sub Aura Summons"
|
||||
},
|
||||
"modals": {
|
||||
"about": {
|
||||
"title": "About"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Cancel",
|
||||
"copy": "Copy link",
|
||||
"delete": "Delete team",
|
||||
"show_info": "Edit info",
|
||||
"hide_info": "Hide info",
|
||||
"save_info": "Save info",
|
||||
"menu": "Menu",
|
||||
"new": "New",
|
||||
"wiki": "View more on gbf.wiki"
|
||||
"delete_team": {
|
||||
"title": "Delete team",
|
||||
"description": "Are you sure you want to permanently delete this team?",
|
||||
"buttons": {
|
||||
"confirm": "Yes, delete",
|
||||
"cancel": "Nevermind"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "Element",
|
||||
"series": "Series",
|
||||
"proficiency": "Proficiency",
|
||||
"rarity": "Rarity"
|
||||
}
|
||||
"login": {
|
||||
"title": "Log in",
|
||||
"buttons": {
|
||||
"confirm": "Log in"
|
||||
},
|
||||
"errors": {
|
||||
"empty_email": "Please enter your email",
|
||||
"empty_password": "Please enter your password",
|
||||
"invalid_email": "That email address is not valid",
|
||||
"invalid_credentials": "Your email address or password is incorrect"
|
||||
},
|
||||
"placeholders": {
|
||||
"email": "Email address",
|
||||
"password": "Password"
|
||||
}
|
||||
},
|
||||
"rarities": {
|
||||
"sr": "SR",
|
||||
"ssr": "SSR"
|
||||
},
|
||||
"elements": {
|
||||
"null": "Null",
|
||||
"wind": "Wind",
|
||||
"fire": "Fire",
|
||||
"water": "Water",
|
||||
"earth": "Earth",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"full": {
|
||||
"all": "All elements",
|
||||
"null": "Null",
|
||||
"wind": "Wind",
|
||||
"fire": "Fire",
|
||||
"water": "Water",
|
||||
"earth": "Earth",
|
||||
"dark": "Dark",
|
||||
"light": "Light"
|
||||
}
|
||||
},
|
||||
"proficiencies": {
|
||||
"sabre": "Sabre",
|
||||
"dagger": "Dagger",
|
||||
"spear": "Spear",
|
||||
"axe": "Axe",
|
||||
"staff": "Staff",
|
||||
"gun": "Gun",
|
||||
"melee": "Melee",
|
||||
"bow": "Bow",
|
||||
"harp": "Harp",
|
||||
"katana": "Katana"
|
||||
},
|
||||
"series": {
|
||||
"seraphic": "Seraphic",
|
||||
"grand": "Grand",
|
||||
"opus": "Dark Opus",
|
||||
"draconic": "Draconic",
|
||||
"primal": "Primal",
|
||||
"olden_primal": "Olden Primal",
|
||||
"beast": "Beast",
|
||||
"omega": "Omega",
|
||||
"militis": "Militis",
|
||||
"xeno": "Xeno",
|
||||
"astral": "Astral",
|
||||
"rose": "Rose",
|
||||
"hollowsky": "Hollowsky",
|
||||
"ultima": "Ultima",
|
||||
"bahamut": "Bahamut",
|
||||
"epic": "Epic",
|
||||
"ennead": "Ennead",
|
||||
"cosmos": "Cosmos",
|
||||
"ancestral": "Ancestral",
|
||||
"superlative": "Superlative",
|
||||
"vintage": "Vintage",
|
||||
"class_champion": "Class Champion",
|
||||
"sephira": "Sephira",
|
||||
"new_world": "New World Foundation"
|
||||
},
|
||||
"recency": {
|
||||
"all_time": "All time",
|
||||
"last_day": "Last day",
|
||||
"last_week": "Last week",
|
||||
"last_month": "Last month",
|
||||
"last_3_months": "Last 3 months",
|
||||
"last_6_months": "Last 6 months",
|
||||
"last_year": "Last year"
|
||||
},
|
||||
"summons": {
|
||||
"main": "Main Summon",
|
||||
"friend": "Friend Summon",
|
||||
"summons": "Summons",
|
||||
"subaura": "Sub Aura Summons"
|
||||
},
|
||||
"modals": {
|
||||
"about": {
|
||||
"title": "About"
|
||||
},
|
||||
"delete_team": {
|
||||
"title": "Delete team",
|
||||
"description": "Are you sure you want to permanently delete this team?",
|
||||
"buttons": {
|
||||
"confirm": "Yes, delete",
|
||||
"cancel": "Nevermind"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Log in",
|
||||
"buttons": {
|
||||
"confirm": "Log in"
|
||||
},
|
||||
"errors": {
|
||||
"empty_email": "Please enter your email",
|
||||
"empty_password": "Please enter your password",
|
||||
"invalid_email": "That email address is not valid",
|
||||
"invalid_credentials": "Your email address or password is incorrect"
|
||||
},
|
||||
"placeholders": {
|
||||
"email": "Email address",
|
||||
"password": "Password"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Account Settings",
|
||||
"labels": {
|
||||
"picture": "Picture",
|
||||
"language": "Language",
|
||||
"gender": "Main Character",
|
||||
"private": "Private"
|
||||
},
|
||||
"descriptions": {
|
||||
"private": "Hide your profile and prevent your grids from showing up in collections"
|
||||
},
|
||||
"gender": {
|
||||
"gran": "Gran",
|
||||
"djeeta": "Djeeta"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"japanese": "Japanese"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "Save settings"
|
||||
}
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
"buttons": {
|
||||
"confirm": "Sign up"
|
||||
},
|
||||
"agreement": "By signing up, I agree to the <br/><2>Privacy Policy</2> and <1>Usage Guidelines</1>.",
|
||||
"errors": {
|
||||
"field_in_use": "This {{field}} is already in use",
|
||||
"empty_email": "Please enter your email",
|
||||
"invalid_email": "That email address is not valid",
|
||||
"username_too_short": "Username must be at least 3 characters",
|
||||
"username_too_long": "Username must be less than 20 characters",
|
||||
"empty_password": "Please enter your password",
|
||||
"password_contains_username": "Your password should not contain your username",
|
||||
"password_too_short": "Password must be at least 8 characters",
|
||||
"mismatched_passwords": "Your passwords don't match"
|
||||
},
|
||||
"placeholders": {
|
||||
"username": "Username",
|
||||
"email": "Email address",
|
||||
"password": "Password",
|
||||
"password_confirm": "Password (again)"
|
||||
}
|
||||
},
|
||||
"weapon": {
|
||||
"title": "Modify Weapon",
|
||||
"buttons": {
|
||||
"confirm": "Save weapon"
|
||||
},
|
||||
"subtitles": {
|
||||
"element": "Element",
|
||||
"ax_skills": "AX Skills",
|
||||
"weapon_keys": "Weapon Keys"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"about": "About",
|
||||
"guides": "Guides",
|
||||
"settings": {
|
||||
"title": "Account Settings",
|
||||
"labels": {
|
||||
"picture": "Picture",
|
||||
"language": "Language",
|
||||
"login": "Log in",
|
||||
"saved": "Saved",
|
||||
"settings": "Settings",
|
||||
"signup": "Sign up",
|
||||
"teams": "Teams",
|
||||
"logout": "Logout"
|
||||
"gender": "Main Character",
|
||||
"private": "Private"
|
||||
},
|
||||
"descriptions": {
|
||||
"private": "Hide your profile and prevent your grids from showing up in collections"
|
||||
},
|
||||
"gender": {
|
||||
"gran": "Gran",
|
||||
"djeeta": "Djeeta"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"japanese": "Japanese"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "Save settings"
|
||||
}
|
||||
},
|
||||
"party": {
|
||||
"segmented_control": {
|
||||
"class": "Class",
|
||||
"characters": "Characters",
|
||||
"weapons": "Weapons",
|
||||
"summons": "Summons"
|
||||
}
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
"buttons": {
|
||||
"confirm": "Sign up"
|
||||
},
|
||||
"agreement": "By signing up, I agree to the <br/><2>Privacy Policy</2> and <1>Usage Guidelines</1>.",
|
||||
"errors": {
|
||||
"field_in_use": "This {{field}} is already in use",
|
||||
"empty_email": "Please enter your email",
|
||||
"invalid_email": "That email address is not valid",
|
||||
"username_too_short": "Username must be at least 3 characters",
|
||||
"username_too_long": "Username must be less than 20 characters",
|
||||
"empty_password": "Please enter your password",
|
||||
"password_contains_username": "Your password should not contain your username",
|
||||
"password_too_short": "Password must be at least 8 characters",
|
||||
"mismatched_passwords": "Your passwords don't match"
|
||||
},
|
||||
"placeholders": {
|
||||
"username": "Username",
|
||||
"email": "Email address",
|
||||
"password": "Password",
|
||||
"password_confirm": "Password (again)"
|
||||
}
|
||||
},
|
||||
"saved": {
|
||||
"title": "Your saved Teams",
|
||||
"loading": "Loading saved teams...",
|
||||
"not_found": "You haven't saved any teams"
|
||||
"weapon": {
|
||||
"title": "Modify Weapon",
|
||||
"buttons": {
|
||||
"confirm": "Save weapon"
|
||||
},
|
||||
"subtitles": {
|
||||
"element": "Element",
|
||||
"ax_skills": "AX Skills",
|
||||
"weapon_keys": "Weapon Keys"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"about": "About",
|
||||
"guides": "Guides",
|
||||
"language": "Language",
|
||||
"login": "Log in",
|
||||
"saved": "Saved",
|
||||
"settings": "Settings",
|
||||
"signup": "Sign up",
|
||||
"teams": "Teams",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"party": {
|
||||
"segmented_control": {
|
||||
"class": "Class",
|
||||
"characters": "Characters",
|
||||
"weapons": "Weapons",
|
||||
"summons": "Summons"
|
||||
}
|
||||
},
|
||||
"saved": {
|
||||
"title": "Your saved Teams",
|
||||
"loading": "Loading saved teams...",
|
||||
"not_found": "You haven't saved any teams"
|
||||
},
|
||||
"search": {
|
||||
"recent": "Recently added",
|
||||
"result_count": "{{record_count}} results",
|
||||
"errors": {
|
||||
"start_typing": "Start typing the name of a {{object}}",
|
||||
"min_length": "Type at least 3 characters",
|
||||
"no_results": "No results found for '{{query}}'",
|
||||
"end_results": "No more results"
|
||||
},
|
||||
"search": {
|
||||
"recent": "Recently added",
|
||||
"result_count": "{{record_count}} results",
|
||||
"errors": {
|
||||
"start_typing": "Start typing the name of a {{object}}",
|
||||
"min_length": "Type at least 3 characters",
|
||||
"no_results": "No results found for '{{query}}'",
|
||||
"end_results": "No more results"
|
||||
},
|
||||
"placeholders": {
|
||||
"weapon": "Search for a weapon...",
|
||||
"summon": "Search for a summon...",
|
||||
"character": "Search for a character..."
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
"title": "Discover Teams",
|
||||
"loading": "Loading teams...",
|
||||
"not_found": "No teams found"
|
||||
},
|
||||
"extra_weapons": "Additional Weapons",
|
||||
"coming_soon": "Coming Soon",
|
||||
"no_title": "Untitled",
|
||||
"no_raid": "No raid",
|
||||
"no_user": "Anonymous"
|
||||
"placeholders": {
|
||||
"weapon": "Search for a weapon...",
|
||||
"summon": "Search for a summon...",
|
||||
"character": "Search for a character..."
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
"title": "Discover Teams",
|
||||
"loading": "Loading teams...",
|
||||
"not_found": "No teams found"
|
||||
},
|
||||
"extra_weapons": "Additional Weapons",
|
||||
"coming_soon": "Coming Soon",
|
||||
"no_title": "Untitled",
|
||||
"no_raid": "No raid",
|
||||
"no_user": "Anonymous"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,244 +1,249 @@
|
|||
{
|
||||
"ax": {
|
||||
"no_skill": "EXスキルなし",
|
||||
"errors": {
|
||||
"value_too_low": "{{name}}は最低{{minValue}}{{suffix}}を入力してください",
|
||||
"value_too_high": "{{name}}は最大{{maxValue}}を入力してください",
|
||||
"value_not_whole": "{{name}}は整数でなければなりません",
|
||||
"value_empty": "{{name}}を入力してください"
|
||||
}
|
||||
"ax": {
|
||||
"no_skill": "EXスキルなし",
|
||||
"errors": {
|
||||
"value_too_low": "{{name}}は最低{{minValue}}{{suffix}}を入力してください",
|
||||
"value_too_high": "{{name}}は最大{{maxValue}}を入力してください",
|
||||
"value_not_whole": "{{name}}は整数でなければなりません",
|
||||
"value_empty": "{{name}}を入力してください"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "キャンセルs",
|
||||
"copy": "リンクをコピー",
|
||||
"delete": "編成を削除",
|
||||
"show_info": "詳細を編集",
|
||||
"save_info": "詳細を保存",
|
||||
"hide_info": "詳細を非表示",
|
||||
"menu": "メニュー",
|
||||
"new": "作成",
|
||||
"wiki": "gbf.wikiで詳しく見る"
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "属性",
|
||||
"series": "シリーズ",
|
||||
"proficiency": "武器種",
|
||||
"rarity": "レアリティ"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"anonymous": "無名",
|
||||
"untitled_team": "{{username}}さんからの無題編成",
|
||||
"new_team": "新編成",
|
||||
"byline": "{{username}}さんからの{{partyName}}"
|
||||
},
|
||||
"rarities": {
|
||||
"sr": "SR",
|
||||
"ssr": "SSR"
|
||||
},
|
||||
"elements": {
|
||||
"null": "無",
|
||||
"wind": "風",
|
||||
"fire": "火",
|
||||
"water": "水",
|
||||
"earth": "土",
|
||||
"dark": "闇",
|
||||
"light": "光",
|
||||
"full": {
|
||||
"all": "全属性",
|
||||
"null": "無属性",
|
||||
"wind": "風属性",
|
||||
"fire": "火属性",
|
||||
"water": "水属性",
|
||||
"earth": "土属性",
|
||||
"dark": "闇属性",
|
||||
"light": "光属性"
|
||||
}
|
||||
},
|
||||
"proficiencies": {
|
||||
"sabre": "剣",
|
||||
"dagger": "短剣",
|
||||
"spear": "槍",
|
||||
"axe": "斧",
|
||||
"staff": "杖",
|
||||
"gun": "銃",
|
||||
"melee": "拳",
|
||||
"bow": "弓",
|
||||
"harp": "琴",
|
||||
"katana": "刀"
|
||||
},
|
||||
"series": {
|
||||
"seraphic": "セラフィックウェポン",
|
||||
"grand": "リミテッドシリーズ",
|
||||
"opus": "終末の神器",
|
||||
"draconic": "ドラコニックウェポン",
|
||||
"primal": "プライマルシリーズ",
|
||||
"olden_primal": "オールド・プライマルシリーズ",
|
||||
"beast": "四象武器",
|
||||
"omega": "マグナシリーズ",
|
||||
"militis": "ミーレスシリーズ",
|
||||
"xeno": "六道武器",
|
||||
"astral": "アストラルウェポン",
|
||||
"rose": "ローズシリーズ",
|
||||
"hollowsky": "虚ろなる神器",
|
||||
"ultima": "オメガウェポン",
|
||||
"bahamut": "バハムートウェポン",
|
||||
"epic": "エピックウェポン",
|
||||
"ennead": "エニアドシリーズ",
|
||||
"cosmos": "コスモスシリーズ",
|
||||
"ancestral": "アンセスタルシリーズ",
|
||||
"superlative": "スペリオシリーズ",
|
||||
"vintage": "ヴィンテージシリーズ",
|
||||
"class_champion": "英雄武器",
|
||||
"sephira": "セフィラン・オールドウェポン",
|
||||
"new_world": "新世界の礎"
|
||||
},
|
||||
"recency": {
|
||||
"all_time": "全ての期間",
|
||||
"last_day": "1日",
|
||||
"last_week": "7日",
|
||||
"last_month": "1ヶ月",
|
||||
"last_3_months": "3ヶ月",
|
||||
"last_6_months": "6ヶ月",
|
||||
"last_year": "1年"
|
||||
},
|
||||
"summons": {
|
||||
"main": "メイン",
|
||||
"friend": "フレンド",
|
||||
"summons": "召喚石",
|
||||
"subaura": "サブ加護召喚石"
|
||||
},
|
||||
"modals": {
|
||||
"about": {
|
||||
"title": "このサイトについて"
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "キャンセルs",
|
||||
"copy": "リンクをコピー",
|
||||
"delete": "編成を削除",
|
||||
"show_info": "詳細を編集",
|
||||
"save_info": "詳細を保存",
|
||||
"hide_info": "詳細を非表示",
|
||||
"menu": "メニュー",
|
||||
"new": "作成",
|
||||
"wiki": "gbf.wikiで詳しく見る"
|
||||
"delete_team": {
|
||||
"title": "編成を削除しますか",
|
||||
"description": "編成を削除する操作は取り消せません。",
|
||||
"buttons": {
|
||||
"confirm": "削除",
|
||||
"cancel": "キャンセル"
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "属性",
|
||||
"series": "シリーズ",
|
||||
"proficiency": "武器種",
|
||||
"rarity": "レアリティ"
|
||||
}
|
||||
"login": {
|
||||
"title": "ログイン",
|
||||
"buttons": {
|
||||
"confirm": "ログイン"
|
||||
},
|
||||
"errors": {
|
||||
"empty_email": "メールアドレスを入力して下さい",
|
||||
"empty_password": "パスワードを入力して下さい",
|
||||
"invalid_email": "メールアドレスは有効ではありません",
|
||||
"invalid_credentials": "パスワードまたはメールアドレスが違います"
|
||||
},
|
||||
"placeholders": {
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード"
|
||||
}
|
||||
},
|
||||
"rarities": {
|
||||
"sr": "SR",
|
||||
"ssr": "SSR"
|
||||
},
|
||||
"elements": {
|
||||
"null": "無",
|
||||
"wind": "風",
|
||||
"fire": "火",
|
||||
"water": "水",
|
||||
"earth": "土",
|
||||
"dark": "闇",
|
||||
"light": "光",
|
||||
"full": {
|
||||
"all": "全属性",
|
||||
"null": "無属性",
|
||||
"wind": "風属性",
|
||||
"fire": "火属性",
|
||||
"water": "水属性",
|
||||
"earth": "土属性",
|
||||
"dark": "闇属性",
|
||||
"light": "光属性"
|
||||
}
|
||||
},
|
||||
"proficiencies": {
|
||||
"sabre": "剣",
|
||||
"dagger": "短剣",
|
||||
"spear": "槍",
|
||||
"axe": "斧",
|
||||
"staff": "杖",
|
||||
"gun": "銃",
|
||||
"melee": "拳",
|
||||
"bow": "弓",
|
||||
"harp": "琴",
|
||||
"katana": "刀"
|
||||
},
|
||||
"series": {
|
||||
"seraphic": "セラフィックウェポン",
|
||||
"grand": "リミテッドシリーズ",
|
||||
"opus": "終末の神器",
|
||||
"draconic": "ドラコニックウェポン",
|
||||
"primal": "プライマルシリーズ",
|
||||
"olden_primal": "オールド・プライマルシリーズ",
|
||||
"beast": "四象武器",
|
||||
"omega": "マグナシリーズ",
|
||||
"militis": "ミーレスシリーズ",
|
||||
"xeno": "六道武器",
|
||||
"astral": "アストラルウェポン",
|
||||
"rose": "ローズシリーズ",
|
||||
"hollowsky": "虚ろなる神器",
|
||||
"ultima": "オメガウェポン",
|
||||
"bahamut": "バハムートウェポン",
|
||||
"epic": "エピックウェポン",
|
||||
"ennead": "エニアドシリーズ",
|
||||
"cosmos": "コスモスシリーズ",
|
||||
"ancestral": "アンセスタルシリーズ",
|
||||
"superlative": "スペリオシリーズ",
|
||||
"vintage": "ヴィンテージシリーズ",
|
||||
"class_champion": "英雄武器",
|
||||
"sephira": "セフィラン・オールドウェポン",
|
||||
"new_world": "新世界の礎"
|
||||
},
|
||||
"recency": {
|
||||
"all_time": "全ての期間",
|
||||
"last_day": "1日",
|
||||
"last_week": "7日",
|
||||
"last_month": "1ヶ月",
|
||||
"last_3_months": "3ヶ月",
|
||||
"last_6_months": "6ヶ月",
|
||||
"last_year": "1年"
|
||||
},
|
||||
"summons": {
|
||||
"main": "メイン",
|
||||
"friend": "フレンド",
|
||||
"summons": "召喚石",
|
||||
"subaura": "サブ加護召喚石"
|
||||
},
|
||||
"modals": {
|
||||
"about": {
|
||||
"title": "このサイトについて"
|
||||
},
|
||||
"delete_team": {
|
||||
"title": "編成を削除しますか",
|
||||
"description": "編成を削除する操作は取り消せません。",
|
||||
"buttons": {
|
||||
"confirm": "削除",
|
||||
"cancel": "キャンセル"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "ログイン",
|
||||
"buttons": {
|
||||
"confirm": "ログイン"
|
||||
},
|
||||
"errors": {
|
||||
"empty_email": "メールアドレスを入力して下さい",
|
||||
"empty_password": "パスワードを入力して下さい",
|
||||
"invalid_email": "メールアドレスは有効ではありません",
|
||||
"invalid_credentials": "パスワードまたはメールアドレスが違います"
|
||||
},
|
||||
"placeholders": {
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "アカウント設定",
|
||||
"labels": {
|
||||
"picture": "プロフィール画像",
|
||||
"language": "言語",
|
||||
"gender": "主人公",
|
||||
"private": "プライベート"
|
||||
},
|
||||
"descriptions": {
|
||||
"private": "プロフィールを隠し、編成をコレクションに表示されないようにします"
|
||||
},
|
||||
"gender": {
|
||||
"gran": "グラン",
|
||||
"djeeta": "ジータ"
|
||||
},
|
||||
"language": {
|
||||
"english": "英語",
|
||||
"japanese": "日本語"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "設定を保存する"
|
||||
}
|
||||
},
|
||||
"signup": {
|
||||
"title": "アカウント登録",
|
||||
"buttons": {
|
||||
"confirm": "登録する"
|
||||
},
|
||||
"agreement": "続行することで<1>利用規約</1>に同意し、<br/><1>プライバシーポリシー</1>を読んだものとみなされます。",
|
||||
"errors": {
|
||||
"field_in_use": "入力された{{field}}は既に登録済みです",
|
||||
"empty_email": "メールアドレスを入力して下さい",
|
||||
"invalid_email": "メールアドレスは有効ではありません",
|
||||
"username_too_short": "ユーザーネームは3文字以上で入力してください",
|
||||
"username_too_long": "ユーザーネームは20文字以内で入力してください",
|
||||
"empty_password": "パスワードを入力して下さい",
|
||||
"password_contains_username": "パスワードにはユーザー名を含めないでください",
|
||||
"password_too_short": "パスワードは8文字以上で入力してください",
|
||||
"mismatched_passwords": "パスワードとパスワード確認を確かめてください",
|
||||
"invalid_credentials": "パスワードまたはメールアドレスが違います"
|
||||
},
|
||||
"placeholders": {
|
||||
"username": "ユーザー名",
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード",
|
||||
"password_confirm": "パスワード確認"
|
||||
}
|
||||
},
|
||||
"weapon": {
|
||||
"title": "武器変更",
|
||||
"buttons": {
|
||||
"confirm": "武器を変更する"
|
||||
},
|
||||
"subtitles": {
|
||||
"element": "属性",
|
||||
"ax_skills": "EXスキル",
|
||||
"weapon_keys": "武器スキル"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"about": "このサイトについて",
|
||||
"guides": "攻略",
|
||||
"settings": {
|
||||
"title": "アカウント設定",
|
||||
"labels": {
|
||||
"picture": "プロフィール画像",
|
||||
"language": "言語",
|
||||
"login": "ログイン",
|
||||
"saved": "保存した編成",
|
||||
"settings": "アカウント設定",
|
||||
"signup": "登録",
|
||||
"teams": "編成一覧",
|
||||
"logout": "ログアウト"
|
||||
"gender": "主人公",
|
||||
"private": "プライベート"
|
||||
},
|
||||
"descriptions": {
|
||||
"private": "プロフィールを隠し、編成をコレクションに表示されないようにします"
|
||||
},
|
||||
"gender": {
|
||||
"gran": "グラン",
|
||||
"djeeta": "ジータ"
|
||||
},
|
||||
"language": {
|
||||
"english": "英語",
|
||||
"japanese": "日本語"
|
||||
},
|
||||
"buttons": {
|
||||
"confirm": "設定を保存する"
|
||||
}
|
||||
},
|
||||
"party": {
|
||||
"segmented_control": {
|
||||
"class": "ジョブ",
|
||||
"characters": "キャラ",
|
||||
"weapons": "武器",
|
||||
"summons": "召喚石"
|
||||
}
|
||||
"signup": {
|
||||
"title": "アカウント登録",
|
||||
"buttons": {
|
||||
"confirm": "登録する"
|
||||
},
|
||||
"agreement": "続行することで<1>利用規約</1>に同意し、<br/><1>プライバシーポリシー</1>を読んだものとみなされます。",
|
||||
"errors": {
|
||||
"field_in_use": "入力された{{field}}は既に登録済みです",
|
||||
"empty_email": "メールアドレスを入力して下さい",
|
||||
"invalid_email": "メールアドレスは有効ではありません",
|
||||
"username_too_short": "ユーザーネームは3文字以上で入力してください",
|
||||
"username_too_long": "ユーザーネームは20文字以内で入力してください",
|
||||
"empty_password": "パスワードを入力して下さい",
|
||||
"password_contains_username": "パスワードにはユーザー名を含めないでください",
|
||||
"password_too_short": "パスワードは8文字以上で入力してください",
|
||||
"mismatched_passwords": "パスワードとパスワード確認を確かめてください",
|
||||
"invalid_credentials": "パスワードまたはメールアドレスが違います"
|
||||
},
|
||||
"placeholders": {
|
||||
"username": "ユーザー名",
|
||||
"email": "メールアドレス",
|
||||
"password": "パスワード",
|
||||
"password_confirm": "パスワード確認"
|
||||
}
|
||||
},
|
||||
"saved": {
|
||||
"title": "保存した編成",
|
||||
"loading": "ロード中...",
|
||||
"not_found": "編成はまだ保存していません"
|
||||
"weapon": {
|
||||
"title": "武器変更",
|
||||
"buttons": {
|
||||
"confirm": "武器を変更する"
|
||||
},
|
||||
"subtitles": {
|
||||
"element": "属性",
|
||||
"ax_skills": "EXスキル",
|
||||
"weapon_keys": "武器スキル"
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"about": "このサイトについて",
|
||||
"guides": "攻略",
|
||||
"language": "言語",
|
||||
"login": "ログイン",
|
||||
"saved": "保存した編成",
|
||||
"settings": "アカウント設定",
|
||||
"signup": "登録",
|
||||
"teams": "編成一覧",
|
||||
"logout": "ログアウト"
|
||||
},
|
||||
"party": {
|
||||
"segmented_control": {
|
||||
"class": "ジョブ",
|
||||
"characters": "キャラ",
|
||||
"weapons": "武器",
|
||||
"summons": "召喚石"
|
||||
}
|
||||
},
|
||||
"saved": {
|
||||
"title": "保存した編成",
|
||||
"loading": "ロード中...",
|
||||
"not_found": "編成はまだ保存していません"
|
||||
},
|
||||
"search": {
|
||||
"recent": "最近追加した",
|
||||
"result_count": "{{record_count}}件",
|
||||
"errors": {
|
||||
"start_typing": "{{object}}名を入力してください",
|
||||
"min_length": "3文字以上を入力してください",
|
||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||
"end_results": "検索結果これ以上ありません"
|
||||
},
|
||||
"search": {
|
||||
"recent": "最近追加した",
|
||||
"result_count": "{{record_count}}件",
|
||||
"errors": {
|
||||
"start_typing": "{{object}}名を入力してください",
|
||||
"min_length": "3文字以上を入力してください",
|
||||
"no_results": "'{{query}}'の検索結果が見つかりませんでした",
|
||||
"end_results": "検索結果これ以上ありません"
|
||||
},
|
||||
"placeholders": {
|
||||
"weapon": "武器を検索...",
|
||||
"summon": "召喚石を検索...",
|
||||
"character": "キャラを検索..."
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
"title": "編成一覧",
|
||||
"loading": "ロード中...",
|
||||
"not_found": "編成は見つかりませんでした"
|
||||
},
|
||||
"extra_weapons": "Additional<br/>Weapons",
|
||||
"coming_soon": "開発中",
|
||||
"no_title": "無題",
|
||||
"no_raid": "マルチなし",
|
||||
"no_user": "無名"
|
||||
"placeholders": {
|
||||
"weapon": "武器を検索...",
|
||||
"summon": "召喚石を検索...",
|
||||
"character": "キャラを検索..."
|
||||
}
|
||||
},
|
||||
"teams": {
|
||||
"title": "編成一覧",
|
||||
"loading": "ロード中...",
|
||||
"not_found": "編成は見つかりませんでした"
|
||||
},
|
||||
"extra_weapons": "Additional<br/>Weapons",
|
||||
"coming_soon": "開発中",
|
||||
"no_title": "無題",
|
||||
"no_raid": "マルチなし",
|
||||
"no_user": "無名"
|
||||
}
|
||||
|
||||
5
types/AccountCookie.d.ts
vendored
Normal file
5
types/AccountCookie.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
interface AccountCookie {
|
||||
userId: string
|
||||
username: string
|
||||
token: string
|
||||
}
|
||||
27
types/Party.d.ts
vendored
27
types/Party.d.ts
vendored
|
|
@ -1,14 +1,15 @@
|
|||
interface Party {
|
||||
id: string
|
||||
name: string
|
||||
raid: Raid
|
||||
shortcode: string
|
||||
extra: boolean
|
||||
favorited: boolean
|
||||
characters: Array<GridCharacter>
|
||||
weapons: Array<GridWeapon>
|
||||
summons: Array<GridSummon>
|
||||
user: User
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
raid: Raid
|
||||
shortcode: string
|
||||
extra: boolean
|
||||
favorited: boolean
|
||||
characters: Array<GridCharacter>
|
||||
weapons: Array<GridWeapon>
|
||||
summons: Array<GridSummon>
|
||||
user: User
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
|
|
|||
6
types/UserCookie.d.ts
vendored
Normal file
6
types/UserCookie.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
interface UserCookie {
|
||||
picture: string
|
||||
element: string
|
||||
language: string
|
||||
gender: number
|
||||
}
|
||||
|
|
@ -1,130 +1,130 @@
|
|||
interface RaidGroup {
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
}
|
||||
|
||||
export const raidGroups: RaidGroup[] = [
|
||||
{
|
||||
name: {
|
||||
en: 'Assorted',
|
||||
ja: 'その他'
|
||||
}
|
||||
{
|
||||
name: {
|
||||
en: "Assorted",
|
||||
ja: "その他",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Guild Wars',
|
||||
ja: '星の古戦場'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Guild Wars",
|
||||
ja: "星の古戦場",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Omega',
|
||||
ja: 'マグナ'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Omega",
|
||||
ja: "マグナ",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'T1 Summons',
|
||||
ja: '召喚石マルチ1(旧)'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "T1 Summons",
|
||||
ja: "召喚石マルチ1(旧)",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'T2 Summons',
|
||||
ja: '召喚石マルチ2(新)'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "T2 Summons",
|
||||
ja: "召喚石マルチ2(新)",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Primarchs',
|
||||
ja: '四大天使'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Primarchs",
|
||||
ja: "四大天使",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Nightmare',
|
||||
ja: 'HELL'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Nightmare",
|
||||
ja: "HELL",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Omega (Impossible)',
|
||||
ja: 'マグナHL'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Omega (Impossible)",
|
||||
ja: "マグナHL",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Omega II',
|
||||
ja: 'マグナII'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Omega II",
|
||||
ja: "マグナII",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Tier 1 Summons (Impossible)',
|
||||
ja: '旧召喚石HL'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Tier 1 Summons (Impossible)",
|
||||
ja: "旧召喚石HL",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Tier 3 Summons',
|
||||
ja: 'エピックHL'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Tier 3 Summons",
|
||||
ja: "エピックHL",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Ennead',
|
||||
ja: 'エニアド'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Ennead",
|
||||
ja: "エニアド",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Malice',
|
||||
ja: 'マリス'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Malice",
|
||||
ja: "マリス",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: '6-Star Raids',
|
||||
ja: '★★★★★★'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "6-Star Raids",
|
||||
ja: "★★★★★★",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Six-Dragons',
|
||||
ja: '六竜HL'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Six-Dragons",
|
||||
ja: "六竜HL",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Nightmare (Impossible)',
|
||||
ja: '高級HELL'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Nightmare (Impossible)",
|
||||
ja: "高級HELL",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Arcarum: Replicard Sandbox',
|
||||
ja: 'アーカルム レプリカルド・サンドボックス'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Arcarum: Replicard Sandbox",
|
||||
ja: "アーカルム レプリカルド・サンドボックス",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Astrals',
|
||||
ja: '星の民'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Astrals",
|
||||
ja: "星の民",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: '10-Star Raids',
|
||||
ja: '★★★★★★★★★★'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Disaster",
|
||||
ja: "災害",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: 'Super Ultimate',
|
||||
ja: 'スーパーアルティメット'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Super Ultimate",
|
||||
ja: "スーパーアルティメット",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
|||
12
utils/useDidMountEffect.tsx
Normal file
12
utils/useDidMountEffect.tsx
Normal 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
|
||||
Loading…
Reference in a new issue