diff --git a/components/Alert/index.tsx b/components/Alert/index.tsx index d7efec50..21a1a1a7 100644 --- a/components/Alert/index.tsx +++ b/components/Alert/index.tsx @@ -3,7 +3,6 @@ import * as AlertDialog from '@radix-ui/react-alert-dialog' import './index.scss' import Button from '~components/Button' -import { ButtonType } from '~utils/enums' // Props interface Props { diff --git a/components/CharacterConflictModal/index.scss b/components/CharacterConflictModal/index.scss index 866ec8f7..e69de29b 100644 --- a/components/CharacterConflictModal/index.scss +++ b/components/CharacterConflictModal/index.scss @@ -1,63 +0,0 @@ -.Conflict.Dialog { - & > p { - line-height: 1.2; - max-width: 400px; - } - img { - border-radius: 1rem; - } - - .arrow { - color: $grey-55; - font-size: 4rem; - text-align: center; - } - - .character { - display: flex; - flex-direction: column; - gap: $unit; - text-align: center; - width: 12rem; - font-weight: $medium; - } - - .diagram { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - align-items: center; - - ul { - display: flex; - flex-direction: column; - gap: $unit * 2; - } - } - - footer { - display: flex; - flex-direction: row; - gap: $unit; - - .Button { - font-size: $font-regular; - padding: ($unit * 1.5) ($unit * 2); - width: 100%; - - &.btn-disabled { - background: $grey-90; - color: $grey-70; - cursor: not-allowed; - } - - &:not(.btn-disabled) { - background: $grey-90; - color: $grey-50; - - &:hover { - background: $grey-80; - } - } - } - } -} diff --git a/components/CharacterConflictModal/index.tsx b/components/CharacterConflictModal/index.tsx index 5faa7ebc..6493b215 100644 --- a/components/CharacterConflictModal/index.tsx +++ b/components/CharacterConflictModal/index.tsx @@ -1,14 +1,10 @@ import React, { useEffect, useState } from 'react' -import { setCookie } from 'cookies-next' -import Router, { useRouter } from 'next/router' -import { useTranslation } from 'react-i18next' -import { AxiosResponse } from 'axios' +import { useRouter } from 'next/router' +import { Trans, useTranslation } from 'next-i18next' import * as Dialog from '@radix-ui/react-dialog' -import api from '~utils/api' import { appState } from '~utils/appState' -import { accountState } from '~utils/accountState' import Button from '~components/Button' @@ -24,7 +20,11 @@ interface Props { } const CharacterConflictModal = (props: Props) => { + // Localization + const router = useRouter() const { t } = useTranslation('common') + const locale = + router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' // States const [open, setOpen] = useState(false) @@ -60,11 +60,11 @@ const CharacterConflictModal = (props: Props) => { function openChange(open: boolean) { setOpen(open) + props.resetConflict() } function close() { setOpen(false) - props.resetConflict() } return ( @@ -75,33 +75,37 @@ const CharacterConflictModal = (props: Props) => { onOpenAutoFocus={(event) => event.preventDefault()} >

- Only one version of a character can be included in each party. Do - you want to change your party members? +

