diff --git a/components/AccountModal/index.tsx b/components/AccountModal/index.tsx index 9dc64075..6b081b20 100644 --- a/components/AccountModal/index.tsx +++ b/components/AccountModal/index.tsx @@ -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() - const languageSelect = React.createRef() - const genderSelect = React.createRef() - const privateSelect = React.createRef() + // 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() + const languageSelect = React.createRef() + const genderSelect = React.createRef() + const privateSelect = React.createRef() - const pictureOptions = ( - pictureData.sort((a, b) => (a.name.en > b.name.en) ? 1 : -1).map((item, i) => { - return ( - - ) - }) - ) + // 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) { - 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 ( + + ) + }) + + function handlePictureChange(event: React.ChangeEvent) { + if (pictureSelect.current) setPicture(pictureSelect.current.value) + } + + function handleLanguageChange(event: React.ChangeEvent) { + if (languageSelect.current) setLanguage(languageSelect.current.value) + } + + function handleGenderChange(event: React.ChangeEvent) { + if (genderSelect.current) setGender(parseInt(genderSelect.current.value)) + } + + function handlePrivateChange(checked: boolean) { + setPrivateProfile(checked) + } + + function update(event: React.FormEvent) { + 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) { - 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) { - 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) { - 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 ( + + +
  • + {t("menu.settings")} +
  • +
    + + event.preventDefault()} + > +
    +
    + + {t("modals.settings.title")} + + + @{account.user?.username} + +
    + + + + + +
    - 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) - } - - return ( - - -
  • - {t('menu.settings')} -
  • -
    - - event.preventDefault() }> -
    -
    - {t('modals.settings.title')} - @{account.user?.username} -
    - - - - - -
    - - -
    -
    - -
    - -
    i.filename === picture)?.element}`}> - Profile preview i.filename === picture)?.element + }`} + > + Profile preview -
    + src={`/profile/${picture}.png`} + /> +
    - -
    -
    -
    - -
    + +
    +
    +
    + +
    - -
    -
    -
    - -
    + +
    +
    +
    + +
    - -
    -
    -
    - -

    {t('modals.settings.descriptions.private')}

    -
    + +
    +
    +
    + +

    + {t("modals.settings.descriptions.private")} +

    +
    - - - -
    + + + + - -
    -
    - -
    -
    - ) + + + + + + + ) } export default AccountModal diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index d4d31ae0..ebfb9115 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -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> - pushHistory?: (path: string) => void + new: boolean + characters?: GridCharacter[] + createParty: () => Promise> + 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) { - 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 ( -
    -
    - -
      - {Array.from(Array(numCharacters)).map((x, i) => { - return ( -
    • - -
    • - ) - })} -
    -
    -
    - ) + // Render: JSX components + return ( +
    +
    + +
      + {Array.from(Array(numCharacters)).map((x, i) => { + return ( +
    • + +
    • + ) + })} +
    +
    +
    + ) } export default CharacterGrid diff --git a/components/GridRep/index.tsx b/components/GridRep/index.tsx index 8f07209b..b43380bb 100644 --- a/components/GridRep/index.tsx +++ b/components/GridRep/index.tsx @@ -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() - const [weapons, setWeapons] = useState>({}) + const [mainhand, setMainhand] = useState() + const [weapons, setWeapons] = useState>({}) - 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) ? - {mainhand.name[locale]} : '' + 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] ? ( + {mainhand.name[locale]} + ) : ( + "" + ) + } - 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]) ? - {weapons[position].name[locale]} : '' + 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] ? ( + {weapons[position].name[locale]} + ) : ( + "" + ) + } - const userImage = () => { - if (props.user) - return ( - {props.user.picture.picture} { + if (props.user) + return ( + {props.user.picture.picture} + src={`/profile/${props.user.picture.picture}.png`} + /> + ) + else return
    + } + + const details = ( +
    +

    + {props.name ? props.name : t("no_title")} +

    +
    +
    + {props.raid ? props.raid.name[locale] : t("no_raid")} +
    + +
    +
    + ) + + const detailsWithUsername = ( +
    +
    +
    +

    + {props.name ? props.name : t("no_title")} +

    +
    + {props.raid ? props.raid.name[locale] : t("no_raid")} +
    +
    + {account.authorized && + ((props.user && account.user && account.user.id !== props.user.id) || + !props.user) ? ( +
    +
    +
    + {userImage()} + {props.user ? props.user.username : t("no_user")} +
    + +
    +
    + ) + + return ( +
    + {props.displayUser ? detailsWithUsername : details} +
    +
    {generateMainhandImage()}
    + +
      + {Array.from(Array(numWeapons)).map((x, i) => { + return ( +
    • + {generateGridImage(i)} +
    • ) - else - return (
      ) - } - - const details = ( -
      -

      { (props.name) ? props.name : t('no_title') }

      -
      -
      { (props.raid) ? props.raid.name[locale] : t('no_raid') }
      - -
      -
      - ) - - const detailsWithUsername = ( -
      -
      -
      -

      { (props.name) ? props.name : t('no_title') }

      -
      { (props.raid) ? props.raid.name[locale] : t('no_raid') }
      -
      - { - (account.authorized && ( - (props.user && account.user && account.user.id !== props.user.id) - || (!props.user) - )) ? -
      -
      -
      - { userImage() } - { (props.user) ? props.user.username : t('no_user') } -
      - -
      -
      - ) - - return ( -
      - { (props.displayUser) ? detailsWithUsername : details} -
      -
      - {generateMainhandImage()} -
      - -
        - { - Array.from(Array(numWeapons)).map((x, i) => { - return ( -
      • - {generateGridImage(i)} -
      • - ) - }) - } -
      -
      -
      - ) + })} +
    +
    +
    + ) } export default GridRep diff --git a/components/GridRepCollection/index.scss b/components/GridRepCollection/index.scss index f2752274..1f0a2db3 100644 --- a/components/GridRepCollection/index.scss +++ b/components/GridRepCollection/index.scss @@ -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; - } -} \ No newline at end of file + 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; +} diff --git a/components/GridRepCollection/index.tsx b/components/GridRepCollection/index.tsx index 041fad4b..77592ffd 100644 --- a/components/GridRepCollection/index.tsx +++ b/components/GridRepCollection/index.tsx @@ -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 ( -
    - {props.children} -
    - ) + const classes = classNames({ + GridRepCollection: true, + }) + + return
    {props.children}
    } export default GridRepCollection diff --git a/components/HeaderMenu/index.tsx b/components/HeaderMenu/index.tsx index 7fe750ba..622c8c2a 100644 --- a/components/HeaderMenu/index.tsx +++ b/components/HeaderMenu/index.tsx @@ -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 ( - - ) - } + useEffect(() => { + const locale = localeCookie + setChecked(locale === "ja" ? true : false) + }, [localeCookie]) - function unauthItems() { - return ( -
      -
      -
    • - {t('menu.language')} - - - JP - EN - -
    • + function handleCheckedChange(value: boolean) { + const language = value ? "ja" : "en" + setCookie("NEXT_LOCALE", language, { path: "/" }) + router.push(router.asPath, undefined, { locale: language }) + } + + function authItems() { + return ( +
      +
      + + +
    • + {t("menu.logout")} +
    • +
      +
    + + ) + } - return (props.authenticated) ? authItems() : unauthItems() + function unauthItems() { + return ( +
      +
      +
    • + {t("menu.language")} + + + JP + EN + +
    • +
      +
      +
    • + {t("menu.teams")} +
    • + +
    • +
      + {t("menu.guides")} + {t("coming_soon")} +
      +
    • +
      +
      + +
      +
      + + +
      +
    + ) + } + + return props.authenticated ? authItems() : unauthItems() } -export default HeaderMenu \ No newline at end of file +export default HeaderMenu diff --git a/components/LoginModal/index.tsx b/components/LoginModal/index.tsx index 8d83ec13..de5f5443 100644 --- a/components/LoginModal/index.tsx +++ b/components/LoginModal/index.tsx @@ -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({ - email: '', - password: '' - }) + // Set up form states and error handling + const [formValid, setFormValid] = useState(false) + const [errors, setErrors] = useState({ + 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 = React.createRef() + const passwordInput: React.RefObject = React.createRef() + const form: React.RefObject[] = [emailInput, passwordInput] - // Set up form refs - const emailInput: React.RefObject = React.createRef() - const passwordInput: React.RefObject = React.createRef() - const form: React.RefObject[] = [emailInput, passwordInput] + function handleChange(event: React.ChangeEvent) { + const { name, value } = event.target + let newErrors = { ...errors } - function handleChange(event: React.ChangeEvent) { - 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 ( - - -
  • - {t('menu.login')} -
  • -
    - - event.preventDefault() }> -
    - {t('modals.login.title')} - - - - - -
    - -
    -
    - -
    - - - - - - - + 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 ( + + +
  • + {t("menu.login")} +
  • +
    + + event.preventDefault()} + > +
    + + {t("modals.login.title")} + + + + + + +
    + +
    +
    + +
    + + + + + + + + ) } -export default LoginModal \ No newline at end of file +export default LoginModal diff --git a/components/Party/index.tsx b/components/Party/index.tsx index eb5a0695..2530de64 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -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() - const [currentTab, setCurrentTab] = useState(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() + const [currentTab, setCurrentTab] = useState(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) { - 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) { + 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) { - 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) { + 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) { - 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) => { + list.forEach((object: GridCharacter) => { + if (object.position != null) + appState.grid.characters[object.position] = object + }) + } + + const storeWeapons = (list: Array) => { + 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) => { + 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) { + 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 = ( + + ) - // 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 = ( + + ) - 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 = ( + + ) - 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 = ( + + ) - 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 = ( - + {navigation} +
    {currentGrid()}
    + { + - ) - - const weaponGrid = ( - - ) - - const summonGrid = ( - - ) - - const characterGrid = ( - - ) - - const currentGrid = () => { - switch(currentTab) { - case GridType.Character: - return characterGrid - case GridType.Weapon: - return weaponGrid - case GridType.Summon: - return summonGrid - } - } - - return ( -
    - { navigation } -
    - { currentGrid() } -
    - { } -
    - ) + } +
    + ) } export default Party diff --git a/components/PartyDetails/index.tsx b/components/PartyDetails/index.tsx index 80884616..b432f127 100644 --- a/components/PartyDetails/index.tsx +++ b/components/PartyDetails/index.tsx @@ -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) => void + editable: boolean + updateCallback: (name?: string, description?: string, raid?: Raid) => void + deleteCallback: ( + event: React.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() - const descriptionInput = React.createRef() - const raidSelect = React.createRef() + const nameInput = React.createRef() + const descriptionInput = React.createRef() + const raidSelect = React.createRef() - 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) { - event.preventDefault() + function handleInputChange(event: React.ChangeEvent) { + 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) { - event.preventDefault() + function handleTextAreaChange(event: React.ChangeEvent) { + 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 ( - {party.user.picture.picture} { + if (party.user) + return ( + {party.user.picture.picture} - ) - else - return (
    ) - } - - const userBlock = () => { - return ( -
    - { userImage() } - { (party.user) ? party.user.username : t('no_user') } -
    - ) - } - - const linkedUserBlock = (user: User) => { - return ( -
    - - {userBlock()} - -
    - ) - } - - const linkedRaidBlock = (raid: Raid) => { - return ( - - ) - } - - const deleteButton = () => { - if (party.editable) { - return ( - - - - - - {t('buttons.delete')} - - - - - - {t('modals.delete_team.title')} - - - {t('modals.delete_team.description')} - -
    - {t('modals.delete_team.buttons.cancel')} - props.deleteCallback(e)}>{t('modals.delete_team.buttons.confirm')} -
    -
    -
    -
    - ) - } else { - return ('') - } - } - - const editable = ( -
    - - - - -
    -
    - { (router.pathname !== '/new') ? deleteButton() : '' } -
    -
    - - - -
    -
    -
    - ) - - const readOnly = ( -
    -
    -
    - { (party.name) ?

    {party.name}

    : '' } -
    - { (party.user) ? linkedUserBlock(party.user) : userBlock() } - { (party.raid) ? linkedRaidBlock(party.raid) : '' } - { (party.created_at != undefined) - ? - : '' } -
    -
    -
    - { (party.editable) - ? - :
    } -
    -
    - { (party.description) ?

    {party.description}

    : '' } -
    - ) - - const emptyDetails = ( -
    - -
    - ) - - 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
    + } + const userBlock = () => { return ( -
    - - {generateTitle()} - - - - - - - - - - - - { (editable && (party.name || party.description || party.raid)) ? readOnly : emptyDetails} - {editable} -
    +
    + {userImage()} + {party.user ? party.user.username : t("no_user")} +
    ) + } + + const linkedUserBlock = (user: User) => { + return ( +
    + + {userBlock()} + +
    + ) + } + + const linkedRaidBlock = (raid: Raid) => { + return ( + + ) + } + + const deleteButton = () => { + if (party.editable) { + return ( + + + + + + {t("buttons.delete")} + + + + + + {t("modals.delete_team.title")} + + + {t("modals.delete_team.description")} + +
    + + {t("modals.delete_team.buttons.cancel")} + + props.deleteCallback(e)} + > + {t("modals.delete_team.buttons.confirm")} + +
    +
    +
    +
    + ) + } else { + return "" + } + } + + const editable = ( +
    + + + + +
    +
    + {router.pathname !== "/new" ? deleteButton() : ""} +
    +
    + + + +
    +
    +
    + ) + + const readOnly = ( +
    +
    +
    + {party.name ?

    {party.name}

    : ""} +
    + {party.user ? linkedUserBlock(party.user) : userBlock()} + {party.raid ? linkedRaidBlock(party.raid) : ""} + {party.created_at != undefined ? ( + + ) : ( + "" + )} +
    +
    +
    + {party.editable ? ( + + ) : ( +
    + )} +
    +
    + {party.description ? ( +

    + {party.description} +

    + ) : ( + "" + )} +
    + ) + + const emptyDetails = ( +
    + {party.editable ? ( + + ) : ( +
    + )} +
    + ) + + 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 ( +
    + + {generateTitle()} + + + + + + + + + + + + {editable && (party.name || party.description || party.raid) + ? readOnly + : emptyDetails} + {editable} +
    + ) } export default PartyDetails diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx index 1e8aa954..88a0e340 100644 --- a/components/SearchModal/index.tsx +++ b/components/SearchModal/index.tsx @@ -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() + let scrollContainer = React.createRef() - let searchInput = React.createRef() - let scrollContainer = React.createRef() + 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) { + const text = event.target.value + if (text.length) { + setQuery(text) + } else { + setQuery("") + } + } - function inputChanged(event: React.ChangeEvent) { - 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 ( - 0) ? results.length : 0} - next={ () => setCurrentPage(currentPage + 1) } - hasMore={totalPages > currentPage} - scrollableTarget="Results" - loader={
    Loading...
    }> - {jsx} -
    - ) + 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 { 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 { 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 { 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 ( - - - {props.children} - - - - - -
    -
    {t('search.result_count', { "record_count": recordCount })}
    - { (open) ? renderResults() : ''} -
    -
    - -
    -
    + 0 ? results.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + scrollableTarget="Results" + loader={
    Loading...
    } + > + {jsx} +
    ) + } + + 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 ( + { + 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 ( + { + 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 ( + { + storeRecentResult(result) + }} + /> + ) + }) + } + + return jsx + } + + function openChange() { + if (open) { + setQuery("") + setFirstLoad(true) + setResults([]) + setRecordCount(0) + setCurrentPage(1) + setOpen(false) + } else { + setOpen(true) + } + } + + return ( + + {props.children} + + + + +
    +
    + {t("search.result_count", { record_count: recordCount })} +
    + {open ? renderResults() : ""} +
    +
    + +
    +
    + ) } -export default SearchModal \ No newline at end of file +export default SearchModal diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index 516c6900..c27ffa79 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -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({ - 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({ + username: "", + email: "", + password: "", + passwordConfirmation: "", + }) - // States - const [open, setOpen] = useState(false) - - // Set up form refs - const usernameInput = React.createRef() - const emailInput = React.createRef() - const passwordInput = React.createRef() - const passwordConfirmationInput = React.createRef() - 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() + const emailInput = React.createRef() + const passwordInput = React.createRef() + const passwordConfirmationInput = React.createRef() + 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) { - 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) { - 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 ( - - -
  • - {t('menu.signup')} -
  • -
    - - event.preventDefault() }> -
    - {t('modals.signup.title')} - - - - - -
    + setCookie("account", cookieObj, { path: "/" }) + } -
    -
    + function fetchUserInfo(id: string) { + return api.userInfo(id) + } -
    + 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, + } -
    + // TODO: Set language + setCookie("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) { + 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) { + 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 ( + + +
  • + {t("menu.signup")} +
  • +
    + + event.preventDefault()} + > +
    + + {t("modals.signup.title")} + + + + + + +
    + + +
    + +
    + +
    + +
    + + + + + {/* By signing up, I agree to the Privacy PolicyUsage Guidelines. */} - - - - - - - ) + + + + + + + ) } - -export default SignupModal \ No newline at end of file +export default SignupModal diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx index f1ee104b..8303801c 100644 --- a/components/SummonGrid/index.tsx +++ b/components/SummonGrid/index.tsx @@ -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> - pushHistory?: (path: string) => void + new: boolean + summons?: GridSummon[] + createParty: () => Promise> + 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) { - 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 = ( +
    +
    {t("summons.main")}
    + +
    + ) - // 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 = ( -
    -
    {t('summons.main')}
    - +
    {t("summons.friend")}
    + +
    + ) + const summonGridElement = ( +
    +
    {t("summons.summons")}
    +
      + {Array.from(Array(numSummons)).map((x, i) => { + return ( +
    • + -
    - ) + /> + + ) + })} + +
    + ) + const subAuraSummonElement = ( + + ) + return ( +
    +
    + {mainSummonElement} + {friendSummonElement} + {summonGridElement} +
    - const friendSummonElement = ( -
    -
    {t('summons.friend')}
    - -
    - ) - const summonGridElement = ( -
    -
    {t('summons.summons')}
    -
      - {Array.from(Array(numSummons)).map((x, i) => { - return (
    • - -
    • ) - })} -
    -
    - ) - const subAuraSummonElement = ( - - ) - return ( -
    -
    - { mainSummonElement } - { friendSummonElement } - { summonGridElement } -
    - - { subAuraSummonElement } -
    - ) + {subAuraSummonElement} +
    + ) } export default SummonGrid diff --git a/components/TopHeader/index.tsx b/components/TopHeader/index.tsx index c7915304..6fdaffe8 100644 --- a/components/TopHeader/index.tsx +++ b/components/TopHeader/index.tsx @@ -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 ( -
    - - { (account.user) ? - : - - } -
    - ) - } + 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 () - else - return () - } + 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 ( -
    - { (router.route === '/p/[party]' && account.user && (!party.user || party.user.id !== account.user.id)) ? - saveButton() : '' - } - { (router.route === '/p/[party]') ? - : '' - } - -
    - ) - } - - + const leftNav = () => { return ( -
    +
    + + {account.user ? ( + + ) : ( + + )} +
    ) + } + + const saveButton = () => { + if (party.favorited) + return ( + + ) + else + return ( + + ) + } + + const rightNav = () => { + return ( +
    + {router.route === "/p/[party]" && + account.user && + (!party.user || party.user.id !== account.user.id) + ? saveButton() + : ""} + {router.route === "/p/[party]" ? ( + + ) : ( + "" + )} + +
    + ) + } + + return
    } -export default TopHeader \ No newline at end of file +export default TopHeader diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index 3eb6b2a3..e4be7183 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -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> - pushHistory?: (path: string) => void + new: boolean + weapons?: GridWeapon[] + createParty: (extra: boolean) => Promise> + 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) { - 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 = ( + + ) - // 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 = ( - - ) - - const weaponGridElement = ( - Array.from(Array(numWeapons)).map((x, i) => { - return ( -
  • - -
  • - ) - }) - ) - - const extraGridElement = ( - - ) - + const weaponGridElement = Array.from(Array(numWeapons)).map((x, i) => { return ( -
    -
    - { mainhandElement } -
      { weaponGridElement }
    -
    - - { (() => { return (party.extra) ? extraGridElement : '' })() } -
    +
  • + +
  • ) + }) + + const extraGridElement = ( + + ) + + return ( +
    +
    + {mainhandElement} +
      {weaponGridElement}
    +
    + + {(() => { + return party.extra ? extraGridElement : "" + })()} +
    + ) } export default WeaponGrid diff --git a/components/WeaponModal/index.tsx b/components/WeaponModal/index.tsx index 1f8a684c..d29b43e6 100644 --- a/components/WeaponModal/index.tsx +++ b/components/WeaponModal/index.tsx @@ -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() - const weaponKey2Select = React.createRef() - const weaponKey3Select = React.createRef() + 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() + const weaponKey2Select = React.createRef() + const weaponKey3Select = React.createRef() - 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 ( -
    -

    {t('modals.weapon.subtitles.element')}

    - -
    - ) - } - - const keySelect = () => { - return ( -
    -

    {t('modals.weapon.subtitles.weapon_keys')}

    - { ([2, 3, 17, 22].includes(props.gridWeapon.object.series)) ? - - : ''} - - { ([2, 3, 17].includes(props.gridWeapon.object.series)) ? - - : ''} - - { (props.gridWeapon.object.series == 17) ? - - : ''} -
    - ) - } - - const axSelect = () => { - return ( -
    -

    {t('modals.weapon.subtitles.ax_skills')}

    - -
    - ) - } - - function openChange(open: boolean) { - setFormValid(false) - setOpen(open) - } - + const elementSelect = () => { return ( - - - { props.children } - - - event.preventDefault() }> -
    -
    - {t('modals.weapon.title')} - {props.gridWeapon.object.name[locale]} -
    - - - - - -
    - -
    - { (props.gridWeapon.object.element == 0) ? elementSelect() : '' } - { ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) ? keySelect() : '' } - { (props.gridWeapon.object.ax > 0) ? axSelect() : '' } - -
    -
    - -
    -
    +
    +

    {t("modals.weapon.subtitles.element")}

    + +
    ) + } + + const keySelect = () => { + return ( +
    +

    {t("modals.weapon.subtitles.weapon_keys")}

    + {[2, 3, 17, 22].includes(props.gridWeapon.object.series) ? ( + + ) : ( + "" + )} + + {[2, 3, 17].includes(props.gridWeapon.object.series) ? ( + + ) : ( + "" + )} + + {props.gridWeapon.object.series == 17 ? ( + + ) : ( + "" + )} +
    + ) + } + + const axSelect = () => { + return ( +
    +

    {t("modals.weapon.subtitles.ax_skills")}

    + +
    + ) + } + + function openChange(open: boolean) { + setFormValid(false) + setOpen(open) + } + + return ( + + {props.children} + + event.preventDefault()} + > +
    +
    + + {t("modals.weapon.title")} + + + {props.gridWeapon.object.name[locale]} + +
    + + + + + +
    + +
    + {props.gridWeapon.object.element == 0 ? elementSelect() : ""} + {[2, 3, 17, 24].includes(props.gridWeapon.object.series) + ? keySelect() + : ""} + {props.gridWeapon.object.ax > 0 ? axSelect() : ""} + +
    +
    + +
    +
    + ) } -export default WeaponModal \ No newline at end of file +export default WeaponModal diff --git a/package-lock.json b/package-lock.json index 7f47bf18..8e8cf478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7710dd37..d83c1ab6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/[username].tsx b/pages/[username].tsx index a2f6f7cc..0412a142 100644 --- a/pages/[username].tsx +++ b/pages/[username].tsx @@ -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) => { + // 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([]) - const [raids, setRaids] = useState() - const [raid, setRaid] = useState() - const [user, setUser] = useState(emptyUser) + // Set up page-specific states + const [parties, setParties] = useState([]) + const [raids, setRaids] = useState() + const [raid, setRaid] = useState() - // 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 - }) - } - - return ( -
    - - @{user.username}'s Teams - - - - - - - - - - - - -
    - {user.picture.picture} -

    {user.username}

    -
    -
    - -
    - 0) ? parties.length : 0} - next={ () => setCurrentPage(currentPage + 1) } - hasMore={totalPages > currentPage} - loader={

    Loading...

    }> - - { renderParties() } - -
    - - { (parties.length == 0) ? -
    -

    { (loading) ? t('teams.loading') : t('teams.not_found') }

    -
    - : '' } -
    -
    - ) -} - -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 ( + + ) + }) + } + + return ( +
    + + @{props.user?.username}'s Teams + + + + + + + + + + + + +
    + {props.user?.picture.picture} +

    {props.user?.username}

    +
    +
    + +
    + 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={ +
    +

    Loading...

    +
    + } + > + {renderParties()} +
    + + {parties.length == 0 ? ( +
    +

    {t("teams.not_found")}

    +
    + ) : ( + "" + )} +
    +
    + ) +} + +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 diff --git a/pages/_app.tsx b/pages/_app.tsx index c7dd8537..91590938 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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 ( - - - - - - ) + return ( + + + + ) } export default appWithTranslation(MyApp) diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index 2c5e9423..9a37db1f 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -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 ( -
    - -
    - ) +import type { NextApiRequest, NextApiResponse } from "next" - // function renderNotFound() { - // return ( - //
    - //

    There's no grid here.

    - // - //
    - // ) - // } - - // if (!found && !loading) { - // return renderNotFound() - // } else if (found && !loading) { - // return render() - // } else { - // return (
    ) - // } +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) => { + return ( +
    + +
    + ) } -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 \ No newline at end of file +// 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 diff --git a/pages/saved.tsx b/pages/saved.tsx index aaa982cf..6d2b7c9e 100644 --- a/pages/saved.tsx +++ b/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([]) - const [raids, setRaids] = useState() - const [raid, setRaid] = useState() - - // 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 - }) - } - - return ( -
    - - {t('saved.title')} - - - - - - - - - - - -

    {t('saved.title')}

    -
    - -
    - 0) ? parties.length : 0} - next={ () => setCurrentPage(currentPage + 1) } - hasMore={totalPages > currentPage} - loader={

    Loading...

    }> - - { renderParties() } - -
    - - { (parties.length == 0) ? -
    -

    { (loading) ? t('saved.loading') : t('saved.not_found') }

    -
    - : '' } -
    -
    - ) +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) => { + // 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([]) + const [raids, setRaids] = useState() + const [raid, setRaid] = useState() + + // 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 ( + + ) + }) + } + + return ( +
    + + {t("saved.title")} + + + + + + + + + + + +

    {t("saved.title")}

    +
    + +
    + 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={ +
    +

    Loading...

    +
    + } + > + {renderParties()} +
    + + {parties.length == 0 ? ( +
    +

    {t("saved.not_found")}

    +
    + ) : ( + "" + )} +
    +
    + ) } -export default SavedRoute \ No newline at end of file +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 diff --git a/pages/teams.tsx b/pages/teams.tsx index a1d8bfb2..28997106 100644 --- a/pages/teams.tsx +++ b/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([]) - const [raids, setRaids] = useState() - const [raid, setRaid] = useState() - - // 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 - }) - } - - return ( -
    - - { t('teams.title') } - - - - - - - - - - - - - -

    {t('teams.title')}

    -
    - -
    - 0) ? parties.length : 0} - next={ () => setCurrentPage(currentPage + 1) } - hasMore={totalPages > currentPage} - loader={

    Loading...

    }> - - { renderParties() } - -
    - - { (parties.length == 0) ? -
    -

    { (loading) ? t('teams.loading') : t('teams.not_found') }

    -
    - : '' } -
    -
    - ) +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) => { + // 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([]) + const [raids, setRaids] = useState() + const [raid, setRaid] = useState() + + // 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 ( + + ) + }) + } + + return ( +
    + + {t("teams.title")} + + + + + + + + + + + + + +

    {t("teams.title")}

    +
    + +
    + 0 ? parties.length : 0} + next={() => setCurrentPage(currentPage + 1)} + hasMore={totalPages > currentPage} + loader={ +
    +

    Loading...

    +
    + } + > + {renderParties()} +
    + + {parties.length == 0 ? ( +
    +

    {t("teams.not_found")}

    +
    + ) : ( + "" + )} +
    +
    + ) } -export default TeamsRoute \ No newline at end of file +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 diff --git a/public/locales/en/common.json b/public/locales/en/common.json index a189bb35..f43a79a0 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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
    <2>Privacy Policy and <1>Usage Guidelines.", - "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
    <2>Privacy Policy and <1>Usage Guidelines.", + "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" } diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index 908a87ef..21d147bc 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -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>プライバシーポリシーを読んだものとみなされます。", - "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>プライバシーポリシーを読んだものとみなされます。", + "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
    Weapons", - "coming_soon": "開発中", - "no_title": "無題", - "no_raid": "マルチなし", - "no_user": "無名" + "placeholders": { + "weapon": "武器を検索...", + "summon": "召喚石を検索...", + "character": "キャラを検索..." + } + }, + "teams": { + "title": "編成一覧", + "loading": "ロード中...", + "not_found": "編成は見つかりませんでした" + }, + "extra_weapons": "Additional
    Weapons", + "coming_soon": "開発中", + "no_title": "無題", + "no_raid": "マルチなし", + "no_user": "無名" } - \ No newline at end of file diff --git a/types/AccountCookie.d.ts b/types/AccountCookie.d.ts new file mode 100644 index 00000000..444f2e4a --- /dev/null +++ b/types/AccountCookie.d.ts @@ -0,0 +1,5 @@ +interface AccountCookie { + userId: string + username: string + token: string +} diff --git a/types/Party.d.ts b/types/Party.d.ts index 11c2158a..5a1dea6b 100644 --- a/types/Party.d.ts +++ b/types/Party.d.ts @@ -1,14 +1,15 @@ interface Party { - id: string - name: string - raid: Raid - shortcode: string - extra: boolean - favorited: boolean - characters: Array - weapons: Array - summons: Array - user: User - created_at: string - updated_at: string -} \ No newline at end of file + id: string + name: string + description: string + raid: Raid + shortcode: string + extra: boolean + favorited: boolean + characters: Array + weapons: Array + summons: Array + user: User + created_at: string + updated_at: string +} diff --git a/types/UserCookie.d.ts b/types/UserCookie.d.ts new file mode 100644 index 00000000..df04aadc --- /dev/null +++ b/types/UserCookie.d.ts @@ -0,0 +1,6 @@ +interface UserCookie { + picture: string + element: string + language: string + gender: number +} diff --git a/utils/raidGroups.tsx b/utils/raidGroups.tsx index 1d411423..da084c6f 100644 --- a/utils/raidGroups.tsx +++ b/utils/raidGroups.tsx @@ -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: "スーパーアルティメット", + }, + }, ] diff --git a/utils/useDidMountEffect.tsx b/utils/useDidMountEffect.tsx new file mode 100644 index 00000000..b532e71d --- /dev/null +++ b/utils/useDidMountEffect.tsx @@ -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