diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index dc7b043f..af78ed7e 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -1,11 +1,18 @@ -import React, { useState } from 'react' +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useCookies } from 'react-cookie' import { useModal as useModal } from '~utils/useModal' +import { AxiosResponse } from 'axios' +import debounce from 'lodash.debounce' + import CharacterUnit from '~components/CharacterUnit' import SearchModal from '~components/SearchModal' +import api from '~utils/api' import './index.scss' +// GridType export enum GridType { Class, Character, @@ -13,67 +20,206 @@ export enum GridType { Summon } +// Props interface Props { - userId?: string - grid: GridArray + partyId?: string + characters: GridArray editable: boolean - exists: boolean - onSelect: (type: GridType, character: Character, position: number) => void + createParty: () => Promise> + pushHistory?: (path: string) => void } const CharacterGrid = (props: Props) => { - const { open, openModal, closeModal } = useModal() - const [searchPosition, setSearchPosition] = useState(0) - + // Constants const numCharacters: number = 5 - function isCharacter(object: Character | Weapon | Summon): object is Character { - // There aren't really any unique fields here - return (object as Character).gender !== undefined - } + // Cookies + const [cookies, _] = useCookies(['user']) + const headers = (cookies.user != null) ? { + headers: { + 'Authorization': `Bearer ${cookies.user.access_token}` + } + } : {} + // Set up state for party + const [partyId, setPartyId] = useState('') + + // Set up states for Grid data + const [characters, setCharacters] = useState>({}) + + // Set up states for Search + const { open, openModal, closeModal } = useModal() + const [itemPositionForSearch, setItemPositionForSearch] = useState(0) + + // Create a temporary state to store previous character uncap values + const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) + + // Create a state dictionary to store pure objects for Search + const [searchGrid, setSearchGrid] = useState>({}) + + // Set states from props + useEffect(() => { + setPartyId(props.partyId || '') + setCharacters(props.characters || {}) + }, [props]) + + // Initialize an array of current uncap values for each characters + useEffect(() => { + let initialPreviousUncapValues: {[key: number]: number} = {} + Object.values(props.characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + setPreviousUncapValues(initialPreviousUncapValues) + }, [props]) + + // Update search grid whenever characters are updated + useEffect(() => { + let newSearchGrid = Object.values(characters).map((o) => o.character) + setSearchGrid(newSearchGrid) + }, [characters]) + + // Methods: Adding an object from search function openSearchModal(position: number) { - setSearchPosition(position) + setItemPositionForSearch(position) openModal() } - function receiveCharacter(character: Character, position: number) { - props.onSelect(GridType.Character, character, position) - } + function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) { + const character = object as Character - function sendData(object: Character | Weapon | Summon, position: number) { - if (isCharacter(object)) { - receiveCharacter(object, position) + if (!partyId) { + props.createParty() + .then(response => { + const party = response.data.party + if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveCharacter(party.id, character, position) + .then(response => storeGridCharacter(response.data.grid_character)) + }) + } else { + saveCharacter(partyId, character, position) + .then(response => storeGridCharacter(response.data.grid_character)) } } + 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, + 'mainhand': (position == -1), + 'uncap_level': characterUncapLevel(character) + } + }, headers) + } + + function storeGridCharacter(gridCharacter: GridCharacter) { + // Store the grid unit at the correct position + let newCharacters = Object.assign({}, characters) + newCharacters[gridCharacter.position] = gridCharacter + setCharacters(newCharacters) + } + + // 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('weapon', 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) + } + } + + const initiateUncapUpdate = useCallback( + (id: string, position: number, uncapLevel: number) => { + memoizeAction(id, position, uncapLevel) + + // Optimistically update UI + updateUncapLevel(position, uncapLevel) + }, [previousUncapValues, characters] + ) + + const memoizeAction = useCallback( + (id: string, position: number, uncapLevel: number) => { + debouncedAction(id, position, uncapLevel) + }, [props] + ) + + const debouncedAction = useMemo(() => + debounce((id, position, number) => { + saveUncap(id, position, number) + }, 500), [props, saveUncap] + ) + + const updateUncapLevel = (position: number, uncapLevel: number) => { + let newCharacters = {...characters} + newCharacters[position].uncap_level = uncapLevel + setCharacters(newCharacters) + } + + function storePreviousUncapValue(position: number) { + // Save the current value in case of an unexpected result + let newPreviousValues = {...previousUncapValues} + newPreviousValues[position] = characters[position].uncap_level + + setPreviousUncapValues(newPreviousValues) + } + + // Render: JSX components return (
    - { - Array.from(Array(numCharacters)).map((x, i) => { - return ( -
  • - { openSearchModal(i) }} - editable={props.editable} - position={i} - character={props.grid[i]} - /> -
  • - ) - }) - } + {Array.from(Array(numCharacters)).map((x, i) => { + return ( +
  • + { openSearchModal(i) }} + updateUncap={initiateUncapUpdate} + /> +
  • + ) + })} + {open ? ( - - ) : null} + + ) : null}
) diff --git a/components/CharacterUnit/index.scss b/components/CharacterUnit/index.scss index 45aa6f99..2d104efc 100644 --- a/components/CharacterUnit/index.scss +++ b/components/CharacterUnit/index.scss @@ -22,7 +22,7 @@ border: 1px solid rgba(0, 0, 0, 0.1); box-shadow: rgba(0, 0, 0, 0.14) 0px 0px 14px; cursor: pointer; - transform: scale(1.1, 1.1); + transform: $scale-tall; } .CharacterUnit.filled h3 { diff --git a/components/CharacterUnit/index.tsx b/components/CharacterUnit/index.tsx index 309b3789..dc0600ae 100644 --- a/components/CharacterUnit/index.tsx +++ b/components/CharacterUnit/index.tsx @@ -2,16 +2,16 @@ import React, { useEffect, useState } from 'react' import classnames from 'classnames' import UncapIndicator from '~components/UncapIndicator' - import PlusIcon from '~public/icons/plus.svg' import './index.scss' interface Props { - onClick: () => void - character: Character | undefined + gridCharacter: GridCharacter | undefined position: number editable: boolean + onClick: () => void + updateUncap: (id: string, position: number, uncap: number) => void } const CharacterUnit = (props: Props) => { @@ -20,27 +20,32 @@ const CharacterUnit = (props: Props) => { const classes = classnames({ CharacterUnit: true, 'editable': props.editable, - 'filled': (props.character !== undefined) + 'filled': (props.gridCharacter !== undefined) }) - const character = props.character + const gridCharacter = props.gridCharacter + const character = gridCharacter?.character useEffect(() => { generateImageUrl() }) - function generateImageUrl() { let imgSrc = "" - if (props.character) { - const character = props.character! + if (props.gridCharacter) { + const character = props.gridCharacter.character! imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg` } setImageUrl(imgSrc) } + function passUncapData(uncap: number) { + if (props.gridCharacter) + props.updateUncap(props.gridCharacter.id, props.position, uncap) + } + return (
@@ -48,11 +53,15 @@ const CharacterUnit = (props: Props) => { {character?.name.en} { (props.editable) ? : '' }
- + { (gridCharacter && character) ? + : '' }

{character?.name.en}

diff --git a/components/ExtraSummons/index.tsx b/components/ExtraSummons/index.tsx index c3b4868e..80d21a4b 100644 --- a/components/ExtraSummons/index.tsx +++ b/components/ExtraSummons/index.tsx @@ -12,12 +12,13 @@ export enum GridType { // Props interface Props { - grid: GridArray + grid: GridArray editable: boolean exists: boolean found?: boolean offset: number onClick: (position: number) => void + updateUncap: (id: string, position: number, uncap: number) => void } const ExtraSummons = (props: Props) => { @@ -32,11 +33,12 @@ const ExtraSummons = (props: Props) => { return (
  • { props.onClick(props.offset + i) }} editable={props.editable} position={props.offset + i} unitType={1} - summon={props.grid[props.offset + i]} + gridSummon={props.grid[props.offset + i]} + onClick={() => { props.onClick(props.offset + i) }} + updateUncap={props.updateUncap} />
  • ) diff --git a/components/ExtraWeapons/index.tsx b/components/ExtraWeapons/index.tsx index 97bab025..efa1dfa5 100644 --- a/components/ExtraWeapons/index.tsx +++ b/components/ExtraWeapons/index.tsx @@ -13,12 +13,12 @@ export enum GridType { // Props interface Props { - grid: GridArray + grid: GridArray editable: boolean - exists: boolean found?: boolean offset: number onClick: (position: number) => void + updateUncap: (id: string, position: number, uncap: number) => void } const ExtraWeapons = (props: Props) => { @@ -34,10 +34,11 @@ const ExtraWeapons = (props: Props) => {
  • { props.onClick(props.offset + i)}} position={props.offset + i} unitType={1} - weapon={props.grid[props.offset + i]} + gridWeapon={props.grid[props.offset + i]} + onClick={() => { props.onClick(props.offset + i)}} + updateUncap={props.updateUncap} />
  • ) diff --git a/components/Party/index.tsx b/components/Party/index.tsx index 3499bda4..99a5ab17 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -1,15 +1,14 @@ -import React, { ChangeEvent, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useCookies } from 'react-cookie' -import api from '~utils/api' -// UI Elements import PartySegmentedControl from '~components/PartySegmentedControl' - -// Grids import WeaponGrid from '~components/WeaponGrid' import SummonGrid from '~components/SummonGrid' import CharacterGrid from '~components/CharacterGrid' +import api from '~utils/api' +import './index.scss' + // GridType enum GridType { Class, @@ -17,96 +16,58 @@ enum GridType { Weapon, Summon } -export { GridType } - -import './index.scss' +// Props interface Props { partyId?: string - mainWeapon?: Weapon - mainSummon?: Summon - friendSummon?: Summon - characters?: GridArray - weapons?: GridArray - summons?: GridArray + mainWeapon?: GridWeapon + mainSummon?: GridSummon + friendSummon?: GridSummon + characters?: GridArray + weapons?: GridArray + summons?: GridArray extra: boolean editable: boolean - exists: boolean pushHistory?: (path: string) => void } const Party = (props: Props) => { + // Cookies const [cookies, _] = useCookies(['user']) - const headers = (cookies.user != null) ? { headers: { 'Authorization': `Bearer ${cookies.user.access_token}` } } : {} - // Grid data - const [characters, setCharacters] = useState>({}) - const [weapons, setWeapons] = useState>({}) - const [summons, setSummons] = useState>({}) - - const [mainWeapon, setMainWeapon] = useState() - const [mainSummon, setMainSummon] = useState() - const [friendSummon, setFriendSummon] = useState() - + // Set up states const [extra, setExtra] = useState(false) - - useEffect(() => { - setPartyId(props.partyId || '') - setMainWeapon(props.mainWeapon) - setMainSummon(props.mainSummon) - setFriendSummon(props.friendSummon) - setCharacters(props.characters || {}) - setWeapons(props.weapons || {}) - setSummons(props.summons || {}) - setExtra(props.extra || false) - }, [props.partyId, props.mainWeapon, props.mainSummon, props.friendSummon, props.characters, props.weapons, props.summons, props.extra]) - - const weaponGrid = ( - - ) - - const summonGrid = ( - - ) - - const characterGrid = ( - - ) - const [currentTab, setCurrentTab] = useState(GridType.Weapon) - const [partyId, setPartyId] = useState('') + // Set states from props + useEffect(() => { + setExtra(props.extra || false) + }, [props]) + + // Methods: Creating a new party + async function createParty() { + let body = { + party: { + ...(cookies.user) && { user_id: cookies.user.user_id }, + is_extra: extra + } + } + + return await api.endpoints.parties.create(body, headers) + } + + // Methods: Updating the party's extra flag + // Note: This doesn't save to the server yet. function checkboxChanged(event: React.ChangeEvent) { setExtra(event.target.checked) } + // Methods: Navigating with segmented control function segmentClicked(event: React.ChangeEvent) { switch(event.target.value) { case 'class': @@ -126,166 +87,66 @@ const Party = (props: Props) => { } } - function itemSelected(type: GridType, item: Character | Weapon | Summon, position: number) { - if (!partyId) { - createParty() - .then(response => { - return response.data.party - }) - .then(party => { - if (props.pushHistory) { - props.pushHistory(`/p/${party.shortcode}`) - } + // Render: JSX components + const navigation = ( + + ) - return party.id - }) - .then(partyId => { - setPartyId(partyId) - saveItem(partyId, type, item, position) - }) - } else { - saveItem(partyId, type, item, position) - } - } + const weaponGrid = ( + + ) - async function createParty() { - const body = (!cookies.user) ? { - party: { - is_extra: extra - } - } : { - party: { - user_id: cookies.user.userId, - is_extra: extra - } - } + const summonGrid = ( + + ) - return await api.endpoints.parties.create(body, headers) - } + const characterGrid = ( + + ) - function saveItem(partyId: string, type: GridType, item: Character | Weapon | Summon, position: number) { - switch(type) { - case GridType.Class: - saveClass() - break + const currentGrid = () => { + switch(currentTab) { case GridType.Character: - const character = item as Character - saveCharacter(character, position, partyId) - .then(() => { - storeCharacter(character, position) - }) - break + return characterGrid case GridType.Weapon: - const weapon = item as Weapon - saveWeapon(weapon, position, partyId) - .then(() => { - storeWeapon(weapon, position) - }) - break + return weaponGrid case GridType.Summon: - const summon = item as Summon - saveSummon(summon, position, partyId) - .then(() => { - storeSummon(summon, position) - }) - break + return summonGrid } } - - // Weapons - function storeWeapon(weapon: Weapon, position: number) { - if (position == -1) { - setMainWeapon(weapon) - } else { - // Store the grid unit weapon at the correct position - let newWeapons = Object.assign({}, weapons) - newWeapons[position] = weapon - setWeapons(newWeapons) - } - } - - async function saveWeapon(weapon: Weapon, position: number, party: string) { - await api.endpoints.weapons.create({ - 'weapon': { - 'party_id': party, - 'weapon_id': weapon.id, - 'position': position, - 'mainhand': (position == -1) - } - }, headers) - } - - // Summons - function storeSummon(summon: Summon, position: number) { - if (position == -1) { - setMainSummon(summon) - } else if (position == 6) { - setFriendSummon(summon) - } else { - // Store the grid unit summon at the correct position - let newSummons = Object.assign({}, summons) - newSummons[position] = summon - setSummons(newSummons) - } - } - - async function saveSummon(summon: Summon, position: number, party: string) { - await api.endpoints.summons.create({ - 'summon': { - 'party_id': party, - 'summon_id': summon.id, - 'position': position, - 'main': (position == -1), - 'friend': (position == 6) - } - }, headers) - } - - // Character - function storeCharacter(character: Character, position: number) { - // Store the grid unit character at the correct position - let newCharacters = Object.assign({}, characters) - newCharacters[position] = character - setCharacters(newCharacters) - } - - async function saveCharacter(character: Character, position: number, party: string) { - await api.endpoints.characters.create({ - 'character': { - 'party_id': party, - 'character_id': character.id, - 'position': position - } - }, headers) - } - - // Class - function saveClass() { - // TODO: Implement this - } return (
    - - - { - (() => { - switch(currentTab) { - case GridType.Character: - return characterGrid - case GridType.Weapon: - return weaponGrid - case GridType.Summon: - return summonGrid - } - })() - } + { navigation } + { currentGrid() }
    ) } diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx index 97cbf927..476a6292 100644 --- a/components/SummonGrid/index.tsx +++ b/components/SummonGrid/index.tsx @@ -1,10 +1,16 @@ -import React, { useState } from 'react' +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useCookies } from 'react-cookie' import { useModal as useModal } from '~utils/useModal' -import SearchModal from '~components/SearchModal' -import ExtraSummons from '~components/ExtraSummons' -import SummonUnit from '~components/SummonUnit' +import { AxiosResponse } from 'axios' +import debounce from 'lodash.debounce' +import SearchModal from '~components/SearchModal' +import SummonUnit from '~components/SummonUnit' +import ExtraSummons from '~components/ExtraSummons' + +import api from '~utils/api' import './index.scss' // GridType @@ -17,106 +23,262 @@ export enum GridType { // Props interface Props { - userId?: string partyId?: string - main?: Summon | undefined - friend?: Summon | undefined - grid: GridArray + mainSummon: GridSummon | undefined + friendSummon: GridSummon | undefined + summons: GridArray editable: boolean - exists: boolean - found?: boolean - onSelect: (type: GridType, summon: Summon, position: number) => void + createParty: () => Promise> + pushHistory?: (path: string) => void } const SummonGrid = (props: Props) => { - const { open, openModal, closeModal } = useModal() - const [searchPosition, setSearchPosition] = useState(0) - + // Constants const numSummons: number = 4 + // Cookies + const [cookies, _] = useCookies(['user']) + const headers = (cookies.user != null) ? { + headers: { + 'Authorization': `Bearer ${cookies.user.access_token}` + } + } : {} + + // Set up state for party + const [partyId, setPartyId] = useState('') + + // Set up states for Grid data + const [summons, setSummons] = useState>({}) + const [mainSummon, setMainSummon] = useState() + const [friendSummon, setFriendSummon] = useState() + + // Set up states for Search + const { open, openModal, closeModal } = useModal() + const [itemPositionForSearch, setItemPositionForSearch] = useState(0) + + // Create a temporary state to store previous weapon uncap value + const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) + + // Create a state dictionary to store pure objects for Search + const [searchGrid, setSearchGrid] = useState>({}) + + // Initialize an array of current uncap values for each summon + useEffect(() => { + let initialPreviousUncapValues: {[key: number]: number} = {} + if (props.mainSummon) initialPreviousUncapValues[-1] = props.mainSummon.uncap_level + if (props.friendSummon) initialPreviousUncapValues[6] = props.friendSummon.uncap_level + Object.values(props.summons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + setPreviousUncapValues(initialPreviousUncapValues) + }, [props]) + + // Set states from props + useEffect(() => { + setSummons(props.summons || {}) + setMainSummon(props.mainSummon) + setFriendSummon(props.friendSummon) + }, [props]) + + // Update search grid whenever any summon is updated + useEffect(() => { + let newSearchGrid = Object.values(summons).map((o) => o.summon) + + if (mainSummon) + newSearchGrid.unshift(mainSummon.summon) + + if (friendSummon) + newSearchGrid.unshift(friendSummon.summon) + + setSearchGrid(newSearchGrid) + }, [summons, mainSummon, friendSummon]) + + // Methods: Adding an object from search function openSearchModal(position: number) { - setSearchPosition(position) + setItemPositionForSearch(position) openModal() } - function receiveSummon(summon: Summon, position: number) { - props.onSelect(GridType.Summon, summon, position) - } + function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) { + const summon = object as Summon - function sendData(object: Character | Weapon | Summon, position: number) { - if (isSummon(object)) { - receiveSummon(object, position) + if (!partyId) { + props.createParty() + .then(response => { + const party = response.data.party + if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveSummon(party.id, summon, position) + .then(response => storeGridSummon(response.data.grid_summon)) + }) + } else { + saveSummon(partyId, summon, position) + .then(response => storeGridSummon(response.data.grid_summon)) } } - function isSummon(object: Character | Weapon | Summon): object is Summon { - // There aren't really any unique fields here - return (object as Summon).granblue_id !== undefined + 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) } + function storeGridSummon(gridSummon: GridSummon) { + if (gridSummon.position == -1) { + setMainSummon(gridSummon) + } else if (gridSummon.position == 6) { + setFriendSummon(gridSummon) + } else { + // Store the grid unit at the correct position + let newSummons = Object.assign({}, summons) + newSummons[gridSummon.position] = gridSummon + setSummons(newSummons) + } + } + + // 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) + } + } + + const initiateUncapUpdate = useCallback( + (id: string, position: number, uncapLevel: number) => { + memoizeAction(id, position, uncapLevel) + + // Optimistically update UI + updateUncapLevel(position, uncapLevel) + }, [previousUncapValues, summons] + ) + + const memoizeAction = useCallback( + (id: string, position: number, uncapLevel: number) => { + debouncedAction(id, position, uncapLevel) + }, [props] + ) + + const debouncedAction = useMemo(() => + debounce((id, position, number) => { + saveUncap(id, position, number) + }, 500), [props, saveUncap] + ) + + const updateUncapLevel = (position: number, uncapLevel: number) => { + let newSummons = Object.assign({}, summons) + newSummons[position].uncap_level = uncapLevel + setSummons(newSummons) + } + + function storePreviousUncapValue(position: number) { + // Save the current value in case of an unexpected result + let newPreviousValues = {...previousUncapValues} + + if (mainSummon && position == -1) newPreviousValues[position] = mainSummon.uncap_level + else if (friendSummon && position == 6) newPreviousValues[position] = friendSummon.uncap_level + else newPreviousValues[position] = summons[position].uncap_level + + setPreviousUncapValues(newPreviousValues) + } + + // Render: JSX components + const mainSummonElement = ( +
    +
    Main Summon
    + { openSearchModal(-1) }} + updateUncap={initiateUncapUpdate} + /> +
    + ) + + const friendSummonElement = ( +
    +
    Friend Summon
    + { openSearchModal(6) }} + updateUncap={initiateUncapUpdate} + /> +
    + ) + const summonGridElement = ( +
    +
    Summons
    +
      + {Array.from(Array(numSummons)).map((x, i) => { + return (
    • + { openSearchModal(i) }} + updateUncap={initiateUncapUpdate} + /> +
    • ) + })} +
    +
    + ) + const subAuraSummonElement = ( + + ) return (
    -
    -
    Main Summon
    - { openSearchModal(0) }} - editable={props.editable} - key="grid_main_summon" - position={-1} - unitType={0} - summon={props.main} - /> -
    - -
    -
    Friend Summon
    - { openSearchModal(6) }} - editable={props.editable} - key="grid_friend_summon" - position={6} - unitType={2} - summon={props.friend} - /> -
    - -
    -
    Summons
    -
      - { - Array.from(Array(numSummons)).map((x, i) => { - return ( -
    • - { openSearchModal(i) }} - editable={props.editable} - position={i} - unitType={1} - summon={props.grid[i]} - /> -
    • - ) - }) - } -
    -
    + { mainSummonElement } + { friendSummonElement } + { summonGridElement }
    - + { subAuraSummonElement } {open ? ( diff --git a/components/SummonUnit/index.scss b/components/SummonUnit/index.scss index 614b482e..1f1492be 100644 --- a/components/SummonUnit/index.scss +++ b/components/SummonUnit/index.scss @@ -33,7 +33,12 @@ border: 1px solid rgba(0, 0, 0, 0.1); box-shadow: rgba(0, 0, 0, 0.14) 0px 0px 14px; cursor: pointer; - transform: scale(1.1, 1.1); + transform: $scale-wide; + } + + &.main.editable .SummonImage:hover, + &.friend.editable .SummonImage:hover { + transform: $scale-tall; } &.filled h3 { diff --git a/components/SummonUnit/index.tsx b/components/SummonUnit/index.tsx index 84f378c8..c76a9e92 100644 --- a/components/SummonUnit/index.tsx +++ b/components/SummonUnit/index.tsx @@ -1,16 +1,15 @@ import React, { useEffect, useState } from 'react' - import classnames from 'classnames' import UncapIndicator from '~components/UncapIndicator' - import PlusIcon from '~public/icons/plus.svg' import './index.scss' interface Props { onClick: () => void - summon: Summon | undefined + updateUncap: (id: string, position: number, uncap: number) => void + gridSummon: GridSummon | undefined position: number editable: boolean unitType: 0 | 1 | 2 @@ -25,10 +24,11 @@ const SummonUnit = (props: Props) => { 'grid': props.unitType == 1, 'friend': props.unitType == 2, 'editable': props.editable, - 'filled': (props.summon !== undefined) + 'filled': (props.gridSummon !== undefined) }) - const summon = props.summon + const gridSummon = props.gridSummon + const summon = gridSummon?.summon useEffect(() => { generateImageUrl() @@ -36,8 +36,8 @@ const SummonUnit = (props: Props) => { function generateImageUrl() { let imgSrc = "" - if (props.summon) { - const summon = props.summon! + if (props.gridSummon) { + const summon = props.gridSummon.summon! // Generate the correct source for the summon if (props.unitType == 0 || props.unitType == 2) @@ -49,6 +49,11 @@ const SummonUnit = (props: Props) => { setImageUrl(imgSrc) } + function passUncapData(uncap: number) { + if (props.gridSummon) + props.updateUncap(props.gridSummon.id, uncap) + } + return (
    @@ -56,12 +61,15 @@ const SummonUnit = (props: Props) => { {summon?.name.en} { (props.editable) ? : '' }
    + { (gridSummon) ? + uncapLevel={gridSummon?.uncap_level} + updateUncap={passUncapData} + special={false} + /> : '' }

    {summon?.name.en}

    diff --git a/components/ToggleSwitch/index.tsx b/components/ToggleSwitch/index.tsx index 379a3c8a..ddcd0e54 100644 --- a/components/ToggleSwitch/index.tsx +++ b/components/ToggleSwitch/index.tsx @@ -12,20 +12,20 @@ interface Props { const ToggleSwitch: React.FC = (props: Props) => { return (
    - - + +
    - ); + ) } export default ToggleSwitch \ No newline at end of file diff --git a/components/UncapIndicator/index.scss b/components/UncapIndicator/index.scss index ceafd8d2..431f20d1 100644 --- a/components/UncapIndicator/index.scss +++ b/components/UncapIndicator/index.scss @@ -7,4 +7,8 @@ list-style: none; margin: 0; padding: 0; + + &:hover { + cursor: pointer; + } } \ No newline at end of file diff --git a/components/UncapIndicator/index.tsx b/components/UncapIndicator/index.tsx index ee74c9d5..5eb58724 100644 --- a/components/UncapIndicator/index.tsx +++ b/components/UncapIndicator/index.tsx @@ -1,46 +1,94 @@ -import React from 'react' -import classnames from 'classnames' +import React, { useEffect, useRef, useState } from 'react' import UncapStar from '~components/UncapStar' import './index.scss' - interface Props { type: 'character' | 'weapon' | 'summon' rarity?: number uncapLevel: number flb: boolean - ulb?: boolean + ulb: boolean + special: boolean + updateUncap: (uncap: number) => void } const UncapIndicator = (props: Props) => { - let numStars + const [uncap, setUncap] = useState(props.uncapLevel) - if (props.type === 'character') { - if (props.flb) { - numStars = 5 + const numStars = setNumStars() + function setNumStars() { + let numStars + + if (props.type === 'character') { + if (props.special) { + if (props.ulb) { + numStars = 5 + } else if (props.flb) { + numStars = 4 + } else { + numStars = 3 + } + } else { + if (props.ulb) { + numStars = 6 + } else if (props.flb) { + numStars = 5 + } else { + numStars = 4 + } + } } else { - numStars = 4 - } - } else { - if (props.ulb) { - numStars = 5 - } else if (props.flb) { - numStars = 4 - } else { - numStars = 3 + if (props.ulb) { + numStars = 5 + } else if (props.flb) { + numStars = 4 + } else { + numStars = 3 + } } + + return numStars + } + + function toggleStar(index: number, empty: boolean) { + if (empty) props.updateUncap(index + 1) + else props.updateUncap(index) + } + + const transcendence = (i: number) => { + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> + } + + const ulb = (i: number) => { + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> + } + + const flb = (i: number) => { + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> + } + + const mlb = (i: number) => { + // console.log("MLB; Number of stars:", props.uncapLevel) + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> } return (
      { Array.from(Array(numStars)).map((x, i) => { - if (props.type === 'character' && i > 3 || + if (props.type === 'character' && i > 4) { + if (props.special) + return ulb(i) + else + return transcendence(i) + } else if ( + props.special && props.type === 'character' && i == 3 || + props.type === 'character' && i == 4 || props.type !== 'character' && i > 2) { - return + return flb(i) } else { - return + return mlb(i) } }) } diff --git a/components/UncapStar/index.scss b/components/UncapStar/index.scss index 7014dbef..4386c5ea 100644 --- a/components/UncapStar/index.scss +++ b/components/UncapStar/index.scss @@ -1,7 +1,55 @@ .UncapStar { - color: #FFA15E; -} + background-repeat: no-repeat; + background-size: 18px 18px; + display: block; + height: 18px; + width: 18px; -.UncapStar.uncap { - color: #65DAFF; + &:hover { + transform: scale(1.2); + } + + &.empty, + &.empty.mlb, + &.empty.flb, + &.empty.ulb, + &.empty.special { + background: url('/icons/uncap/empty.svg'); + + &:hover { + background: url('/icons/uncap/empty-hover.svg'); + } + } + + &.mlb { + background: url('/icons/uncap/yellow.svg'); + + &:hover { + background: url('/icons/uncap/yellow-hover.svg'); + } + } + + &.special { + background: url('/icons/uncap/red.svg'); + + &:hover { + background: url('/icons/uncap/red-hover.svg'); + } + } + + &.flb { + background: url('/icons/uncap/blue.svg'); + + &:hover { + background: url('/icons/uncap/blue-hover.svg'); + } + } + + &.ulb { + background: url('/icons/uncap/purple.svg'); + + &:hover { + background: url('/icons/uncap/purple-hover.svg'); + } + } } \ No newline at end of file diff --git a/components/UncapStar/index.tsx b/components/UncapStar/index.tsx index 6cc0d45e..a9459ecc 100644 --- a/components/UncapStar/index.tsx +++ b/components/UncapStar/index.tsx @@ -4,18 +4,39 @@ import classnames from 'classnames' import './index.scss' interface Props { - uncap: boolean + empty: boolean + special: boolean + flb: boolean + ulb: boolean + index: number + onClick: (index: number, empty: boolean) => void } const UncapStar = (props: Props) => { const classes = classnames({ - UncapStar: true, - 'uncap': props.uncap + UncapStar: true, + 'empty': props.empty, + 'special': props.special, + 'mlb': !props.special, + 'flb': props.flb, + 'ulb': props.ulb + }) + function clicked() { + props.onClick(props.index, props.empty) + } + return ( -
    • +
    • ) } +UncapStar.defaultProps = { + empty: false, + special: false, + flb: false, + ulb: false +} + export default UncapStar \ No newline at end of file diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index ef7b6a46..a684299a 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -1,10 +1,16 @@ -import React, { useState } from 'react' +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useCookies } from 'react-cookie' import { useModal as useModal } from '~utils/useModal' +import { AxiosResponse } from 'axios' +import debounce from 'lodash.debounce' + import SearchModal from '~components/SearchModal' import WeaponUnit from '~components/WeaponUnit' import ExtraWeapons from '~components/ExtraWeapons' +import api from '~utils/api' import './index.scss' // GridType @@ -17,95 +23,235 @@ export enum GridType { // Props interface Props { - userId?: string partyId?: string - mainhand?: Weapon | undefined - grid: GridArray + mainhand: GridWeapon | undefined + weapons: GridArray extra: boolean editable: boolean - exists: boolean - found?: boolean - onSelect: (type: GridType, weapon: Weapon, position: number) => void + createParty: () => Promise> + pushHistory?: (path: string) => void } const WeaponGrid = (props: Props) => { - const { open, openModal, closeModal } = useModal() - const [searchPosition, setSearchPosition] = useState(0) - + // Constants const numWeapons: number = 9 - const extraGrid = ( - - ) - - function receiveWeapon(weapon: Weapon, position: number) { - props.onSelect(GridType.Weapon, weapon, position) - } - - function sendData(object: Character | Weapon | Summon, position: number) { - if (isWeapon(object)) { - receiveWeapon(object, position) + // Cookies + const [cookies, _] = useCookies(['user']) + const headers = (cookies.user != null) ? { + headers: { + 'Authorization': `Bearer ${cookies.user.access_token}` } - } + } : {} - function isWeapon(object: Character | Weapon | Summon): object is Weapon { - return (object as Weapon).proficiency !== undefined - } + // Set up state for party + const [partyId, setPartyId] = useState('') + // Set up states for Grid data + const [weapons, setWeapons] = useState>({}) + const [mainWeapon, setMainWeapon] = useState() + + // Set up states for Search + const { open, openModal, closeModal } = useModal() + const [itemPositionForSearch, setItemPositionForSearch] = useState(0) + + // Create a temporary state to store previous weapon uncap values + const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) + + // Create a state dictionary to store pure objects for Search + const [searchGrid, setSearchGrid] = useState>({}) + + // Set states from props + useEffect(() => { + setPartyId(props.partyId || '') + setWeapons(props.weapons || {}) + setMainWeapon(props.mainhand) + }, [props]) + + // Initialize an array of current uncap values for each weapon + useEffect(() => { + let initialPreviousUncapValues: {[key: number]: number} = {} + if (props.mainhand) initialPreviousUncapValues[-1] = props.mainhand.uncap_level + Object.values(props.weapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + setPreviousUncapValues(initialPreviousUncapValues) + }, [props]) + + // Update search grid whenever weapons or the mainhand are updated + useEffect(() => { + let newSearchGrid = Object.values(weapons).map((o) => o.weapon) + + if (mainWeapon) + newSearchGrid.unshift(mainWeapon.weapon) + + setSearchGrid(newSearchGrid) + }, [weapons, mainWeapon]) + + // Methods: Adding an object from search function openSearchModal(position: number) { - setSearchPosition(position) + setItemPositionForSearch(position) openModal() } + function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) { + const weapon = object as Weapon + + if (!partyId) { + props.createParty() + .then(response => { + const party = response.data.party + setPartyId(party.id) + + if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveWeapon(party.id, weapon, position) + .then(response => storeGridWeapon(response.data.grid_weapon)) + }) + } else { + saveWeapon(partyId, weapon, position) + .then(response => storeGridWeapon(response.data.grid_weapon)) + } + } + + 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) { + setMainWeapon(gridWeapon) + } else { + // Store the grid unit at the correct position + let newWeapons = Object.assign({}, weapons) + newWeapons[gridWeapon.position] = gridWeapon + setWeapons(newWeapons) + } + } + + // 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) + } + } + + 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 (mainWeapon && position == -1) { + mainWeapon.uncap_level = uncapLevel + setMainWeapon(mainWeapon) + } else { + let newWeapons = Object.assign({}, weapons) + newWeapons[position].uncap_level = uncapLevel + setWeapons(newWeapons) + } + } + + function storePreviousUncapValue(position: number) { + // Save the current value in case of an unexpected result + let newPreviousValues = {...previousUncapValues} + newPreviousValues[position] = (mainWeapon && position == -1) ? mainWeapon.uncap_level : weapons[position].uncap_level + setPreviousUncapValues(newPreviousValues) + } + + // Render: JSX components + const mainhandElement = ( + { openSearchModal(-1) }} + updateUncap={initiateUncapUpdate} + /> + ) + + const weaponGridElement = ( + Array.from(Array(numWeapons)).map((x, i) => { + return ( +
    • + { openSearchModal(i) }} + updateUncap={initiateUncapUpdate} + /> +
    • + ) + }) + ) + + const extraGridElement = ( + + ) + return (
      - { openSearchModal(-1) }} - editable={props.editable} - key="grid_mainhand" - position={-1} - unitType={0} - weapon={props.mainhand} - /> - -
        - { - Array.from(Array(numWeapons)).map((x, i) => { - return ( -
      • - { openSearchModal(i) }} - editable={props.editable} - position={i} - unitType={1} - weapon={props.grid[i]} - /> -
      • - ) - }) - } -
      + { mainhandElement } +
        { weaponGridElement }
      - { (() => { - if(props.extra) { - return extraGrid - } - })() } + { (() => { return (props.extra) ? extraGridElement : '' })() } {open ? ( diff --git a/components/WeaponUnit/index.scss b/components/WeaponUnit/index.scss index 93c26597..0b3fc9d4 100644 --- a/components/WeaponUnit/index.scss +++ b/components/WeaponUnit/index.scss @@ -11,6 +11,7 @@ display: flex; align-items: center; justify-content: center; + margin-bottom: 2px; overflow: hidden; transition: all 0.18s ease-in-out; } @@ -19,7 +20,11 @@ border: 1px solid rgba(0, 0, 0, 0.1); box-shadow: rgba(0, 0, 0, 0.14) 0px 0px 14px; cursor: pointer; - transform: scale(1.1, 1.1); + transform: $scale-wide; +} + +.WeaponUnit.mainhand.editable .WeaponImage:hover { + transform: $scale-tall; } .WeaponUnit.filled h3 { diff --git a/components/WeaponUnit/index.tsx b/components/WeaponUnit/index.tsx index 17e231dc..6f2ef0c3 100644 --- a/components/WeaponUnit/index.tsx +++ b/components/WeaponUnit/index.tsx @@ -1,19 +1,18 @@ import React, { useEffect, useState } from 'react' - import classnames from 'classnames' import UncapIndicator from '~components/UncapIndicator' - import PlusIcon from '~public/icons/plus.svg' import './index.scss' interface Props { - onClick: () => void - weapon: Weapon | undefined + gridWeapon: GridWeapon | undefined + unitType: 0 | 1 position: number editable: boolean - unitType: 0 | 1 + onClick: () => void + updateUncap: (id: string, position: number, uncap: number) => void } const WeaponUnit = (props: Props) => { @@ -24,10 +23,11 @@ const WeaponUnit = (props: Props) => { 'mainhand': props.unitType == 0, 'grid': props.unitType == 1, 'editable': props.editable, - 'filled': (props.weapon !== undefined) + 'filled': (props.gridWeapon !== undefined) }) - const weapon = props.weapon + const gridWeapon = props.gridWeapon + const weapon = gridWeapon?.weapon useEffect(() => { generateImageUrl() @@ -35,8 +35,8 @@ const WeaponUnit = (props: Props) => { function generateImageUrl() { let imgSrc = "" - if (props.weapon) { - const weapon = props.weapon! + if (props.gridWeapon) { + const weapon = props.gridWeapon.weapon! if (props.unitType == 0) imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg` @@ -47,6 +47,11 @@ const WeaponUnit = (props: Props) => { setImageUrl(imgSrc) } + function passUncapData(uncap: number) { + if (props.gridWeapon) + props.updateUncap(props.gridWeapon.id, props.position, uncap) + } + return (
      @@ -54,13 +59,17 @@ const WeaponUnit = (props: Props) => { {weapon?.name.en} { (props.editable) ? : '' }
      -

      {weapon?.name.en}

      + { (gridWeapon) ? + : '' + }
      ) diff --git a/package-lock.json b/package-lock.json index 26a22fd7..5d16aaeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "@radix-ui/react-label": "^0.1.4", "@radix-ui/react-switch": "^0.1.4", "@svgr/webpack": "^6.2.0", + "@types/axios": "^0.14.0", "axios": "^0.25.0", "classnames": "^2.3.1", + "lodash.debounce": "^4.0.8", "meyer-reset-scss": "^2.0.4", "next": "12.0.8", "react": "17.0.2", @@ -23,6 +25,7 @@ "sass": "^1.49.0" }, "devDependencies": { + "@types/lodash.debounce": "^4.0.6", "@types/node": "17.0.11", "@types/react": "17.0.38", "@types/react-dom": "^17.0.11", @@ -2980,6 +2983,15 @@ "node": ">=10.13.0" } }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/cookie": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", @@ -3000,6 +3012,21 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz", + "integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.11.tgz", @@ -8737,6 +8764,14 @@ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" }, + "@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", + "requires": { + "axios": "*" + } + }, "@types/cookie": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", @@ -8757,6 +8792,21 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, + "@types/lodash.debounce": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz", + "integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/node": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.11.tgz", diff --git a/package.json b/package.json index f3d377f4..898e39c7 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "@radix-ui/react-label": "^0.1.4", "@radix-ui/react-switch": "^0.1.4", "@svgr/webpack": "^6.2.0", + "@types/axios": "^0.14.0", "axios": "^0.25.0", "classnames": "^2.3.1", + "lodash.debounce": "^4.0.8", "meyer-reset-scss": "^2.0.4", "next": "12.0.8", "react": "17.0.2", @@ -28,6 +30,7 @@ "sass": "^1.49.0" }, "devDependencies": { + "@types/lodash.debounce": "^4.0.6", "@types/node": "17.0.11", "@types/react": "17.0.38", "@types/react-dom": "^17.0.11", diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index 9b85cd4e..1ee727cf 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -22,13 +22,13 @@ const PartyRoute: React.FC = () => { const [loading, setLoading] = useState(true) const [editable, setEditable] = useState(false) - const [characters, setCharacters] = useState>({}) - const [weapons, setWeapons] = useState>({}) - const [summons, setSummons] = useState>({}) + const [characters, setCharacters] = useState>({}) + const [weapons, setWeapons] = useState>({}) + const [summons, setSummons] = useState>({}) - const [mainWeapon, setMainWeapon] = useState() - const [mainSummon, setMainSummon] = useState() - const [friendSummon, setFriendSummon] = useState() + const [mainWeapon, setMainWeapon] = useState() + const [mainSummon, setMainSummon] = useState() + const [friendSummon, setFriendSummon] = useState() const [partyId, setPartyId] = useState('') const [extra, setExtra] = useState(false) @@ -73,39 +73,39 @@ const PartyRoute: React.FC = () => { } function populateCharacters(list: [GridCharacter]) { - let characters: GridArray = {} + let characters: GridArray = {} list.forEach((object: GridCharacter) => { if (object.position != null) - characters[object.position] = object.character + characters[object.position] = object }) return characters } function populateWeapons(list: [GridWeapon]) { - let weapons: GridArray = {} + let weapons: GridArray = {} list.forEach((object: GridWeapon) => { if (object.mainhand) - setMainWeapon(object.weapon) + setMainWeapon(object) else if (!object.mainhand && object.position != null) - weapons[object.position] = object.weapon + weapons[object.position] = object }) return weapons } function populateSummons(list: [GridSummon]) { - let summons: GridArray = {} + let summons: GridArray = {} list.forEach((object: GridSummon) => { if (object.main) - setMainSummon(object.summon) + setMainSummon(object) else if (object.friend) - setFriendSummon(object.summon) + setFriendSummon(object) else if (!object.main && !object.friend && object.position != null) - summons[object.position] = object.summon + summons[object.position] = object }) return summons @@ -129,7 +129,6 @@ const PartyRoute: React.FC = () => { weapons={weapons} summons={summons} editable={editable} - exists={found} extra={extra} /> diff --git a/public/icons/uncap/blue-hover.svg b/public/icons/uncap/blue-hover.svg new file mode 100644 index 00000000..cb846567 --- /dev/null +++ b/public/icons/uncap/blue-hover.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/blue.svg b/public/icons/uncap/blue.svg new file mode 100644 index 00000000..2469d0de --- /dev/null +++ b/public/icons/uncap/blue.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/empty-hover.svg b/public/icons/uncap/empty-hover.svg new file mode 100644 index 00000000..6debb5dd --- /dev/null +++ b/public/icons/uncap/empty-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/empty.svg b/public/icons/uncap/empty.svg new file mode 100644 index 00000000..9aad37c9 --- /dev/null +++ b/public/icons/uncap/empty.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/purple-hover.svg b/public/icons/uncap/purple-hover.svg new file mode 100644 index 00000000..b4d08a3e --- /dev/null +++ b/public/icons/uncap/purple-hover.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/purple.svg b/public/icons/uncap/purple.svg new file mode 100644 index 00000000..93a1147c --- /dev/null +++ b/public/icons/uncap/purple.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/red-hover.svg b/public/icons/uncap/red-hover.svg new file mode 100644 index 00000000..3ed4a968 --- /dev/null +++ b/public/icons/uncap/red-hover.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/red.svg b/public/icons/uncap/red.svg new file mode 100644 index 00000000..c5da53d5 --- /dev/null +++ b/public/icons/uncap/red.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/yellow-hover.svg b/public/icons/uncap/yellow-hover.svg new file mode 100644 index 00000000..c6a6d5dd --- /dev/null +++ b/public/icons/uncap/yellow-hover.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/uncap/yellow.svg b/public/icons/uncap/yellow.svg new file mode 100644 index 00000000..70b8ece3 --- /dev/null +++ b/public/icons/uncap/yellow.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/styles/variables.scss b/styles/variables.scss index b5caf4c7..cb23afef 100644 --- a/styles/variables.scss +++ b/styles/variables.scss @@ -26,4 +26,8 @@ $bold: 600; $font-small: 12px; $font-regular: 14px; $font-large: 18px; -$font-xlarge: 21px; \ No newline at end of file +$font-xlarge: 21px; + +// Scale factors +$scale-wide: scale(1.05, 1.05); +$scale-tall: scale(1.012, 1.012); \ No newline at end of file diff --git a/types/Character.d.ts b/types/Character.d.ts index dc7a1ffd..e004581e 100644 --- a/types/Character.d.ts +++ b/types/Character.d.ts @@ -21,6 +21,7 @@ interface Character { } uncap: { flb: boolean + ulb: boolean } race: { race1: number @@ -31,4 +32,5 @@ interface Character { proficiency2: number } position?: number + special: boolean } \ No newline at end of file diff --git a/types/GridCharacter.d.ts b/types/GridCharacter.d.ts index 0f3dd5f3..47784b17 100644 --- a/types/GridCharacter.d.ts +++ b/types/GridCharacter.d.ts @@ -1,5 +1,6 @@ interface GridCharacter { id: string - position: number | null + position: number character: Character + uncap_level: number } \ No newline at end of file diff --git a/types/GridSummon.d.ts b/types/GridSummon.d.ts index 2f366574..0ce8c2fc 100644 --- a/types/GridSummon.d.ts +++ b/types/GridSummon.d.ts @@ -2,6 +2,7 @@ interface GridSummon { id: string main: boolean friend: boolean - position: number | null + position: number summon: Summon + uncap_level: number } \ No newline at end of file diff --git a/types/GridWeapon.d.ts b/types/GridWeapon.d.ts index 2c329833..3a6c2aa4 100644 --- a/types/GridWeapon.d.ts +++ b/types/GridWeapon.d.ts @@ -1,6 +1,7 @@ interface GridWeapon { id: string mainhand: boolean - position: number | null + position: number weapon: Weapon + uncap_level: number } \ No newline at end of file diff --git a/utils/api.tsx b/utils/api.tsx index a5490811..11391545 100644 --- a/utils/api.tsx +++ b/utils/api.tsx @@ -58,12 +58,23 @@ class Api { return axios.get(url) } - check(resource: string, value: string) { + check(resource: 'username'|'email', value: string) { const resourceUrl = `${this.url}/check/${resource}` return axios.post(resourceUrl, { [resource]: value }) } + + updateUncap(resource: 'character'|'weapon'|'summon', id: string, value: number) { + const pluralized = resource + 's' + const resourceUrl = `${this.url}/${pluralized}/update_uncap` + return axios.post(resourceUrl, { + [resource]: { + id: id, + uncap_level: value + } + }) + } } const api: Api = new Api({ url: process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/api/v1'})