diff --git a/components/AboutModal/index.scss b/components/AboutModal/index.scss
index 2a96b969..32f12aad 100644
--- a/components/AboutModal/index.scss
+++ b/components/AboutModal/index.scss
@@ -1,10 +1,21 @@
-.AboutModal p {
- font-size: $font-regular;
- line-height: 1.24;
- margin: 0;
- margin-bottom: $unit * 2;
+.About.Dialog {
+ width: $unit * 60;
- &:last-of-type {
- margin-bottom: 0;
+ section {
+ margin-bottom: $unit;
+
+ h2 {
+ margin-bottom: $unit * 3;
+ }
+ }
+
+ .DialogDescription {
+ font-size: $font-regular;
+ line-height: 1.24;
+ margin-bottom: $unit;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
}
}
diff --git a/components/AboutModal/index.tsx b/components/AboutModal/index.tsx
index 6c01e75e..afcdbdba 100644
--- a/components/AboutModal/index.tsx
+++ b/components/AboutModal/index.tsx
@@ -1,35 +1,55 @@
import React from 'react'
-import { createPortal } from 'react-dom'
-import api from '~utils/api'
-
-import Modal from '~components/Modal'
-import Overlay from '~components/Overlay'
+import * as Dialog from '@radix-ui/react-dialog'
+import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
-interface Props {
- close: () => void
-}
-
-const AboutModal = (props: Props) => {
+const AboutModal = () => {
return (
- createPortal(
-
-
{} }
- >
-
-
Siero is a tool to save and share parties for Granblue Fantasy.
-
All you need to do to get started is start adding things. Siero will make a URL and you can share your party with the world.
-
If you want to save your parties for safe keeping or to edit them later, you can make a free account.
+
+
+ About
+
+
+ event.preventDefault() }>
+
+ About
+
+
+
+
+
-
-
- ,
- document.body
- )
+
+
+
+ Granblue.team is a tool to save and share team compositions for Granblue Fantasy.
+
+
+ Start adding things to a team and a URL will be created for you to share it wherever you like, no account needed.
+
+
+ You can make an account to save any teams you find for future reference, or to keep all of your teams together in one place.
+
+
+
+
+
+
+ Open Source
+
+ This app is open source. You can contribute on Github.
+
+
+
+
+
+
)
}
diff --git a/components/BottomHeader/index.tsx b/components/BottomHeader/index.tsx
new file mode 100644
index 00000000..013b1f2a
--- /dev/null
+++ b/components/BottomHeader/index.tsx
@@ -0,0 +1,86 @@
+import React, { MouseEventHandler, useContext, useEffect, useState } from 'react'
+import { useCookies } from 'react-cookie'
+import { useRouter } from 'next/router'
+
+import AppContext from '~context/AppContext'
+
+import * as AlertDialog from '@radix-ui/react-alert-dialog';
+
+import Header from '~components/Header'
+import Button from '~components/Button'
+
+import { ButtonType } from '~utils/enums'
+import CrossIcon from '~public/icons/Cross.svg'
+
+
+const BottomHeader = () => {
+ const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext)
+
+ const [username, setUsername] = useState(undefined)
+ const [cookies, _, removeCookie] = useCookies(['user'])
+
+ const router = useRouter()
+
+ useEffect(() => {
+ if (cookies.user) {
+ setAuthenticated(true)
+ setUsername(cookies.user.username)
+ } else {
+ setAuthenticated(false)
+ }
+ }, [cookies, setUsername, setAuthenticated])
+
+ function deleteTeam(event: React.MouseEvent) {
+ // TODO: Implement deleting teams
+ console.log("Deleting team...")
+ }
+
+ const leftNav = () => {
+ return (
+ {}}>Add more info
+ )
+ }
+
+ const rightNav = () => {
+ if (editable && router.route === '/p/[party]') {
+ return (
+
+
+
+
+
+ Delete team
+
+
+
+
+
+ Delete team
+
+
+ Are you sure you want to permanently delete this team?
+
+
+
Nevermind
+
deleteTeam(e)}>Yes, delete
+
+
+
+
+ )
+ } else {
+ return (
)
+ }
+ }
+
+
+ return (
+
+ )
+}
+
+export default BottomHeader
\ No newline at end of file
diff --git a/components/Button/index.scss b/components/Button/index.scss
index 818625be..666d1800 100644
--- a/components/Button/index.scss
+++ b/components/Button/index.scss
@@ -16,7 +16,33 @@
color: $grey-00;
.icon svg {
- fill: $grey-50;
+ fill: $grey-00;
+ }
+
+ .icon.stroke svg {
+ fill: none;
+ stroke: $grey-00;
+ }
+ }
+
+ &.destructive:hover {
+ background: $error;
+ color: white;
+
+ .icon svg {
+ fill: white;
+ }
+ }
+
+ &.modal:hover {
+ background: $grey-90;
+ }
+
+ &.modal.destructive {
+ color: $error;
+
+ &:hover {
+ color: darken($error, 10)
}
}
@@ -28,6 +54,11 @@
height: 12px;
width: 12px;
}
+
+ &.stroke svg {
+ fill: none;
+ stroke: $grey-50;
+ }
}
&.btn-blue {
diff --git a/components/Button/index.tsx b/components/Button/index.tsx
index b24b1550..d355730f 100644
--- a/components/Button/index.tsx
+++ b/components/Button/index.tsx
@@ -1,15 +1,22 @@
import React from 'react'
import classNames from 'classnames'
+import Link from 'next/link'
+
import AddIcon from '~public/icons/Add.svg'
+import CrossIcon from '~public/icons/Cross.svg'
+import EditIcon from '~public/icons/Edit.svg'
+import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg'
import './index.scss'
+import { ButtonType } from '~utils/enums'
+
interface Props {
- color: string
disabled: boolean
- type: string | null
+ icon: string | null
+ type: ButtonType
click: any
}
@@ -19,9 +26,9 @@ interface State {
class Button extends React.Component {
static defaultProps: Props = {
- color: 'grey',
disabled: false,
- type: null,
+ icon: null,
+ type: ButtonType.Base,
click: () => {}
}
@@ -34,17 +41,25 @@ class Button extends React.Component {
render() {
let icon
- if (this.props.type === 'new') {
+ if (this.props.icon === 'new') {
icon =
- } else if (this.props.type === 'menu') {
+ } else if (this.props.icon === 'menu') {
icon =
- } else if (this.props.type === 'link') {
+ } else if (this.props.icon === 'link') {
+ icon =
+
+
+ } else if (this.props.icon === 'cross') {
icon =
-
+
+
+ } else if (this.props.icon === 'edit') {
+ icon =
+
}
@@ -52,7 +67,7 @@ class Button extends React.Component {
Button: true,
'btn-pressed': this.state.isPressed,
'btn-disabled': this.props.disabled,
- [`btn-${this.props.color}`]: true
+ 'destructive': this.props.type == ButtonType.Destructive
})
return
diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx
index 5a9b4064..025df637 100644
--- a/components/CharacterGrid/index.tsx
+++ b/components/CharacterGrid/index.tsx
@@ -1,18 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
-import { useModal as useModal } from '~utils/useModal'
+import { useSnapshot } from 'valtio'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
-import AppContext from '~context/AppContext'
-import PartyContext from '~context/PartyContext'
-
import CharacterUnit from '~components/CharacterUnit'
import SearchModal from '~components/SearchModal'
import api from '~utils/api'
+import state from '~utils/state'
+
import './index.scss'
// Props
@@ -35,44 +34,28 @@ const CharacterGrid = (props: Props) => {
} : {}
// Set up state for view management
+ const { party, grid } = useSnapshot(state)
+
+ const [slug, setSlug] = useState()
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
- const { id, setId } = useContext(PartyContext)
- const { slug, setSlug } = useContext(PartyContext)
- const { editable, setEditable } = useContext(AppContext)
-
- // 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>({})
// Fetch data from the server
useEffect(() => {
const shortcode = (props.slug) ? props.slug : slug
if (shortcode) fetchGrid(shortcode)
- else setEditable(true)
+ else state.party.editable = true
}, [slug, props.slug])
// Initialize an array of current uncap values for each characters
useEffect(() => {
let initialPreviousUncapValues: {[key: number]: number} = {}
- Object.values(characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
+ Object.values(state.grid.characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
setPreviousUncapValues(initialPreviousUncapValues)
- }, [characters])
-
- // Update search grid whenever characters are updated
- useEffect(() => {
- let newSearchGrid = Object.values(characters).map((o) => o.character)
- setSearchGrid(newSearchGrid)
- }, [characters])
+ }, [state.grid.characters])
// Methods: Fetching an object from the server
async function fetchGrid(shortcode: string) {
@@ -90,11 +73,12 @@ const CharacterGrid = (props: Props) => {
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
- setEditable(true)
+ party.editable = true
}
// Store the important party and state-keeping values
- setId(party.id)
+ state.party.id = party.id
+
setFound(true)
setLoading(false)
@@ -114,60 +98,48 @@ const CharacterGrid = (props: Props) => {
}
function populateCharacters(list: [GridCharacter]) {
- let characters: GridArray = {}
-
list.forEach((object: GridCharacter) => {
if (object.position != null)
- characters[object.position] = object
+ state.grid.characters[object.position] = object
})
-
- setCharacters(characters)
}
-
// Methods: Adding an object from search
- function openSearchModal(position: number) {
- setItemPositionForSearch(position)
- openModal()
- }
-
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
const character = object as Character
- if (!id) {
+ if (!party.id) {
props.createParty()
.then(response => {
const party = response.data.party
- setId(party.id)
+ state.party.id = party.id
setSlug(party.shortcode)
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 {
- saveCharacter(id, character, position)
+ 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) {
+ 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)
+ state.grid.characters[gridCharacter.position] = gridCharacter
}
// Methods: Helpers
@@ -209,39 +181,37 @@ const CharacterGrid = (props: Props) => {
}
}
- const initiateUncapUpdate = useCallback(
- (id: string, position: number, uncapLevel: number) => {
- memoizeAction(id, position, uncapLevel)
+ function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
+ memoizeAction(id, position, uncapLevel)
- // Optimistically update UI
- updateUncapLevel(position, uncapLevel)
- }, [previousUncapValues, characters]
- )
+ // Optimistically update UI
+ updateUncapLevel(position, uncapLevel)
+ }
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
- }, [characters]
+ }, [props, previousUncapValues]
)
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
- }, 500), [characters, saveUncap]
+ }, 500), [props, saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
- let newCharacters = {...characters}
- newCharacters[position].uncap_level = uncapLevel
- setCharacters(newCharacters)
+ state.grid.characters[position].uncap_level = uncapLevel
}
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)
+ if (grid.characters[position]) {
+ newPreviousValues[position] = grid.characters[position].uncap_level
+ setPreviousUncapValues(newPreviousValues)
+ }
}
// Render: JSX components
@@ -252,26 +222,15 @@ const CharacterGrid = (props: Props) => {
return (
{ openSearchModal(i) }}
+ updateObject={receiveCharacterFromSearch}
updateUncap={initiateUncapUpdate}
/>
)
})}
-
- {open ? (
-
- ) : null}
)
diff --git a/components/CharacterUnit/index.tsx b/components/CharacterUnit/index.tsx
index 0d07b81c..945c1e30 100644
--- a/components/CharacterUnit/index.tsx
+++ b/components/CharacterUnit/index.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'
import classnames from 'classnames'
+import SearchModal from '~components/SearchModal'
import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/Add.svg'
@@ -10,7 +11,7 @@ interface Props {
gridCharacter: GridCharacter | undefined
position: number
editable: boolean
- onClick: () => void
+ updateObject: (object: Character | Weapon | Summon, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
@@ -24,7 +25,7 @@ const CharacterUnit = (props: Props) => {
})
const gridCharacter = props.gridCharacter
- const character = gridCharacter?.character
+ const character = gridCharacter?.object
useEffect(() => {
generateImageUrl()
@@ -34,7 +35,7 @@ const CharacterUnit = (props: Props) => {
let imgSrc = ""
if (props.gridCharacter) {
- const character = props.gridCharacter.character!
+ const character = props.gridCharacter.object!
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg`
}
@@ -49,10 +50,17 @@ const CharacterUnit = (props: Props) => {
return (
-
{} }>
-
- { (props.editable) ?
: '' }
-
+
+
+
+ { (props.editable) ?
: '' }
+
+
+
{ (gridCharacter && character) ?
@@ -17,7 +9,7 @@ interface Props {
exists: boolean
found?: boolean
offset: number
- onClick: (position: number) => void
+ updateObject: (object: Character | Weapon | Summon, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
@@ -37,7 +29,7 @@ const ExtraSummons = (props: Props) => {
position={props.offset + i}
unitType={1}
gridSummon={props.grid[props.offset + i]}
- onClick={() => { props.onClick(props.offset + i) }}
+ updateObject={props.updateObject}
updateUncap={props.updateUncap}
/>
diff --git a/components/ExtraWeapons/index.tsx b/components/ExtraWeapons/index.tsx
index 71fcda5c..100d4ce6 100644
--- a/components/ExtraWeapons/index.tsx
+++ b/components/ExtraWeapons/index.tsx
@@ -3,21 +3,13 @@ import WeaponUnit from '~components/WeaponUnit'
import './index.scss'
-// GridType
-export enum GridType {
- Class,
- Character,
- Weapon,
- Summon
-}
-
// Props
interface Props {
grid: GridArray
editable: boolean
found?: boolean
offset: number
- onClick: (position: number) => void
+ updateObject: (object: Character | Weapon | Summon, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
@@ -37,7 +29,7 @@ const ExtraWeapons = (props: Props) => {
position={props.offset + i}
unitType={1}
gridWeapon={props.grid[props.offset + i]}
- onClick={() => { props.onClick(props.offset + i)}}
+ updateObject={props.updateObject}
updateUncap={props.updateUncap}
/>
diff --git a/components/GridRep/index.tsx b/components/GridRep/index.tsx
index 7d2b60b0..74a86f52 100644
--- a/components/GridRep/index.tsx
+++ b/components/GridRep/index.tsx
@@ -19,9 +19,9 @@ const GridRep = (props: Props) => {
for (const [key, value] of Object.entries(props.grid)) {
if (value.position == -1)
- setMainhand(value.weapon)
+ setMainhand(value.object)
else if (!value.mainhand && value.position != null)
- newWeapons[value.position] = value.weapon
+ newWeapons[value.position] = value.object
}
setWeapons(newWeapons)
diff --git a/components/Header/index.scss b/components/Header/index.scss
index 5eb0d7cc..d5f5afea 100644
--- a/components/Header/index.scss
+++ b/components/Header/index.scss
@@ -1,8 +1,13 @@
-#Header {
+.Header {
display: flex;
height: 34px;
width: 100%;
+ &.bottom {
+ position: sticky;
+ bottom: $unit * 2;
+ }
+
#right {
display: flex;
gap: 8px;
diff --git a/components/Header/index.tsx b/components/Header/index.tsx
index 0ae20284..d82d83e3 100644
--- a/components/Header/index.tsx
+++ b/components/Header/index.tsx
@@ -1,82 +1,19 @@
-import React, { useContext, useEffect, useState } from 'react'
-import { useCookies } from 'react-cookie'
-import { useRouter } from 'next/router'
-
-import AppContext from '~context/AppContext'
-
-import Button from '~components/Button'
-import HeaderMenu from '~components/HeaderMenu'
+import React from 'react'
import './index.scss'
-interface Props {}
+interface Props {
+ position: 'top' | 'bottom'
+ left: JSX.Element,
+ right: JSX.Element
+}
-const Header = (props: Props) => {
- const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext)
-
- const [username, setUsername] = useState(undefined)
- const [cookies, _, removeCookie] = useCookies(['user'])
-
- const router = useRouter()
-
- useEffect(() => {
- if (cookies.user) {
- setAuthenticated(true)
- setUsername(cookies.user.username)
- console.log(`Logged in as user "${cookies.user.username}"`)
- } else {
- setAuthenticated(false)
- console.log('You are currently not logged in.')
- }
- }, [cookies, setUsername, setAuthenticated])
-
- function copyToClipboard() {
- const el = document.createElement('input')
- el.value = window.location.href
- el.id = 'url-input'
- document.body.appendChild(el)
-
- el.select()
- document.execCommand('copy')
- el.remove()
- }
-
- function newParty() {
- router.push('/')
- }
-
- function logout() {
- removeCookie('user')
-
- setAuthenticated(false)
- if (editable) setEditable(false)
-
- // How can we log out without navigating to root
- router.push('/')
- return false
- }
-
+const Header = (props: Props) => {
return (
-
- Teams
+ Teams
@@ -46,10 +46,7 @@ const HeaderMenu = (props: Props) => {
-
About
- {aboutOpen ? (
-
- ) : null}
+
Settings
Logout
@@ -62,14 +59,11 @@ const HeaderMenu = (props: Props) => {
return (
-
About
- {aboutOpen ? (
-
- ) : null}
+
- Teams
+ Teams
diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx
index 3b78f2c8..f939c35a 100644
--- a/components/Layout/index.tsx
+++ b/components/Layout/index.tsx
@@ -1,5 +1,6 @@
import type { ReactElement } from 'react'
-import Header from '~components/Header'
+import TopHeader from '~components/TopHeader'
+import BottomHeader from '~components/BottomHeader'
interface Props {
children: ReactElement
@@ -8,8 +9,9 @@ interface Props {
const Layout = ({children}: Props) => {
return (
<>
-
+
{children}
+
>
)
}
diff --git a/components/Modal/index.scss b/components/Modal/index.scss
index cb93fc4b..acc22037 100644
--- a/components/Modal/index.scss
+++ b/components/Modal/index.scss
@@ -22,9 +22,9 @@
overflow-y: auto;
padding: $unit * 3;
position: relative;
- z-index: 10;
+ z-index: 21;
- #ModalTop {
+ #ModalHeader {
display: flex;
flex-direction: row;
align-items: center;
diff --git a/components/Overlay/index.scss b/components/Overlay/index.scss
index 1895992e..2c6a0aa2 100644
--- a/components/Overlay/index.scss
+++ b/components/Overlay/index.scss
@@ -1,12 +1,12 @@
-.Overlay {
- background: black;
- position: absolute;
- opacity: 0.28;
+// .Overlay {
+// background: black;
+// position: absolute;
+// opacity: 0.28;
- height: 100%;
- width: 100%;
+// height: 100%;
+// width: 100%;
- top: 0;
- left: 0;
- z-index: 2;
-}
\ No newline at end of file
+// top: 0;
+// left: 0;
+
+// }
\ No newline at end of file
diff --git a/components/Party/index.tsx b/components/Party/index.tsx
index fa20c988..2e6e2dce 100644
--- a/components/Party/index.tsx
+++ b/components/Party/index.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
+import { useSnapshot } from 'valtio'
import { useCookies } from 'react-cookie'
-import PartyContext from '~context/PartyContext'
import PartySegmentedControl from '~components/PartySegmentedControl'
import WeaponGrid from '~components/WeaponGrid'
@@ -9,18 +9,11 @@ import SummonGrid from '~components/SummonGrid'
import CharacterGrid from '~components/CharacterGrid'
import api from '~utils/api'
-import { TeamElement } from '~utils/enums'
+import state from '~utils/state'
+import { GridType, TeamElement } from '~utils/enums'
import './index.scss'
-// GridType
-enum GridType {
- Class,
- Character,
- Weapon,
- Summon
-}
-
// Props
interface Props {
slug?: string
@@ -37,12 +30,8 @@ const Party = (props: Props) => {
} : {}
// Set up states
+ const { party } = useSnapshot(state)
const [currentTab, setCurrentTab] = useState(GridType.Weapon)
- const [id, setId] = useState('')
- const [slug, setSlug] = useState('')
- const [element, setElement] = useState(TeamElement.Any)
- const [editable, setEditable] = useState(false)
- const [hasExtra, setHasExtra] = useState(false)
// Methods: Creating a new party
async function createParty(extra: boolean = false) {
@@ -58,10 +47,12 @@ const Party = (props: Props) => {
// Methods: Updating the party's extra flag
function checkboxChanged(event: React.ChangeEvent) {
- setHasExtra(event.target.checked)
- api.endpoints.parties.update(id, {
- 'party': { 'is_extra': event.target.checked }
- }, headers)
+ if (party.id) {
+ state.party.extra = event.target.checked
+ api.endpoints.parties.update(party.id, {
+ 'party': { 'is_extra': event.target.checked }
+ }, headers)
+ }
}
// Methods: Navigating with segmented control
@@ -130,10 +121,8 @@ const Party = (props: Props) => {
return (
-
- { navigation }
- { currentGrid() }
-
+ { navigation }
+ { currentGrid() }
)
}
diff --git a/components/PartySegmentedControl/index.tsx b/components/PartySegmentedControl/index.tsx
index 3d30e8f4..ef448d68 100644
--- a/components/PartySegmentedControl/index.tsx
+++ b/components/PartySegmentedControl/index.tsx
@@ -1,19 +1,14 @@
import React, { useContext } from 'react'
import './index.scss'
-import PartyContext from '~context/PartyContext'
+import state from '~utils/state'
import SegmentedControl from '~components/SegmentedControl'
import Segment from '~components/Segment'
import ToggleSwitch from '~components/ToggleSwitch'
-// GridType
-export enum GridType {
- Class,
- Character,
- Weapon,
- Summon
-}
+import { GridType } from '~utils/enums'
+import { useSnapshot } from 'valtio'
interface Props {
selectedTab: GridType
@@ -22,10 +17,10 @@ interface Props {
}
const PartySegmentedControl = (props: Props) => {
- const { editable, element, hasExtra } = useContext(PartyContext)
+ const { party } = useSnapshot(state)
function getElement() {
- switch(element) {
+ switch(party.element) {
case 1: return "wind"; break
case 2: return "fire"; break
case 3: return "water"; break
@@ -40,8 +35,8 @@ const PartySegmentedControl = (props: Props) => {
Extra
@@ -80,7 +75,7 @@ const PartySegmentedControl = (props: Props) => {
{
(() => {
- if (editable && props.selectedTab == GridType.Weapon) {
+ if (party.editable && props.selectedTab == GridType.Weapon) {
return extraToggle
}
})()
diff --git a/components/SearchModal/index.scss b/components/SearchModal/index.scss
index 6882a290..ce57bae8 100644
--- a/components/SearchModal/index.scss
+++ b/components/SearchModal/index.scss
@@ -1,11 +1,11 @@
-.ModalContainer .Modal.SearchModal {
+.Modal.Search {
display: flex;
flex-direction: column;
min-height: 420px;
min-width: 600px;
padding: 0;
- #ModalTop {
+ #ModalHeader {
background: $grey-90;
gap: $unit;
margin: 0;
@@ -13,12 +13,28 @@
position: sticky;
top: 0;
+ button {
+ background: transparent;
+ border: none;
+ height: 42px;
+ padding: 0;
+
+ svg {
+ height: 24px;
+ width: 24px;
+ vertical-align: middle;
+ }
+ }
+
label {
width: 100%;
.Input {
+ border: 1px solid $grey-70;
+ border-radius: $unit / 2;
box-sizing: border-box;
- padding: 12px 8px;
+ font-size: $font-regular;
+ padding: $unit * 1.5;
text-align: left;
width: 100%;
}
@@ -26,13 +42,13 @@
}
}
-.SearchModal #results_container {
+.Search.Modal #results_container {
margin: 0;
max-height: 330px;
padding: 0 12px 12px 12px;
}
-.SearchModal #NoResults {
+.Search.Modal #NoResults {
display: flex;
flex-direction: column;
align-items: center;
@@ -40,7 +56,7 @@
flex-grow: 1;
}
-.SearchModal #NoResults h2 {
+.Search.Modal #NoResults h2 {
color: #ccc;
font-size: $font-large;
font-weight: 500;
diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx
index 29181049..a9b59964 100644
--- a/components/SearchModal/index.tsx
+++ b/components/SearchModal/index.tsx
@@ -1,10 +1,11 @@
-import React from 'react'
+import React, { useEffect, useState } from 'react'
+import { useSnapshot } from 'valtio'
-import { createPortal } from 'react-dom'
+import state from '~utils/state'
import api from '~utils/api'
-import Modal from '~components/Modal'
-import Overlay from '~components/Overlay'
+import * as Dialog from '@radix-ui/react-dialog'
+
import CharacterResult from '~components/CharacterResult'
import WeaponResult from '~components/WeaponResult'
import SummonResult from '~components/SummonResult'
@@ -13,138 +14,143 @@ import './index.scss'
import PlusIcon from '~public/icons/Add.svg'
interface Props {
- close: () => void
send: (object: Character | Weapon | Summon, position: number) => any
- grid: GridArray
placeholderText: string
fromPosition: number
- object: 'weapons' | 'characters' | 'summons'
+ object: 'weapons' | 'characters' | 'summons',
+ children: React.ReactNode
}
-interface State {
- query: string,
- results: { [key: string]: any }
- loading: boolean
- message: string
- totalResults: number
-}
+const SearchModal = (props: Props) => {
+ let { grid } = useSnapshot(state)
-class SearchModal extends React.Component {
- searchInput: React.RefObject
+ let searchInput = React.createRef()
- constructor(props: Props) {
- super(props)
- this.state = {
- query: '',
- results: {},
- loading: false,
- message: '',
- totalResults: 0
- }
- this.searchInput = React.createRef()
- }
+ const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>()
+ const [open, setOpen] = useState(false)
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState({})
+ const [loading, setLoading] = useState(false)
+ const [message, setMessage] = useState('')
+ const [totalResults, setTotalResults] = useState(0)
- componentDidMount() {
- if (this.searchInput.current) {
- this.searchInput.current.focus()
- }
- }
+ useEffect(() => {
+ setObjects(grid[props.object])
+ }, [grid, props.object])
- filterExclusions = (o: Character | Weapon | Summon) => {
- if (this.props.grid[this.props.fromPosition] &&
- o.granblue_id == this.props.grid[this.props.fromPosition].granblue_id) {
- return null
- } else return o
- }
+ useEffect(() => {
+ if (searchInput.current)
+ searchInput.current.focus()
+ }, [searchInput])
- fetchResults = (query: string) => {
- const excludes = Object.values(this.props.grid).filter(this.filterExclusions).map((o) => { return o.name.en }).join(',')
+ function inputChanged(event: React.ChangeEvent) {
+ const text = event.target.value
+ if (text.length) {
+ setQuery(text)
+ setLoading(true)
+ setMessage('')
- api.search(this.props.object, query, excludes)
- .then((response) => {
- const data = response.data
- const totalResults = data.length
- this.setState({
- results: data,
- totalResults: totalResults,
- loading: false
- })
- }, (error) => {
- this.setState({
- loading: false,
- message: error
- })
- })
- }
-
- inputChanged = (event: React.ChangeEvent) => {
- const query = event.target.value
- if (query.length) {
- this.setState({ query, loading: true, message: '' }, () => {
- this.fetchResults(query)
- })
+ if (text.length > 2)
+ fetchResults()
} else {
- this.setState({ query, results: {}, message: '' })
+ setQuery('')
+ setResults({})
+ setMessage('')
}
}
+
+ function fetchResults() {
+ // Exclude objects in grid from search results
+ // unless the object is in the position that the user clicked
+ // so that users can replace object versions with
+ // compatible other objects.
+ const excludes = (objects) ? Object.values(objects)
+ .filter(filterExclusions)
+ .map((o) => { return (o.object) ? o.object.name.en : undefined }).join(',') : ''
- sendData = (result: Character | Weapon | Summon) => {
- this.props.send(result, this.props.fromPosition)
- this.props.close()
+ api.search(props.object, query, excludes)
+ .then(response => {
+ setResults(response.data)
+ setTotalResults(response.data.length)
+ setLoading(false)
+ })
+ .catch(error => {
+ setMessage(error)
+ setLoading(false)
+ })
}
- renderSearchResults = () => {
- const { results } = this.state
-
- switch(this.props.object) {
+ function filterExclusions(gridObject: GridCharacter | GridWeapon | GridSummon) {
+ if (objects && gridObject.object &&
+ gridObject.object.granblue_id === objects[props.fromPosition]?.object.granblue_id)
+ return null
+ else return gridObject
+ }
+
+ function sendData(result: Character | Weapon | Summon) {
+ props.send(result, props.fromPosition)
+ setOpen(false)
+ }
+
+ function renderResults() {
+ switch(props.object) {
case 'weapons':
- return this.renderWeaponSearchResults(results)
-
+ return renderWeaponSearchResults(results)
+ break
case 'summons':
- return this.renderSummonSearchResults(results)
-
+ return renderSummonSearchResults(results)
+ break
case 'characters':
- return this.renderCharacterSearchResults(results)
+ return renderCharacterSearchResults(results)
+ break
}
}
- renderWeaponSearchResults = (results: { [key: string]: any }) => {
- return (
-
- { results.map( (result: Weapon) => {
- return { this.sendData(result) }} />
- })}
-
- )
+ function renderWeaponSearchResults(results: { [key: string]: any }) {
+ const elements = results.map((result: Weapon) => {
+ return { sendData(result) }}
+ />
+ })
+
+ return ()
}
- renderSummonSearchResults = (results: { [key: string]: any }) => {
- return (
-
- { results.map( (result: Summon) => {
- return { this.sendData(result) }} />
- })}
-
- )
+ function renderSummonSearchResults(results: { [key: string]: any }) {
+ const elements = results.map((result: Summon) => {
+ return { sendData(result) }}
+ />
+ })
+
+ return ()
}
- renderCharacterSearchResults = (results: { [key: string]: any }) => {
- return (
-
- { results.map( (result: Character) => {
- return { this.sendData(result) }} />
- })}
-
- )
+ function renderCharacterSearchResults(results: { [key: string]: any }) {
+ const elements = results.map((result: Character) => {
+ return { sendData(result) }}
+ />
+ })
+
+ return ()
}
- renderEmptyState = () => {
+ function renderEmptyState() {
let string = ''
- if (this.state.query === '') {
- string = `No ${this.props.object}`
+ if (query === '') {
+ string = `No ${props.object}`
+ } else if (query.length < 3) {
+ string = `Type at least 3 characters`
} else {
- string = `No results found for '${this.state.query}'`
+ string = `No results found for '${query}'`
}
return (
@@ -153,48 +159,46 @@ class SearchModal extends React.Component {
)
}
-
- render() {
- const { query, loading } = this.state
-
- let content: JSX.Element
- if (Object.entries(this.state.results).length == 0) {
- content = this.renderEmptyState()
- } else {
- content = this.renderSearchResults()
- }
-
- return (
- createPortal(
- ,
- document.body
- )
- )
+
+ function resetAndClose() {
+ setQuery('')
+ setResults({})
+ setOpen(true)
}
+
+ return (
+
+
+ {props.children}
+
+
+
+
+
+ { ((Object.entries(results).length == 0) ? renderEmptyState() : renderResults()) }
+
+
+
+
+
+ )
}
export default SearchModal
\ No newline at end of file
diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx
index c19ba415..ce53a204 100644
--- a/components/SummonGrid/index.tsx
+++ b/components/SummonGrid/index.tsx
@@ -1,19 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
-import { useModal as useModal } from '~utils/useModal'
+import { useSnapshot } from 'valtio'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
-import AppContext from '~context/AppContext'
-import PartyContext from '~context/PartyContext'
-
-import SearchModal from '~components/SearchModal'
import SummonUnit from '~components/SummonUnit'
import ExtraSummons from '~components/ExtraSummons'
import api from '~utils/api'
+import state from '~utils/state'
+
import './index.scss'
// Props
@@ -36,55 +34,37 @@ const SummonGrid = (props: Props) => {
} : {}
// Set up state for view management
+ const { party, grid } = useSnapshot(state)
+
+ const [slug, setSlug] = useState()
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
- const { id, setId } = useContext(PartyContext)
- const { slug, setSlug } = useContext(PartyContext)
- const { editable, setEditable } = useContext(AppContext)
-
- // 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>({})
-
// Fetch data from the server
useEffect(() => {
const shortcode = (props.slug) ? props.slug : slug
if (shortcode) fetchGrid(shortcode)
- else setEditable(true)
+ else state.party.editable = true
}, [slug, props.slug])
// Initialize an array of current uncap values for each summon
useEffect(() => {
let initialPreviousUncapValues: {[key: number]: number} = {}
- if (mainSummon) initialPreviousUncapValues[-1] = mainSummon.uncap_level
- if (friendSummon) initialPreviousUncapValues[6] = friendSummon.uncap_level
- Object.values(summons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
+
+ if (state.grid.summons.mainSummon)
+ initialPreviousUncapValues[-1] = state.grid.summons.mainSummon.uncap_level
+
+ if (state.grid.summons.friendSummon)
+ initialPreviousUncapValues[6] = state.grid.summons.friendSummon.uncap_level
+
+ Object.values(state.grid.summons.allSummons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
+
setPreviousUncapValues(initialPreviousUncapValues)
- }, [summons, mainSummon, friendSummon])
+ }, [state.grid.summons.mainSummon, state.grid.summons.friendSummon, state.grid.summons.allSummons])
- // 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: Fetching an object from the server
async function fetchGrid(shortcode: string) {
@@ -102,11 +82,12 @@ const SummonGrid = (props: Props) => {
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
- setEditable(true)
+ state.party.editable = true
}
// Store the important party and state-keeping values
- setId(party.id)
+ state.party.id = party.id
+
setFound(true)
setLoading(false)
@@ -126,42 +107,34 @@ const SummonGrid = (props: Props) => {
}
function populateSummons(list: [GridSummon]) {
- let summons: GridArray = {}
-
- list.forEach((object: GridSummon) => {
- if (object.main)
- setMainSummon(object)
- else if (object.friend)
- setFriendSummon(object)
- else if (!object.main && !object.friend && object.position != null)
- summons[object.position] = object
+ list.forEach((gridObject: GridSummon) => {
+ if (gridObject.main)
+ state.grid.summons.mainSummon = gridObject
+ else if (gridObject.friend)
+ state.grid.summons.friendSummon = gridObject
+ else if (!gridObject.main && !gridObject.friend && gridObject.position != null)
+ state.grid.summons.allSummons[gridObject.position] = gridObject
})
-
- setSummons(summons)
}
// Methods: Adding an object from search
- function openSearchModal(position: number) {
- setItemPositionForSearch(position)
- openModal()
- }
-
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
const summon = object as Summon
- if (!id) {
+ if (!party.id) {
props.createParty()
.then(response => {
const party = response.data.party
- setId(party.id)
+ state.party.id = party.id
setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
+
saveSummon(party.id, summon, position)
.then(response => storeGridSummon(response.data.grid_summon))
})
} else {
- saveSummon(id, summon, position)
+ saveSummon(party.id, summon, position)
.then(response => storeGridSummon(response.data.grid_summon))
}
}
@@ -184,16 +157,12 @@ const SummonGrid = (props: Props) => {
}
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)
- }
+ if (gridSummon.position == -1)
+ state.grid.summons.mainSummon = gridSummon
+ else if (gridSummon.position == 6)
+ state.grid.summons.friendSummon = gridSummon
+ else
+ state.grid.summons.allSummons[gridSummon.position] = gridSummon
}
// Methods: Updating uncap level
@@ -218,40 +187,41 @@ const SummonGrid = (props: Props) => {
}
}
- const initiateUncapUpdate = useCallback(
- (id: string, position: number, uncapLevel: number) => {
- memoizeAction(id, position, uncapLevel)
+ function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
+ memoizeAction(id, position, uncapLevel)
- // Optimistically update UI
- updateUncapLevel(position, uncapLevel)
- }, [previousUncapValues, summons]
- )
+ // Optimistically update UI
+ updateUncapLevel(position, uncapLevel)
+ }
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
- }, [summons, mainSummon, friendSummon]
+ }, [props, previousUncapValues]
)
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
- }, 500), [summons, mainSummon, friendSummon, saveUncap]
+ }, 500), [props, saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
- let newSummons = Object.assign({}, summons)
- newSummons[position].uncap_level = uncapLevel
- setSummons(newSummons)
+ if (state.grid.summons.mainSummon && position == -1)
+ state.grid.summons.mainSummon.uncap_level = uncapLevel
+ else if (state.grid.summons.friendSummon && position == 6)
+ state.grid.summons.friendSummon.uncap_level = uncapLevel
+ else
+ state.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 (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
+ if (state.grid.summons.mainSummon && position == -1) newPreviousValues[position] = state.grid.summons.mainSummon.uncap_level
+ else if (state.grid.summons.friendSummon && position == 6) newPreviousValues[position] = state.grid.summons.friendSummon.uncap_level
+ else newPreviousValues[position] = state.grid.summons.allSummons[position].uncap_level
setPreviousUncapValues(newPreviousValues)
}
@@ -261,12 +231,12 @@ const SummonGrid = (props: Props) => {
Main Summon
{ openSearchModal(-1) }}
+ updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate}
/>
@@ -276,12 +246,12 @@ const SummonGrid = (props: Props) => {
Friend Summon
{ openSearchModal(6) }}
+ updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate}
/>
@@ -293,11 +263,11 @@ const SummonGrid = (props: Props) => {
{Array.from(Array(numSummons)).map((x, i) => {
return (
{ openSearchModal(i) }}
+ updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate}
/>
)
@@ -307,11 +277,11 @@ const SummonGrid = (props: Props) => {
)
const subAuraSummonElement = (
)
@@ -324,17 +294,6 @@ const SummonGrid = (props: Props) => {
{ subAuraSummonElement }
-
- {open ? (
-
- ) : null}
)
}
diff --git a/components/SummonResult/index.scss b/components/SummonResult/index.scss
index ccf99f58..d92d790f 100644
--- a/components/SummonResult/index.scss
+++ b/components/SummonResult/index.scss
@@ -12,7 +12,7 @@
background: #e9e9e9;
border-radius: 6px;
display: inline-block;
- height: 72px;
+ height: auto;
width: 120px;
}
diff --git a/components/SummonUnit/index.tsx b/components/SummonUnit/index.tsx
index 13246573..ce8cb3e5 100644
--- a/components/SummonUnit/index.tsx
+++ b/components/SummonUnit/index.tsx
@@ -1,18 +1,19 @@
import React, { useEffect, useState } from 'react'
import classnames from 'classnames'
+import SearchModal from '~components/SearchModal'
import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/Add.svg'
import './index.scss'
interface Props {
- onClick: () => void
- updateUncap: (id: string, position: number, uncap: number) => void
gridSummon: GridSummon | undefined
+ unitType: 0 | 1 | 2
position: number
editable: boolean
- unitType: 0 | 1 | 2
+ updateObject: (object: Character | Weapon | Summon, position: number) => void
+ updateUncap: (id: string, position: number, uncap: number) => void
}
const SummonUnit = (props: Props) => {
@@ -28,7 +29,7 @@ const SummonUnit = (props: Props) => {
})
const gridSummon = props.gridSummon
- const summon = gridSummon?.summon
+ const summon = gridSummon?.object
useEffect(() => {
generateImageUrl()
@@ -37,7 +38,7 @@ const SummonUnit = (props: Props) => {
function generateImageUrl() {
let imgSrc = ""
if (props.gridSummon) {
- const summon = props.gridSummon.summon!
+ const summon = props.gridSummon.object!
// Generate the correct source for the summon
if (props.unitType == 0 || props.unitType == 2)
@@ -57,19 +58,27 @@ const SummonUnit = (props: Props) => {
return (
-
{} }>
-
- { (props.editable) ?
: '' }
-
+
+
+
+ { (props.editable) ?
: '' }
+
+
+
{ (gridSummon) ?
-
: '' }
+
: ''
+ }
{summon?.name.en}
diff --git a/components/TopHeader/index.scss b/components/TopHeader/index.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/components/TopHeader/index.tsx b/components/TopHeader/index.tsx
new file mode 100644
index 00000000..fa85365f
--- /dev/null
+++ b/components/TopHeader/index.tsx
@@ -0,0 +1,89 @@
+import React, { useContext, useEffect, useState } from 'react'
+import { useCookies } from 'react-cookie'
+import { useRouter } from 'next/router'
+
+import AppContext from '~context/AppContext'
+
+import Header from '~components/Header'
+import Button from '~components/Button'
+import HeaderMenu from '~components/HeaderMenu'
+
+const TopHeader = () => {
+ const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext)
+
+ const [username, setUsername] = useState(undefined)
+ const [cookies, _, removeCookie] = useCookies(['user'])
+
+ const router = useRouter()
+
+ useEffect(() => {
+ if (cookies.user) {
+ setAuthenticated(true)
+ setUsername(cookies.user.username)
+ console.log(`Logged in as user "${cookies.user.username}"`)
+ } else {
+ setAuthenticated(false)
+ console.log('You are currently not logged in.')
+ }
+ }, [cookies, setUsername, setAuthenticated])
+
+ function copyToClipboard() {
+ const el = document.createElement('input')
+ el.value = window.location.href
+ el.id = 'url-input'
+ document.body.appendChild(el)
+
+ el.select()
+ document.execCommand('copy')
+ el.remove()
+ }
+
+ function newParty() {
+ router.push('/')
+ }
+
+ function logout() {
+ removeCookie('user')
+
+ setAuthenticated(false)
+ if (editable) setEditable(false)
+
+ // TODO: How can we log out without navigating to root
+ router.push('/')
+ return false
+ }
+
+ const leftNav = () => {
+ return (
+
+ Menu
+ { (username) ?
+ :
+
+ }
+
+ )
+ }
+
+ const rightNav = () => {
+ return (
+
+ { (router.route === '/p/[party]') ?
+ Copy link : ''
+ }
+ New
+
+ )
+ }
+
+
+ return (
+
+ )
+}
+
+export default TopHeader
\ No newline at end of file
diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx
index a15a6863..238636a1 100644
--- a/components/WeaponGrid/index.tsx
+++ b/components/WeaponGrid/index.tsx
@@ -1,19 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
-import { useModal as useModal } from '~utils/useModal'
+import { useSnapshot } from 'valtio'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
-import AppContext from '~context/AppContext'
-import PartyContext from '~context/PartyContext'
-
-import SearchModal from '~components/SearchModal'
import WeaponUnit from '~components/WeaponUnit'
import ExtraWeapons from '~components/ExtraWeapons'
import api from '~utils/api'
+import state from '~utils/state'
+
import './index.scss'
// Props
@@ -36,58 +34,33 @@ const WeaponGrid = (props: Props) => {
} : {}
// Set up state for view management
+ const { party, grid } = useSnapshot(state)
+
+ const [slug, setSlug] = useState()
const [found, setFound] = useState(false)
const [loading, setLoading] = useState(true)
-
- // Set up the party context
- const { setEditable: setAppEditable } = useContext(AppContext)
- const { id, setId } = useContext(PartyContext)
- const { slug, setSlug } = useContext(PartyContext)
- const { editable, setEditable } = useContext(PartyContext)
- const { hasExtra, setHasExtra } = useContext(PartyContext)
- const { setElement } = useContext(PartyContext)
-
- // 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>({})
-
// Fetch data from the server
useEffect(() => {
const shortcode = (props.slug) ? props.slug : slug
if (shortcode) fetchGrid(shortcode)
- else {
- setEditable(true)
- setAppEditable(true)
- }
+ else state.party.editable = true
}, [slug, props.slug])
// Initialize an array of current uncap values for each weapon
useEffect(() => {
let initialPreviousUncapValues: {[key: number]: number} = {}
- if (mainWeapon) initialPreviousUncapValues[-1] = mainWeapon.uncap_level
- Object.values(weapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
+
+ if (state.grid.weapons.mainWeapon)
+ initialPreviousUncapValues[-1] = state.grid.weapons.mainWeapon.uncap_level
+
+ Object.values(state.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
+
setPreviousUncapValues(initialPreviousUncapValues)
- }, [mainWeapon, weapons])
-
- // 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])
+ }, [state.grid.weapons.mainWeapon, state.grid.weapons.allWeapons])
// Methods: Fetching an object from the server
async function fetchGrid(shortcode: string) {
@@ -105,13 +78,13 @@ const WeaponGrid = (props: Props) => {
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
- setEditable(true)
- setAppEditable(true)
+ state.party.editable = true
}
// Store the important party and state-keeping values
- setId(party.id)
- setHasExtra(party.is_extra)
+ state.party.id = party.id
+ state.party.extra = party.is_extra
+
setFound(true)
setLoading(false)
@@ -131,43 +104,36 @@ const WeaponGrid = (props: Props) => {
}
function populateWeapons(list: [GridWeapon]) {
- let weapons: GridArray = {}
-
- list.forEach((object: GridWeapon) => {
- if (object.mainhand) {
- setMainWeapon(object)
- setElement(object.weapon.element)
- } else if (!object.mainhand && object.position != null) {
- weapons[object.position] = object
+ list.forEach((gridObject: GridWeapon) => {
+ if (gridObject.mainhand) {
+ state.grid.weapons.mainWeapon = gridObject
+ state.party.element = gridObject.object.element
+ } else if (!gridObject.mainhand && gridObject.position != null) {
+ state.grid.weapons.allWeapons[gridObject.position] = gridObject
}
})
-
- setWeapons(weapons)
}
-
+
// Methods: Adding an object from search
- function openSearchModal(position: number) {
- setItemPositionForSearch(position)
- openModal()
- }
-
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
const weapon = object as Weapon
- setElement(weapon.element)
+ if (position == 1)
+ state.party.element = weapon.element
- if (!id) {
- props.createParty(hasExtra)
+ if (!party.id) {
+ props.createParty(party.extra)
.then(response => {
const party = response.data.party
- setId(party.id)
+ state.party.id = party.id
setSlug(party.shortcode)
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
+
saveWeapon(party.id, weapon, position)
.then(response => storeGridWeapon(response.data.grid_weapon))
})
} else {
- saveWeapon(id, weapon, position)
+ saveWeapon(party.id, weapon, position)
.then(response => storeGridWeapon(response.data.grid_weapon))
}
}
@@ -190,12 +156,11 @@ const WeaponGrid = (props: Props) => {
function storeGridWeapon(gridWeapon: GridWeapon) {
if (gridWeapon.position == -1) {
- setMainWeapon(gridWeapon)
+ state.grid.weapons.mainWeapon = gridWeapon
+ state.party.element = gridWeapon.object.element
} else {
// Store the grid unit at the correct position
- let newWeapons = Object.assign({}, weapons)
- newWeapons[gridWeapon.position] = gridWeapon
- setWeapons(newWeapons)
+ state.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
}
}
@@ -241,32 +206,29 @@ const WeaponGrid = (props: Props) => {
)
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)
- }
+ if (state.grid.weapons.mainWeapon && position == -1)
+ state.grid.weapons.mainWeapon.uncap_level = uncapLevel
+ else
+ state.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] = (mainWeapon && position == -1) ? mainWeapon.uncap_level : weapons[position].uncap_level
+ newPreviousValues[position] = (state.grid.weapons.mainWeapon && position == -1) ?
+ state.grid.weapons.mainWeapon.uncap_level : state.grid.weapons.allWeapons[position].uncap_level
setPreviousUncapValues(newPreviousValues)
}
// Render: JSX components
const mainhandElement = (
{ openSearchModal(-1) }}
+ updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate}
/>
)
@@ -276,11 +238,11 @@ const WeaponGrid = (props: Props) => {
return (
{ openSearchModal(i) }}
+ updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate}
/>
@@ -290,10 +252,10 @@ const WeaponGrid = (props: Props) => {
const extraGridElement = (
)
@@ -305,18 +267,7 @@ const WeaponGrid = (props: Props) => {
- { (() => { return (hasExtra) ? extraGridElement : '' })() }
-
- {open ? (
-
- ) : null}
+ { (() => { return (party.extra) ? extraGridElement : '' })() }
)
}
diff --git a/components/WeaponUnit/index.tsx b/components/WeaponUnit/index.tsx
index 396ceeaa..12f83fa8 100644
--- a/components/WeaponUnit/index.tsx
+++ b/components/WeaponUnit/index.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'
import classnames from 'classnames'
+import SearchModal from '~components/SearchModal'
import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/Add.svg'
@@ -11,7 +12,7 @@ interface Props {
unitType: 0 | 1
position: number
editable: boolean
- onClick: () => void
+ updateObject: (object: Character | Weapon | Summon, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
@@ -27,7 +28,7 @@ const WeaponUnit = (props: Props) => {
})
const gridWeapon = props.gridWeapon
- const weapon = gridWeapon?.weapon
+ const weapon = gridWeapon?.object
useEffect(() => {
generateImageUrl()
@@ -36,7 +37,7 @@ const WeaponUnit = (props: Props) => {
function generateImageUrl() {
let imgSrc = ""
if (props.gridWeapon) {
- const weapon = props.gridWeapon.weapon!
+ const weapon = props.gridWeapon.object!
if (props.unitType == 0)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
@@ -55,15 +56,22 @@ const WeaponUnit = (props: Props) => {
return (
-
{} }>
-
- { (props.editable) ?
: '' }
-
+
+
+
+ { (props.editable) ?
: '' }
+
+
+
{ (gridWeapon) ?
=16.8",
+ "react-dom": "^16.8 || ^17.0"
+ }
+ },
"node_modules/@radix-ui/react-arrow": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
@@ -4293,6 +4314,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/eslint-plugin-valtio": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-valtio/-/eslint-plugin-valtio-0.4.1.tgz",
+ "integrity": "sha512-mORVREchU66YRWa0svret65i9U6gSliNThPH2GJEJlNHE/J1sYdcEcuobKAAMKlz5WpflC38nslkRxBKpiU/rA==",
+ "dev": true
+ },
"node_modules/eslint-scope": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
@@ -5762,6 +5789,11 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
+ "node_modules/proxy-compare": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
+ "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
+ },
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -6667,6 +6699,37 @@
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
+ "node_modules/valtio": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.3.0.tgz",
+ "integrity": "sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==",
+ "dependencies": {
+ "proxy-compare": "2.0.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@babel/helper-module-imports": ">=7.12",
+ "@babel/types": ">=7.13",
+ "babel-plugin-macros": ">=3.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@babel/helper-module-imports": {
+ "optional": true
+ },
+ "@babel/types": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@@ -8284,6 +8347,20 @@
"@babel/runtime": "^7.13.10"
}
},
+ "@radix-ui/react-alert-dialog": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.1.5.tgz",
+ "integrity": "sha512-Lq9h3GSvw752e7dFll3UWvm4uWiTlYAXLFX6wr/VQPRoa7XaQO8/1NBu4ikLHAecGEd/uDGZLY3aP7ovGPQYtg==",
+ "requires": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "0.1.0",
+ "@radix-ui/react-compose-refs": "0.1.0",
+ "@radix-ui/react-context": "0.1.1",
+ "@radix-ui/react-dialog": "0.1.5",
+ "@radix-ui/react-primitive": "0.1.3",
+ "@radix-ui/react-slot": "0.1.2"
+ }
+ },
"@radix-ui/react-arrow": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
@@ -9813,6 +9890,12 @@
"dev": true,
"requires": {}
},
+ "eslint-plugin-valtio": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-valtio/-/eslint-plugin-valtio-0.4.1.tgz",
+ "integrity": "sha512-mORVREchU66YRWa0svret65i9U6gSliNThPH2GJEJlNHE/J1sYdcEcuobKAAMKlz5WpflC38nslkRxBKpiU/rA==",
+ "dev": true
+ },
"eslint-scope": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
@@ -10820,6 +10903,11 @@
}
}
},
+ "proxy-compare": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
+ "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
+ },
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -11429,6 +11517,14 @@
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
+ "valtio": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.3.0.tgz",
+ "integrity": "sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==",
+ "requires": {
+ "proxy-compare": "2.0.2"
+ }
+ },
"void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
diff --git a/package.json b/package.json
index 898e39c7..f6b2f8b1 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"lint": "next lint"
},
"dependencies": {
+ "@radix-ui/react-alert-dialog": "^0.1.5",
"@radix-ui/react-dialog": "^0.1.5",
"@radix-ui/react-dropdown-menu": "^0.1.4",
"@radix-ui/react-hover-card": "^0.1.3",
@@ -27,7 +28,8 @@
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"react-i18next": "^11.15.3",
- "sass": "^1.49.0"
+ "sass": "^1.49.0",
+ "valtio": "^1.3.0"
},
"devDependencies": {
"@types/lodash.debounce": "^4.0.6",
@@ -36,6 +38,7 @@
"@types/react-dom": "^17.0.11",
"eslint": "8.7.0",
"eslint-config-next": "12.0.8",
+ "eslint-plugin-valtio": "^0.4.1",
"typescript": "4.5.5"
}
}
diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx
index a317520c..e43e2b07 100644
--- a/pages/p/[party].tsx
+++ b/pages/p/[party].tsx
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import Party from '~components/Party'
+import * as AlertDialog from '@radix-ui/react-alert-dialog'
const PartyRoute: React.FC = () => {
const router = useRouter()
diff --git a/public/icons/Cross.svg b/public/icons/Cross.svg
new file mode 100644
index 00000000..0b1bbd9f
--- /dev/null
+++ b/public/icons/Cross.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/styles/globals.scss b/styles/globals.scss
index 295a5a80..2d5e5894 100644
--- a/styles/globals.scss
+++ b/styles/globals.scss
@@ -3,25 +3,35 @@
html {
background: $background-color;
font-size: 62.5%;
- padding: $unit * 2;
}
body {
-webkit-font-smoothing: antialiased;
- font-family: system-ui, -apple-system, Helvetica Neue, Helvetica, Arial, sans-serif;
+ box-sizing: border-box;
+ font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 1.4rem;
+ height: 100vh;
+ padding: $unit * 2 !important;
&.no-scroll {
overflow: hidden;
}
}
+#__next {
+ height: 100%;
+}
+
+main {
+ min-height: 90%;
+}
+
a {
text-decoration: none;
}
button, input {
- font-family: system-ui, -apple-system, Helvetica Neue, Helvetica, Arial, sans-serif;
+ font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1, h2, h3, p {
@@ -48,3 +58,80 @@ h1 {
}
+.Overlay {
+ background: rgba(0, 0, 0, 0.6);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 20;
+}
+
+.Dialog {
+ animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running openModal;
+ background: white;
+ border-radius: $unit;
+ display: flex;
+ flex-direction: column;
+ gap: $unit * 3;
+ height: auto;
+ min-width: $unit * 48;
+ min-height: $unit * 12;
+ padding: $unit * 3;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 21;
+
+ .DialogHeader {
+ display: flex;
+ }
+
+ .DialogClose {
+ background: transparent;
+ height: 21px;
+ width: 21px;
+
+ &:hover {
+ cursor: pointer;
+
+ svg {
+ fill: $grey-00;
+ }
+ }
+
+ svg {
+ fill: $grey-10;
+ }
+ }
+
+ .DialogTitle {
+ font-size: $font-large;
+ flex-grow: 1;
+ }
+
+ .DialogDescription {
+ flex-grow: 1;
+ }
+
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+ }
+}
+
+@keyframes openModal {
+ 0% {
+ opacity: 0;
+ transform: translate(-50%, -48%) scale(0.96);
+ }
+
+ 100% {
+ // opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+
diff --git a/types/Character.d.ts b/types/Character.d.ts
index e004581e..01037ff1 100644
--- a/types/Character.d.ts
+++ b/types/Character.d.ts
@@ -1,4 +1,6 @@
interface Character {
+ type: 'character'
+
id: string
granblue_id: string
element: number
diff --git a/types/GridCharacter.d.ts b/types/GridCharacter.d.ts
index 47784b17..cb3481e8 100644
--- a/types/GridCharacter.d.ts
+++ b/types/GridCharacter.d.ts
@@ -1,6 +1,6 @@
-interface GridCharacter {
+interface GridCharacter {
id: string
position: number
- character: Character
+ object: Character
uncap_level: number
}
\ No newline at end of file
diff --git a/types/GridSummon.d.ts b/types/GridSummon.d.ts
index 0ce8c2fc..e02aafdf 100644
--- a/types/GridSummon.d.ts
+++ b/types/GridSummon.d.ts
@@ -1,8 +1,8 @@
-interface GridSummon {
+interface GridSummon {
id: string
main: boolean
friend: boolean
position: number
- summon: Summon
+ object: Summon
uncap_level: number
}
\ No newline at end of file
diff --git a/types/GridWeapon.d.ts b/types/GridWeapon.d.ts
index 3a6c2aa4..f2d7dd37 100644
--- a/types/GridWeapon.d.ts
+++ b/types/GridWeapon.d.ts
@@ -2,6 +2,6 @@ interface GridWeapon {
id: string
mainhand: boolean
position: number
- weapon: Weapon
+ object: Weapon
uncap_level: number
}
\ No newline at end of file
diff --git a/types/Summon.d.ts b/types/Summon.d.ts
index 8b03037e..fdd961ac 100644
--- a/types/Summon.d.ts
+++ b/types/Summon.d.ts
@@ -1,4 +1,6 @@
interface Summon {
+ type: 'summon'
+
id: string
granblue_id: number
element: number
diff --git a/types/Weapon.d.ts b/types/Weapon.d.ts
index 295543ef..0f3c1ead 100644
--- a/types/Weapon.d.ts
+++ b/types/Weapon.d.ts
@@ -1,4 +1,6 @@
interface Weapon {
+ type: 'weapon'
+
id: string
granblue_id: number
element: number
diff --git a/utils/enums.tsx b/utils/enums.tsx
index 86e2af6d..ca8289b1 100644
--- a/utils/enums.tsx
+++ b/utils/enums.tsx
@@ -1,3 +1,8 @@
+export enum ButtonType {
+ Base,
+ Destructive
+}
+
export enum GridType {
Class,
Character,
diff --git a/utils/state.tsx b/utils/state.tsx
new file mode 100644
index 00000000..42f66db4
--- /dev/null
+++ b/utils/state.tsx
@@ -0,0 +1,57 @@
+import { proxy } from "valtio";
+
+interface State {
+ app: {
+ authenticated: boolean
+ },
+ party: {
+ id: string | undefined,
+ editable: boolean,
+ element: number,
+ extra: boolean
+ },
+ grid: {
+ weapons: {
+ mainWeapon: GridWeapon | undefined,
+ allWeapons: GridArray
+ },
+ summons: {
+ mainSummon: GridSummon | undefined,
+ friendSummon: GridSummon | undefined,
+ allSummons: GridArray
+ },
+ characters: GridArray
+ },
+ search: {
+ sourceItem: GridCharacter | GridWeapon | GridSummon | undefined
+ }
+}
+
+const state: State = {
+ app: {
+ authenticated: false
+ },
+ party: {
+ id: undefined,
+ editable: false,
+ element: 0,
+ extra: false
+ },
+ grid: {
+ weapons: {
+ mainWeapon: undefined,
+ allWeapons: {}
+ },
+ summons: {
+ mainSummon: undefined,
+ friendSummon: undefined,
+ allSummons: {}
+ },
+ characters: {}
+ },
+ search: {
+ sourceItem: undefined
+ }
+}
+
+export default proxy(state)
\ No newline at end of file