-
+
-
- {props.incomingCharacter?.name.en} - {props.incomingCharacter?.name.en} +
+
+ {props.incomingCharacter?.name[locale]} + {props.incomingCharacter?.name[locale]} +
-
diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index 6924c474..2f009a63 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -145,7 +145,8 @@ const CharacterGrid = (props: Props) => { async function resolveConflict() { if (incoming && conflicts.length > 0) { await api - .resolveCharacterConflict({ + .resolveConflict({ + object: 'characters', incoming: incoming.id, conflicting: conflicts.map((c) => c.id), position: position, diff --git a/components/Dialog/index.scss b/components/Dialog/index.scss index c1d9f0b0..df8f4d31 100644 --- a/components/Dialog/index.scss +++ b/components/Dialog/index.scss @@ -92,4 +92,101 @@ justify-content: flex-end; width: 100%; } + + &.Conflict.Dialog { + $weapon-diameter: 14rem; + + & > p { + line-height: 1.2; + max-width: 400px; + + strong { + font-weight: $bold; + } + + &:lang(ja) { + line-height: 1.4; + } + } + + .weapon, + .character { + display: flex; + flex-direction: column; + gap: $unit; + text-align: center; + width: $weapon-diameter; + font-weight: $medium; + + img { + border-radius: 1rem; + width: $weapon-diameter; + height: auto; + } + + span { + line-height: 1.3; + } + } + + .Diagram { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: flex-start; + + &.CharacterDiagram { + align-items: center; + } + + ul { + align-items: center; + display: flex; + flex-direction: column; + gap: $unit * 2; + } + + .wrapper { + display: flex; + justify-content: center; + width: 100%; + } + + .arrow { + align-items: center; + color: $grey-55; + display: flex; + font-size: 4rem; + text-align: center; + height: $weapon-diameter; + justify-content: center; + } + } + + footer { + display: flex; + flex-direction: row; + gap: $unit; + + .Button { + font-size: $font-regular; + padding: ($unit * 1.5) ($unit * 2); + width: 100%; + + &.btn-disabled { + background: $grey-90; + color: $grey-70; + cursor: not-allowed; + } + + &:not(.btn-disabled) { + background: $grey-90; + color: $grey-50; + + &:hover { + background: $grey-80; + } + } + } + } + } } diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx index 9b5912de..b7cdebb9 100644 --- a/components/SearchModal/index.tsx +++ b/components/SearchModal/index.tsx @@ -151,9 +151,22 @@ const SearchModal = (props: Props) => { openChange() } + const extraPositions = () => { + if (props.object === 'weapons') return [9, 10, 11] + else if (props.object === 'summons') return [4, 5] + else return [] + } + function receiveFilters(filters: { [key: string]: any }) { setCurrentPage(1) setResults([]) + + // Only show extra or subaura objects if invoked from those positions + if (extraPositions().includes(props.fromPosition)) { + if (props.object === 'weapons') filters.extra = true + else if (props.object === 'summons') filters.subaura = true + } + setFilters(filters) } @@ -175,7 +188,12 @@ const SearchModal = (props: Props) => { : [] if (open) { - if (firstLoad && cookieObj && cookieObj.length > 0) { + if ( + firstLoad && + cookieObj && + cookieObj.length > 0 && + !extraPositions().includes(props.fromPosition) + ) { setResults(cookieObj) setRecordCount(cookieObj.length) setFirstLoad(false) diff --git a/components/WeaponConflictModal/index.scss b/components/WeaponConflictModal/index.scss new file mode 100644 index 00000000..e69de29b diff --git a/components/WeaponConflictModal/index.tsx b/components/WeaponConflictModal/index.tsx new file mode 100644 index 00000000..84bd3541 --- /dev/null +++ b/components/WeaponConflictModal/index.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import { Trans, useTranslation } from 'react-i18next' + +import * as Dialog from '@radix-ui/react-dialog' +import Button from '~components/Button' + +import mapWeaponSeries from '~utils/mapWeaponSeries' + +import './index.scss' + +interface Props { + open: boolean + incomingWeapon: Weapon + conflictingWeapons: GridWeapon[] + desiredPosition: number + resolveConflict: () => void + resetConflict: () => void +} + +const WeaponConflictModal = (props: Props) => { + // Localization + const router = useRouter() + const { t } = useTranslation('common') + const locale = + router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' + + // States + const [open, setOpen] = useState(false) + + useEffect(() => { + setOpen(props.open) + }, [setOpen, props.open]) + + function imageUrl(weapon?: Weapon) { + return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${weapon?.granblue_id}.jpg` + } + + function openChange(open: boolean) { + setOpen(open) + props.resetConflict() + } + + function close() { + setOpen(false) + } + + const infoString = () => { + const series = props.incomingWeapon.series + const seriesSlug = t(`series.${mapWeaponSeries(series)}`) + + return [2, 3].includes(series) ? ( + + ) : ( + + Only one weapon from the + {{ series: seriesSlug }} Series can be included in each + party. Do you want to change your weapons? + + ) + } + + return ( + + + event.preventDefault()} + > +

{infoString()}

+
+
    + {props.conflictingWeapons?.map((weapon, i) => ( +
  • + {weapon.object.name[locale]} + {weapon.object.name[locale]} +
  • + ))} +
+ +
+
+ {props.incomingWeapon?.name[locale]} + {props.incomingWeapon?.name[locale]} +
+
+
+
+
+
+ +
+
+ ) +} + +export default WeaponConflictModal diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index ee296e71..a051c7ed 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -2,6 +2,7 @@ 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' @@ -15,6 +16,8 @@ import { appState } from '~utils/appState' import type { SearchableObject } from '~types' import './index.scss' +import WeaponConflictModal from '~components/WeaponConflictModal' +import Alert from '~components/Alert' // Props interface Props { @@ -28,18 +31,25 @@ const WeaponGrid = (props: Props) => { // Constants const numWeapons: number = 9 + // Localization + 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}` } } - : {} // Set up state for view management const { party, grid } = useSnapshot(appState) const [slug, setSlug] = useState() + const [modalOpen, setModalOpen] = useState(false) + const [showIncompatibleAlert, setShowIncompatibleAlert] = useState(false) + + // Set up state for conflict management + const [incoming, setIncoming] = useState() + const [conflicts, setConflicts] = useState([]) + const [position, setPosition] = useState(0) // Create a temporary state to store previous weapon uncap values const [previousUncapValues, setPreviousUncapValues] = useState<{ @@ -90,9 +100,45 @@ const WeaponGrid = (props: Props) => { ) }) } else { - saveWeapon(party.id, weapon, position).then((response) => - storeGridWeapon(response.data) - ) + if (party.editable) + saveWeapon(party.id, weapon, position) + .then((response) => handleWeaponResponse(response.data)) + .catch((error) => { + const code = error.response.status + const data = error.response.data + console.log(error.response) + + console.log(data, code) + if (code === 422) { + if (data.code === 'incompatible_weapon_for_position') { + console.log('Here') + setShowIncompatibleAlert(true) + } + } + }) + } + } + + async function handleWeaponResponse(data: any) { + if (data.hasOwnProperty('conflicts')) { + if (data.incoming) setIncoming(data.incoming) + if (data.conflicts) setConflicts(data.conflicts) + if (data.position) setPosition(data.position) + setModalOpen(true) + } else { + storeGridWeapon(data.grid_weapon) + + // If we replaced an existing weapon, remove it from the grid + if (data.hasOwnProperty('meta') && data.meta['replaced'] !== undefined) { + const position = data.meta['replaced'] + + if (position == -1) { + appState.grid.weapons.mainWeapon = undefined + appState.party.element = 0 + } else { + appState.grid.weapons.allWeapons[position] = undefined + } + } } } @@ -101,18 +147,15 @@ const WeaponGrid = (props: Props) => { 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, - }, + 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) { @@ -125,6 +168,44 @@ const WeaponGrid = (props: Props) => { } } + async function resolveConflict() { + if (incoming && conflicts.length > 0) { + await api + .resolveConflict({ + object: 'weapons', + incoming: incoming.id, + conflicting: conflicts.map((c) => c.id), + position: position, + }) + .then((response) => { + // Store new character in state + storeGridWeapon(response.data) + + // Remove conflicting characters from state + conflicts.forEach((c) => { + if (appState.grid.weapons.mainWeapon?.object.id === c.id) { + appState.grid.weapons.mainWeapon = undefined + appState.party.element = 0 + } else { + appState.grid.weapons.allWeapons[c.position] = undefined + } + }) + + // Reset conflict + resetConflict() + + // Close modal + setModalOpen(false) + }) + } + } + + function resetConflict() { + setPosition(-1) + setConflicts([]) + setIncoming(undefined) + } + // Methods: Updating uncap level // Note: Saves, but debouncing is not working properly async function saveUncap(id: string, position: number, uncapLevel: number) { @@ -242,8 +323,39 @@ const WeaponGrid = (props: Props) => { /> ) + const conflictModal = () => { + return incoming && conflicts ? ( + + ) : ( + '' + ) + } + + const incompatibleAlert = () => { + console.log(t('alert.incompatible_weapon')) + return showIncompatibleAlert ? ( + setShowIncompatibleAlert(!showIncompatibleAlert)} + cancelActionText={t('buttons.confirm')} + message={t('alert.incompatible_weapon')} + > + ) : ( + '' + ) + } + return (
+ {conflicts ? conflictModal() : ''} + {incompatibleAlert()}
{mainhandElement}
    {weaponGridElement}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2de21696..ced95973 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,4 +1,7 @@ { + "alert": { + "incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots." + }, "ax": { "no_skill": "No AX Skill", "errors": { @@ -20,6 +23,7 @@ "buttons": { "cancel": "Cancel", "copy": "Copy link", + "confirm": "Got it", "delete": "Delete team", "show_info": "Edit info", "hide_info": "Hide info", @@ -86,6 +90,7 @@ "olden_primal": "Olden Primal", "beast": "Beast", "omega": "Omega", + "regalia": "Regalia", "militis": "Militis", "xeno": "Xeno", "astral": "Astral", @@ -101,7 +106,10 @@ "vintage": "Vintage", "class_champion": "Class Champion", "sephira": "Sephira", - "new_world": "New World Foundation" + "new_world": "New World Foundation", + "revenant": "Revenant", + "proving": "Proving Grounds", + "disaster": "Disaster" }, "recency": { "all_time": "All time", @@ -122,6 +130,16 @@ "about": { "title": "About" }, + "conflict": { + "character": "Only one version of a character can be included in each party. Do you want to change your party members?", + "weapon": { + "generic": "Only one weapon from the {{series}} Series can be included in each party. Do you want to change your weapons?", + "opus-draconic": "Only one Dark Opus or Draconic Weapon can be included in each party. Do you want to change your weapons?" + }, + "buttons": { + "confirm": "Confirm" + } + }, "delete_team": { "title": "Delete team", "description": "Are you sure you want to permanently delete this team?", diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index 7dc42e3f..335e109e 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -1,4 +1,7 @@ { + "alert": { + "incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。" + }, "ax": { "no_skill": "EXスキルなし", "errors": { @@ -18,8 +21,9 @@ } }, "buttons": { - "cancel": "キャンセルs", + "cancel": "キャンセル", "copy": "リンクをコピー", + "confirm": "OK", "delete": "編成を削除", "show_info": "詳細を編集", "save_info": "詳細を保存", @@ -86,6 +90,7 @@ "olden_primal": "オールド・プライマルシリーズ", "beast": "四象武器", "omega": "マグナシリーズ", + "regalia": "レガリアシリーズ", "militis": "ミーレスシリーズ", "xeno": "六道武器", "astral": "アストラルウェポン", @@ -101,7 +106,10 @@ "vintage": "ヴィンテージシリーズ", "class_champion": "英雄武器", "sephira": "セフィラン・オールドウェポン", - "new_world": "新世界の礎" + "new_world": "新世界の礎", + "revenant": "天星器", + "proving": "ブレイブグラウンド", + "disaster": "災害シリーズ" }, "recency": { "all_time": "全ての期間", @@ -122,6 +130,16 @@ "about": { "title": "このサイトについて" }, + "conflict": { + "character": "同じ名前のキャラクターがパーティに編成されています。
以下のキャラクターを入れ替えますか?", + "weapon": { + "generic": "{{series}}の武器に装備制限があり、武器が既に装備されています。

以下の武器を入れ替えますか?", + "opus-draconic": "終末の神器シリーズドラコニックシリーズから1本しか装備できない制限があり、武器が既に装備されています。

以下の武器を入れ替えますか?" + }, + "buttons": { + "confirm": "入れ替える" + } + }, "delete_team": { "title": "編成を削除しますか", "description": "編成を削除する操作は取り消せません。", diff --git a/utils/api.tsx b/utils/api.tsx index 5b762cb9..5dcaa937 100644 --- a/utils/api.tsx +++ b/utils/api.tsx @@ -77,7 +77,8 @@ class Api { }) } - resolveCharacterConflict({ incoming, conflicting, position, params }: { + resolveConflict({ object, incoming, conflicting, position, params }: { + object: 'characters' | 'weapons' incoming: string conflicting: string[] position: number, @@ -90,7 +91,7 @@ class Api { position: position, }, } - const resourceUrl = `${this.url}/characters/resolve` + const resourceUrl = `${this.url}/${object}/resolve` return axios.post(resourceUrl, body, { headers: params }) } diff --git a/utils/mapWeaponSeries.tsx b/utils/mapWeaponSeries.tsx new file mode 100644 index 00000000..9bc63448 --- /dev/null +++ b/utils/mapWeaponSeries.tsx @@ -0,0 +1,4 @@ +import { weaponSeries } from '~utils/weaponSeries' + +export default (id: number) => + weaponSeries.find((series) => series.id === id)?.slug diff --git a/utils/weaponSeries.tsx b/utils/weaponSeries.tsx new file mode 100644 index 00000000..5c53e65c --- /dev/null +++ b/utils/weaponSeries.tsx @@ -0,0 +1,119 @@ +export interface WeaponSeries { + id: number + slug: string +} + +export const weaponSeries: WeaponSeries[] = [ + { + id: 0, + slug: 'seraphic', + }, + { + id: 1, + slug: 'grand', + }, + { + id: 2, + slug: 'opus', + }, + { + id: 3, + slug: 'draconic', + }, + { + id: 4, + slug: 'revenant', + }, + { + id: 6, + slug: 'primal', + }, + { + id: 7, + slug: 'beast', + }, + { + id: 8, + slug: 'regalia', + }, + { + id: 9, + slug: 'omega', + }, + { + id: 10, + slug: 'olden_primal', + }, + { + id: 11, + slug: 'militis', + }, + { + id: 12, + slug: 'hollowsky', + }, + { + id: 13, + slug: 'xeno', + }, + { + id: 14, + slug: 'astral', + }, + { + id: 15, + slug: 'rose', + }, + { + id: 16, + slug: 'bahamut', + }, + { + id: 17, + slug: 'ultima', + }, + { + id: 18, + slug: 'epic', + }, + { + id: 19, + slug: 'ennead', + }, + { + id: 20, + slug: 'cosmic', + }, + { + id: 21, + slug: 'ancestral', + }, + { + id: 22, + slug: 'superlative', + }, + { + id: 23, + slug: 'vintage', + }, + { + id: 24, + slug: 'class_champion', + }, + { + id: 25, + slug: 'proving', + }, + { + id: 28, + slug: 'sephira', + }, + { + id: 29, + slug: 'new_world', + }, + { + id: 30, + slug: 'disaster', + }, +]