Merge pull request #8 from jedmund/radix
Add valtio state management and Radix components
This commit is contained in:
commit
c1879d2277
40 changed files with 1006 additions and 676 deletions
|
|
@ -1,10 +1,21 @@
|
||||||
.AboutModal p {
|
.About.Dialog {
|
||||||
font-size: $font-regular;
|
width: $unit * 60;
|
||||||
line-height: 1.24;
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: $unit * 2;
|
|
||||||
|
|
||||||
&:last-of-type {
|
section {
|
||||||
margin-bottom: 0;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,55 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
import api from '~utils/api'
|
|
||||||
|
|
||||||
import Modal from '~components/Modal'
|
|
||||||
import Overlay from '~components/Overlay'
|
|
||||||
|
|
||||||
|
import CrossIcon from '~public/icons/Cross.svg'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
const AboutModal = () => {
|
||||||
close: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const AboutModal = (props: Props) => {
|
|
||||||
return (
|
return (
|
||||||
createPortal(
|
<Dialog.Root>
|
||||||
<div>
|
<Dialog.Trigger asChild>
|
||||||
<Modal
|
<li className="MenuItem">About</li>
|
||||||
title="About"
|
</Dialog.Trigger>
|
||||||
styleName="AboutModal"
|
<Dialog.Portal>
|
||||||
close={ () => {} }
|
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||||
>
|
<div className="DialogHeader">
|
||||||
<div>
|
<Dialog.Title className="DialogTitle">About</Dialog.Title>
|
||||||
<p>Siero is a tool to save and share parties for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a></p>
|
<Dialog.Close className="DialogClose" asChild>
|
||||||
<p>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.</p>
|
<span>
|
||||||
<p>If you want to save your parties for safe keeping or to edit them later, you can make a free account.</p>
|
<CrossIcon />
|
||||||
|
</span>
|
||||||
|
</Dialog.Close>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
|
||||||
<Overlay onClick={props.close} />
|
<section>
|
||||||
</div>,
|
<Dialog.Description className="DialogDescription">
|
||||||
document.body
|
Granblue.team is a tool to save and share team compositions for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a>
|
||||||
)
|
</Dialog.Description>
|
||||||
|
<Dialog.Description className="DialogDescription">
|
||||||
|
Start adding things to a team and a URL will be created for you to share it wherever you like, no account needed.
|
||||||
|
</Dialog.Description>
|
||||||
|
<Dialog.Description className="DialogDescription">
|
||||||
|
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.
|
||||||
|
</Dialog.Description>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Dialog.Title className="DialogTitle">Credits</Dialog.Title>
|
||||||
|
<Dialog.Description className="DialogDescription">
|
||||||
|
Granblue.team was built by <a href="https://twitter.com/jedmund">@jedmund</a> with a lot of help from <a href="https://twitter.com/lalalalinna">@lalalalinna</a> and <a href="https://twitter.com/tarngerine">@tarngerine</a>.
|
||||||
|
</Dialog.Description>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Dialog.Title className="DialogTitle">Open Source</Dialog.Title>
|
||||||
|
<Dialog.Description className="DialogDescription">
|
||||||
|
This app is open source. You can contribute on Github.
|
||||||
|
</Dialog.Description>
|
||||||
|
</section>
|
||||||
|
</Dialog.Content>
|
||||||
|
<Dialog.Overlay className="Overlay" />
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
86
components/BottomHeader/index.tsx
Normal file
86
components/BottomHeader/index.tsx
Normal file
|
|
@ -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<HTMLButtonElement, MouseEvent>) {
|
||||||
|
// TODO: Implement deleting teams
|
||||||
|
console.log("Deleting team...")
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftNav = () => {
|
||||||
|
return (
|
||||||
|
<Button icon="edit" click={() => {}}>Add more info</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightNav = () => {
|
||||||
|
if (editable && router.route === '/p/[party]') {
|
||||||
|
return (
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger className="Button destructive">
|
||||||
|
<span className='icon'>
|
||||||
|
<CrossIcon />
|
||||||
|
</span>
|
||||||
|
<span className="text">Delete team</span>
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
<AlertDialog.Portal>
|
||||||
|
<AlertDialog.Overlay className="Overlay" />
|
||||||
|
<AlertDialog.Content className="Dialog">
|
||||||
|
<AlertDialog.Title className="DialogTitle">
|
||||||
|
Delete team
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description className="DialogDescription">
|
||||||
|
Are you sure you want to permanently delete this team?
|
||||||
|
</AlertDialog.Description>
|
||||||
|
<div className="actions">
|
||||||
|
<AlertDialog.Cancel className="Button modal">Nevermind</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action className="Button modal destructive" onClick={(e) => deleteTeam(e)}>Yes, delete</AlertDialog.Action>
|
||||||
|
</div>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (<div />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header
|
||||||
|
position="bottom"
|
||||||
|
left={ leftNav() }
|
||||||
|
right={ rightNav() }
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BottomHeader
|
||||||
|
|
@ -16,7 +16,33 @@
|
||||||
color: $grey-00;
|
color: $grey-00;
|
||||||
|
|
||||||
.icon svg {
|
.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;
|
height: 12px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.stroke svg {
|
||||||
|
fill: none;
|
||||||
|
stroke: $grey-50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-blue {
|
&.btn-blue {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
import AddIcon from '~public/icons/Add.svg'
|
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 MenuIcon from '~public/icons/Menu.svg'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
import { ButtonType } from '~utils/enums'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
color: string
|
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
type: string | null
|
icon: string | null
|
||||||
|
type: ButtonType
|
||||||
click: any
|
click: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,9 +26,9 @@ interface State {
|
||||||
|
|
||||||
class Button extends React.Component<Props, State> {
|
class Button extends React.Component<Props, State> {
|
||||||
static defaultProps: Props = {
|
static defaultProps: Props = {
|
||||||
color: 'grey',
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
type: null,
|
icon: null,
|
||||||
|
type: ButtonType.Base,
|
||||||
click: () => {}
|
click: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,17 +41,25 @@ class Button extends React.Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let icon
|
let icon
|
||||||
if (this.props.type === 'new') {
|
if (this.props.icon === 'new') {
|
||||||
icon = <span className='icon'>
|
icon = <span className='icon'>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</span>
|
</span>
|
||||||
} else if (this.props.type === 'menu') {
|
} else if (this.props.icon === 'menu') {
|
||||||
icon = <span className='icon'>
|
icon = <span className='icon'>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</span>
|
</span>
|
||||||
} else if (this.props.type === 'link') {
|
} else if (this.props.icon === 'link') {
|
||||||
|
icon = <span className='icon stroke'>
|
||||||
|
<LinkIcon />
|
||||||
|
</span>
|
||||||
|
} else if (this.props.icon === 'cross') {
|
||||||
icon = <span className='icon'>
|
icon = <span className='icon'>
|
||||||
<img alt="" src="/icons/Link.svg" />
|
<CrossIcon />
|
||||||
|
</span>
|
||||||
|
} else if (this.props.icon === 'edit') {
|
||||||
|
icon = <span className='icon'>
|
||||||
|
<EditIcon />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +67,7 @@ class Button extends React.Component<Props, State> {
|
||||||
Button: true,
|
Button: true,
|
||||||
'btn-pressed': this.state.isPressed,
|
'btn-pressed': this.state.isPressed,
|
||||||
'btn-disabled': this.props.disabled,
|
'btn-disabled': this.props.disabled,
|
||||||
[`btn-${this.props.color}`]: true
|
'destructive': this.props.type == ButtonType.Destructive
|
||||||
})
|
})
|
||||||
|
|
||||||
return <button className={classes} disabled={this.props.disabled} onClick={this.props.click}>
|
return <button className={classes} disabled={this.props.disabled} onClick={this.props.click}>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
import { useModal as useModal } from '~utils/useModal'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
import AppContext from '~context/AppContext'
|
|
||||||
import PartyContext from '~context/PartyContext'
|
|
||||||
|
|
||||||
import CharacterUnit from '~components/CharacterUnit'
|
import CharacterUnit from '~components/CharacterUnit'
|
||||||
import SearchModal from '~components/SearchModal'
|
import SearchModal from '~components/SearchModal'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import state from '~utils/state'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -35,44 +34,28 @@ const CharacterGrid = (props: Props) => {
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
// Set up state for view management
|
// Set up state for view management
|
||||||
|
const { party, grid } = useSnapshot(state)
|
||||||
|
|
||||||
|
const [slug, setSlug] = useState()
|
||||||
const [found, setFound] = useState(false)
|
const [found, setFound] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
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<GridArray<GridCharacter>>({})
|
|
||||||
|
|
||||||
// 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
|
// Create a temporary state to store previous character uncap values
|
||||||
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
||||||
|
|
||||||
// Create a state dictionary to store pure objects for Search
|
|
||||||
const [searchGrid, setSearchGrid] = useState<GridArray<Character>>({})
|
|
||||||
|
|
||||||
// Fetch data from the server
|
// Fetch data from the server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shortcode = (props.slug) ? props.slug : slug
|
const shortcode = (props.slug) ? props.slug : slug
|
||||||
if (shortcode) fetchGrid(shortcode)
|
if (shortcode) fetchGrid(shortcode)
|
||||||
else setEditable(true)
|
else state.party.editable = true
|
||||||
}, [slug, props.slug])
|
}, [slug, props.slug])
|
||||||
|
|
||||||
// Initialize an array of current uncap values for each characters
|
// Initialize an array of current uncap values for each characters
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
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)
|
setPreviousUncapValues(initialPreviousUncapValues)
|
||||||
}, [characters])
|
}, [state.grid.characters])
|
||||||
|
|
||||||
// Update search grid whenever characters are updated
|
|
||||||
useEffect(() => {
|
|
||||||
let newSearchGrid = Object.values(characters).map((o) => o.character)
|
|
||||||
setSearchGrid(newSearchGrid)
|
|
||||||
}, [characters])
|
|
||||||
|
|
||||||
// Methods: Fetching an object from the server
|
// Methods: Fetching an object from the server
|
||||||
async function fetchGrid(shortcode: string) {
|
async function fetchGrid(shortcode: string) {
|
||||||
|
|
@ -90,11 +73,12 @@ const CharacterGrid = (props: Props) => {
|
||||||
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
||||||
|
|
||||||
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
||||||
setEditable(true)
|
party.editable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the important party and state-keeping values
|
// Store the important party and state-keeping values
|
||||||
setId(party.id)
|
state.party.id = party.id
|
||||||
|
|
||||||
setFound(true)
|
setFound(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
|
@ -114,60 +98,48 @@ const CharacterGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateCharacters(list: [GridCharacter]) {
|
function populateCharacters(list: [GridCharacter]) {
|
||||||
let characters: GridArray<GridCharacter> = {}
|
|
||||||
|
|
||||||
list.forEach((object: GridCharacter) => {
|
list.forEach((object: GridCharacter) => {
|
||||||
if (object.position != null)
|
if (object.position != null)
|
||||||
characters[object.position] = object
|
state.grid.characters[object.position] = object
|
||||||
})
|
})
|
||||||
|
|
||||||
setCharacters(characters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Methods: Adding an object from search
|
// Methods: Adding an object from search
|
||||||
function openSearchModal(position: number) {
|
|
||||||
setItemPositionForSearch(position)
|
|
||||||
openModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
|
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||||
const character = object as Character
|
const character = object as Character
|
||||||
|
|
||||||
if (!id) {
|
if (!party.id) {
|
||||||
props.createParty()
|
props.createParty()
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const party = response.data.party
|
const party = response.data.party
|
||||||
setId(party.id)
|
state.party.id = party.id
|
||||||
setSlug(party.shortcode)
|
setSlug(party.shortcode)
|
||||||
|
|
||||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||||
saveCharacter(party.id, character, position)
|
saveCharacter(party.id, character, position)
|
||||||
.then(response => storeGridCharacter(response.data.grid_character))
|
.then(response => storeGridCharacter(response.data.grid_character))
|
||||||
|
.catch(error => console.error(error))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
saveCharacter(id, character, position)
|
saveCharacter(party.id, character, position)
|
||||||
.then(response => storeGridCharacter(response.data.grid_character))
|
.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({
|
return await api.endpoints.characters.create({
|
||||||
'character': {
|
'character': {
|
||||||
'party_id': partyId,
|
'party_id': partyId,
|
||||||
'character_id': character.id,
|
'character_id': character.id,
|
||||||
'position': position,
|
'position': position,
|
||||||
'mainhand': (position == -1),
|
|
||||||
'uncap_level': characterUncapLevel(character)
|
'uncap_level': characterUncapLevel(character)
|
||||||
}
|
}
|
||||||
}, headers)
|
}, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeGridCharacter(gridCharacter: GridCharacter) {
|
function storeGridCharacter(gridCharacter: GridCharacter) {
|
||||||
// Store the grid unit at the correct position
|
state.grid.characters[gridCharacter.position] = gridCharacter
|
||||||
let newCharacters = Object.assign({}, characters)
|
|
||||||
newCharacters[gridCharacter.position] = gridCharacter
|
|
||||||
setCharacters(newCharacters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Helpers
|
// Methods: Helpers
|
||||||
|
|
@ -209,39 +181,37 @@ const CharacterGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateUncapUpdate = useCallback(
|
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||||
(id: string, position: number, uncapLevel: number) => {
|
memoizeAction(id, position, uncapLevel)
|
||||||
memoizeAction(id, position, uncapLevel)
|
|
||||||
|
|
||||||
// Optimistically update UI
|
// Optimistically update UI
|
||||||
updateUncapLevel(position, uncapLevel)
|
updateUncapLevel(position, uncapLevel)
|
||||||
}, [previousUncapValues, characters]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const memoizeAction = useCallback(
|
const memoizeAction = useCallback(
|
||||||
(id: string, position: number, uncapLevel: number) => {
|
(id: string, position: number, uncapLevel: number) => {
|
||||||
debouncedAction(id, position, uncapLevel)
|
debouncedAction(id, position, uncapLevel)
|
||||||
}, [characters]
|
}, [props, previousUncapValues]
|
||||||
)
|
)
|
||||||
|
|
||||||
const debouncedAction = useMemo(() =>
|
const debouncedAction = useMemo(() =>
|
||||||
debounce((id, position, number) => {
|
debounce((id, position, number) => {
|
||||||
saveUncap(id, position, number)
|
saveUncap(id, position, number)
|
||||||
}, 500), [characters, saveUncap]
|
}, 500), [props, saveUncap]
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||||
let newCharacters = {...characters}
|
state.grid.characters[position].uncap_level = uncapLevel
|
||||||
newCharacters[position].uncap_level = uncapLevel
|
|
||||||
setCharacters(newCharacters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function storePreviousUncapValue(position: number) {
|
function storePreviousUncapValue(position: number) {
|
||||||
// Save the current value in case of an unexpected result
|
// Save the current value in case of an unexpected result
|
||||||
let newPreviousValues = {...previousUncapValues}
|
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
|
// Render: JSX components
|
||||||
|
|
@ -252,26 +222,15 @@ const CharacterGrid = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<li key={`grid_unit_${i}`} >
|
<li key={`grid_unit_${i}`} >
|
||||||
<CharacterUnit
|
<CharacterUnit
|
||||||
gridCharacter={characters[i]}
|
gridCharacter={grid.characters[i]}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
position={i}
|
position={i}
|
||||||
onClick={() => { openSearchModal(i) }}
|
updateObject={receiveCharacterFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{open ? (
|
|
||||||
<SearchModal
|
|
||||||
grid={searchGrid}
|
|
||||||
close={closeModal}
|
|
||||||
send={receiveCharacterFromSearch}
|
|
||||||
fromPosition={itemPositionForSearch}
|
|
||||||
object="characters"
|
|
||||||
placeholderText="Search for a character..."
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
import SearchModal from '~components/SearchModal'
|
||||||
import UncapIndicator from '~components/UncapIndicator'
|
import UncapIndicator from '~components/UncapIndicator'
|
||||||
import PlusIcon from '~public/icons/Add.svg'
|
import PlusIcon from '~public/icons/Add.svg'
|
||||||
|
|
||||||
|
|
@ -10,7 +11,7 @@ interface Props {
|
||||||
gridCharacter: GridCharacter | undefined
|
gridCharacter: GridCharacter | undefined
|
||||||
position: number
|
position: number
|
||||||
editable: boolean
|
editable: boolean
|
||||||
onClick: () => void
|
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||||
updateUncap: (id: string, position: number, uncap: number) => void
|
updateUncap: (id: string, position: number, uncap: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,7 +25,7 @@ const CharacterUnit = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const gridCharacter = props.gridCharacter
|
const gridCharacter = props.gridCharacter
|
||||||
const character = gridCharacter?.character
|
const character = gridCharacter?.object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateImageUrl()
|
generateImageUrl()
|
||||||
|
|
@ -34,7 +35,7 @@ const CharacterUnit = (props: Props) => {
|
||||||
let imgSrc = ""
|
let imgSrc = ""
|
||||||
|
|
||||||
if (props.gridCharacter) {
|
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`
|
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,10 +50,17 @@ const CharacterUnit = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="CharacterImage" onClick={ (props.editable) ? props.onClick : () => {} }>
|
<SearchModal
|
||||||
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
|
placeholderText="Search for a character..."
|
||||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
fromPosition={props.position}
|
||||||
</div>
|
object="characters"
|
||||||
|
send={props.updateObject}>
|
||||||
|
<div className="CharacterImage">
|
||||||
|
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
|
||||||
|
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||||
|
</div>
|
||||||
|
</SearchModal>
|
||||||
|
|
||||||
{ (gridCharacter && character) ?
|
{ (gridCharacter && character) ?
|
||||||
<UncapIndicator
|
<UncapIndicator
|
||||||
type="character"
|
type="character"
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@ import React from 'react'
|
||||||
import SummonUnit from '~components/SummonUnit'
|
import SummonUnit from '~components/SummonUnit'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// GridType
|
|
||||||
export enum GridType {
|
|
||||||
Class,
|
|
||||||
Character,
|
|
||||||
Weapon,
|
|
||||||
Summon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
grid: GridArray<GridSummon>
|
grid: GridArray<GridSummon>
|
||||||
|
|
@ -17,7 +9,7 @@ interface Props {
|
||||||
exists: boolean
|
exists: boolean
|
||||||
found?: boolean
|
found?: boolean
|
||||||
offset: number
|
offset: number
|
||||||
onClick: (position: number) => void
|
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||||
updateUncap: (id: string, position: number, uncap: number) => void
|
updateUncap: (id: string, position: number, uncap: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +29,7 @@ const ExtraSummons = (props: Props) => {
|
||||||
position={props.offset + i}
|
position={props.offset + i}
|
||||||
unitType={1}
|
unitType={1}
|
||||||
gridSummon={props.grid[props.offset + i]}
|
gridSummon={props.grid[props.offset + i]}
|
||||||
onClick={() => { props.onClick(props.offset + i) }}
|
updateObject={props.updateObject}
|
||||||
updateUncap={props.updateUncap}
|
updateUncap={props.updateUncap}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,13 @@ import WeaponUnit from '~components/WeaponUnit'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// GridType
|
|
||||||
export enum GridType {
|
|
||||||
Class,
|
|
||||||
Character,
|
|
||||||
Weapon,
|
|
||||||
Summon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
grid: GridArray<GridWeapon>
|
grid: GridArray<GridWeapon>
|
||||||
editable: boolean
|
editable: boolean
|
||||||
found?: boolean
|
found?: boolean
|
||||||
offset: number
|
offset: number
|
||||||
onClick: (position: number) => void
|
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||||
updateUncap: (id: string, position: number, uncap: number) => void
|
updateUncap: (id: string, position: number, uncap: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +29,7 @@ const ExtraWeapons = (props: Props) => {
|
||||||
position={props.offset + i}
|
position={props.offset + i}
|
||||||
unitType={1}
|
unitType={1}
|
||||||
gridWeapon={props.grid[props.offset + i]}
|
gridWeapon={props.grid[props.offset + i]}
|
||||||
onClick={() => { props.onClick(props.offset + i)}}
|
updateObject={props.updateObject}
|
||||||
updateUncap={props.updateUncap}
|
updateUncap={props.updateUncap}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ const GridRep = (props: Props) => {
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(props.grid)) {
|
for (const [key, value] of Object.entries(props.grid)) {
|
||||||
if (value.position == -1)
|
if (value.position == -1)
|
||||||
setMainhand(value.weapon)
|
setMainhand(value.object)
|
||||||
else if (!value.mainhand && value.position != null)
|
else if (!value.mainhand && value.position != null)
|
||||||
newWeapons[value.position] = value.weapon
|
newWeapons[value.position] = value.object
|
||||||
}
|
}
|
||||||
|
|
||||||
setWeapons(newWeapons)
|
setWeapons(newWeapons)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
#Header {
|
.Header {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
position: sticky;
|
||||||
|
bottom: $unit * 2;
|
||||||
|
}
|
||||||
|
|
||||||
#right {
|
#right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,19 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React 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 './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {}
|
interface Props {
|
||||||
|
position: 'top' | 'bottom'
|
||||||
|
left: JSX.Element,
|
||||||
|
right: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
const Header = (props: Props) => {
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav id="Header">
|
<nav className={`Header ${props.position}`}>
|
||||||
<div id="left">
|
<div id="left">{ props.left }</div>
|
||||||
<div className="dropdown">
|
|
||||||
<Button type="menu">Menu</Button>
|
|
||||||
{ (username) ?
|
|
||||||
<HeaderMenu authenticated={authenticated} username={username} logout={logout} /> :
|
|
||||||
<HeaderMenu authenticated={authenticated} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="push" />
|
<div className="push" />
|
||||||
<div id="right">
|
<div id="right">{ props.right }</div>
|
||||||
{ (editable && router.route === '/p/[slug]') ?
|
|
||||||
<Button color="red" type="link" click={() => {}}>Delete</Button> : ''
|
|
||||||
}
|
|
||||||
{ (router.route === '/p/[slug]') ?
|
|
||||||
<Button type="link" click={copyToClipboard}>Copy link</Button> : ''
|
|
||||||
}
|
|
||||||
<Button type="new" click={newParty}>New</Button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const HeaderMenu = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="MenuGroup">
|
<div className="MenuGroup">
|
||||||
<li className="MenuItem">
|
<li className="MenuItem">
|
||||||
<Link href='/teans'>Teams</Link>
|
<Link href='/teams'>Teams</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li className="MenuItem">
|
<li className="MenuItem">
|
||||||
|
|
@ -46,10 +46,7 @@ const HeaderMenu = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<div className="MenuGroup">
|
<div className="MenuGroup">
|
||||||
<li className="MenuItem" onClick={openAboutModal}>About</li>
|
<AboutModal />
|
||||||
{aboutOpen ? (
|
|
||||||
<AboutModal close={closeAboutModal} />
|
|
||||||
) : null}
|
|
||||||
<li className="MenuItem">Settings</li>
|
<li className="MenuItem">Settings</li>
|
||||||
<li className="MenuItem" onClick={props.logout}>Logout</li>
|
<li className="MenuItem" onClick={props.logout}>Logout</li>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,14 +59,11 @@ const HeaderMenu = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<ul className="Menu unauth">
|
<ul className="Menu unauth">
|
||||||
<div className="MenuGroup">
|
<div className="MenuGroup">
|
||||||
<li className="MenuItem" onClick={openAboutModal}>About</li>
|
<AboutModal />
|
||||||
{aboutOpen ? (
|
|
||||||
<AboutModal close={closeAboutModal} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="MenuGroup">
|
<div className="MenuGroup">
|
||||||
<li className="MenuItem">
|
<li className="MenuItem">
|
||||||
<Link href='/teans'>Teams</Link>
|
<Link href='/teams'>Teams</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li className="MenuItem">
|
<li className="MenuItem">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import Header from '~components/Header'
|
import TopHeader from '~components/TopHeader'
|
||||||
|
import BottomHeader from '~components/BottomHeader'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactElement
|
children: ReactElement
|
||||||
|
|
@ -8,8 +9,9 @@ interface Props {
|
||||||
const Layout = ({children}: Props) => {
|
const Layout = ({children}: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<TopHeader />
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
<BottomHeader />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: $unit * 3;
|
padding: $unit * 3;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 21;
|
||||||
|
|
||||||
#ModalTop {
|
#ModalHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
.Overlay {
|
// .Overlay {
|
||||||
background: black;
|
// background: black;
|
||||||
position: absolute;
|
// position: absolute;
|
||||||
opacity: 0.28;
|
// opacity: 0.28;
|
||||||
|
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
|
|
||||||
top: 0;
|
// top: 0;
|
||||||
left: 0;
|
// left: 0;
|
||||||
z-index: 2;
|
|
||||||
}
|
// }
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
|
|
||||||
import PartyContext from '~context/PartyContext'
|
|
||||||
|
|
||||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||||
import WeaponGrid from '~components/WeaponGrid'
|
import WeaponGrid from '~components/WeaponGrid'
|
||||||
|
|
@ -9,18 +9,11 @@ import SummonGrid from '~components/SummonGrid'
|
||||||
import CharacterGrid from '~components/CharacterGrid'
|
import CharacterGrid from '~components/CharacterGrid'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import { TeamElement } from '~utils/enums'
|
import state from '~utils/state'
|
||||||
|
import { GridType, TeamElement } from '~utils/enums'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// GridType
|
|
||||||
enum GridType {
|
|
||||||
Class,
|
|
||||||
Character,
|
|
||||||
Weapon,
|
|
||||||
Summon
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
slug?: string
|
slug?: string
|
||||||
|
|
@ -37,12 +30,8 @@ const Party = (props: Props) => {
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
// Set up states
|
// Set up states
|
||||||
|
const { party } = useSnapshot(state)
|
||||||
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
||||||
const [id, setId] = useState('')
|
|
||||||
const [slug, setSlug] = useState('')
|
|
||||||
const [element, setElement] = useState<TeamElement>(TeamElement.Any)
|
|
||||||
const [editable, setEditable] = useState(false)
|
|
||||||
const [hasExtra, setHasExtra] = useState(false)
|
|
||||||
|
|
||||||
// Methods: Creating a new party
|
// Methods: Creating a new party
|
||||||
async function createParty(extra: boolean = false) {
|
async function createParty(extra: boolean = false) {
|
||||||
|
|
@ -58,10 +47,12 @@ const Party = (props: Props) => {
|
||||||
|
|
||||||
// Methods: Updating the party's extra flag
|
// Methods: Updating the party's extra flag
|
||||||
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
setHasExtra(event.target.checked)
|
if (party.id) {
|
||||||
api.endpoints.parties.update(id, {
|
state.party.extra = event.target.checked
|
||||||
'party': { 'is_extra': event.target.checked }
|
api.endpoints.parties.update(party.id, {
|
||||||
}, headers)
|
'party': { 'is_extra': event.target.checked }
|
||||||
|
}, headers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Navigating with segmented control
|
// Methods: Navigating with segmented control
|
||||||
|
|
@ -130,10 +121,8 @@ const Party = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PartyContext.Provider value={{ id, setId, slug, setSlug, element, setElement, editable, setEditable, hasExtra, setHasExtra }}>
|
{ navigation }
|
||||||
{ navigation }
|
{ currentGrid() }
|
||||||
{ currentGrid() }
|
|
||||||
</PartyContext.Provider>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
import PartyContext from '~context/PartyContext'
|
import state from '~utils/state'
|
||||||
|
|
||||||
import SegmentedControl from '~components/SegmentedControl'
|
import SegmentedControl from '~components/SegmentedControl'
|
||||||
import Segment from '~components/Segment'
|
import Segment from '~components/Segment'
|
||||||
import ToggleSwitch from '~components/ToggleSwitch'
|
import ToggleSwitch from '~components/ToggleSwitch'
|
||||||
|
|
||||||
// GridType
|
import { GridType } from '~utils/enums'
|
||||||
export enum GridType {
|
import { useSnapshot } from 'valtio'
|
||||||
Class,
|
|
||||||
Character,
|
|
||||||
Weapon,
|
|
||||||
Summon
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedTab: GridType
|
selectedTab: GridType
|
||||||
|
|
@ -22,10 +17,10 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PartySegmentedControl = (props: Props) => {
|
const PartySegmentedControl = (props: Props) => {
|
||||||
const { editable, element, hasExtra } = useContext(PartyContext)
|
const { party } = useSnapshot(state)
|
||||||
|
|
||||||
function getElement() {
|
function getElement() {
|
||||||
switch(element) {
|
switch(party.element) {
|
||||||
case 1: return "wind"; break
|
case 1: return "wind"; break
|
||||||
case 2: return "fire"; break
|
case 2: return "fire"; break
|
||||||
case 3: return "water"; break
|
case 3: return "water"; break
|
||||||
|
|
@ -40,8 +35,8 @@ const PartySegmentedControl = (props: Props) => {
|
||||||
Extra
|
Extra
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
name="ExtraSwitch"
|
name="ExtraSwitch"
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
checked={hasExtra}
|
checked={party.extra}
|
||||||
onChange={props.onCheckboxChange}
|
onChange={props.onCheckboxChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,7 +75,7 @@ const PartySegmentedControl = (props: Props) => {
|
||||||
|
|
||||||
{
|
{
|
||||||
(() => {
|
(() => {
|
||||||
if (editable && props.selectedTab == GridType.Weapon) {
|
if (party.editable && props.selectedTab == GridType.Weapon) {
|
||||||
return extraToggle
|
return extraToggle
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
.ModalContainer .Modal.SearchModal {
|
.Modal.Search {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 420px;
|
min-height: 420px;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
#ModalTop {
|
#ModalHeader {
|
||||||
background: $grey-90;
|
background: $grey-90;
|
||||||
gap: $unit;
|
gap: $unit;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -13,12 +13,28 @@
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.Input {
|
.Input {
|
||||||
|
border: 1px solid $grey-70;
|
||||||
|
border-radius: $unit / 2;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 12px 8px;
|
font-size: $font-regular;
|
||||||
|
padding: $unit * 1.5;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -26,13 +42,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchModal #results_container {
|
.Search.Modal #results_container {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-height: 330px;
|
max-height: 330px;
|
||||||
padding: 0 12px 12px 12px;
|
padding: 0 12px 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchModal #NoResults {
|
.Search.Modal #NoResults {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -40,7 +56,7 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchModal #NoResults h2 {
|
.Search.Modal #NoResults h2 {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-size: $font-large;
|
font-size: $font-large;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
|
||||||
|
|
@ -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 api from '~utils/api'
|
||||||
|
|
||||||
import Modal from '~components/Modal'
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
import Overlay from '~components/Overlay'
|
|
||||||
import CharacterResult from '~components/CharacterResult'
|
import CharacterResult from '~components/CharacterResult'
|
||||||
import WeaponResult from '~components/WeaponResult'
|
import WeaponResult from '~components/WeaponResult'
|
||||||
import SummonResult from '~components/SummonResult'
|
import SummonResult from '~components/SummonResult'
|
||||||
|
|
@ -13,138 +14,143 @@ import './index.scss'
|
||||||
import PlusIcon from '~public/icons/Add.svg'
|
import PlusIcon from '~public/icons/Add.svg'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
close: () => void
|
|
||||||
send: (object: Character | Weapon | Summon, position: number) => any
|
send: (object: Character | Weapon | Summon, position: number) => any
|
||||||
grid: GridArray<Character|Weapon|Summon>
|
|
||||||
placeholderText: string
|
placeholderText: string
|
||||||
fromPosition: number
|
fromPosition: number
|
||||||
object: 'weapons' | 'characters' | 'summons'
|
object: 'weapons' | 'characters' | 'summons',
|
||||||
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
const SearchModal = (props: Props) => {
|
||||||
query: string,
|
let { grid } = useSnapshot(state)
|
||||||
results: { [key: string]: any }
|
|
||||||
loading: boolean
|
|
||||||
message: string
|
|
||||||
totalResults: number
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchModal extends React.Component<Props, State> {
|
let searchInput = React.createRef<HTMLInputElement>()
|
||||||
searchInput: React.RefObject<HTMLInputElement>
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>()
|
||||||
super(props)
|
const [open, setOpen] = useState(false)
|
||||||
this.state = {
|
const [query, setQuery] = useState('')
|
||||||
query: '',
|
const [results, setResults] = useState({})
|
||||||
results: {},
|
const [loading, setLoading] = useState(false)
|
||||||
loading: false,
|
const [message, setMessage] = useState('')
|
||||||
message: '',
|
const [totalResults, setTotalResults] = useState(0)
|
||||||
totalResults: 0
|
|
||||||
}
|
|
||||||
this.searchInput = React.createRef<HTMLInputElement>()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
if (this.searchInput.current) {
|
setObjects(grid[props.object])
|
||||||
this.searchInput.current.focus()
|
}, [grid, props.object])
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterExclusions = (o: Character | Weapon | Summon) => {
|
useEffect(() => {
|
||||||
if (this.props.grid[this.props.fromPosition] &&
|
if (searchInput.current)
|
||||||
o.granblue_id == this.props.grid[this.props.fromPosition].granblue_id) {
|
searchInput.current.focus()
|
||||||
return null
|
}, [searchInput])
|
||||||
} else return o
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchResults = (query: string) => {
|
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const excludes = Object.values(this.props.grid).filter(this.filterExclusions).map((o) => { return o.name.en }).join(',')
|
const text = event.target.value
|
||||||
|
if (text.length) {
|
||||||
|
setQuery(text)
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
api.search(this.props.object, query, excludes)
|
if (text.length > 2)
|
||||||
.then((response) => {
|
fetchResults()
|
||||||
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<HTMLInputElement>) => {
|
|
||||||
const query = event.target.value
|
|
||||||
if (query.length) {
|
|
||||||
this.setState({ query, loading: true, message: '' }, () => {
|
|
||||||
this.fetchResults(query)
|
|
||||||
})
|
|
||||||
} else {
|
} 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) => {
|
api.search(props.object, query, excludes)
|
||||||
this.props.send(result, this.props.fromPosition)
|
.then(response => {
|
||||||
this.props.close()
|
setResults(response.data)
|
||||||
|
setTotalResults(response.data.length)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
setMessage(error)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSearchResults = () => {
|
function filterExclusions(gridObject: GridCharacter | GridWeapon | GridSummon) {
|
||||||
const { results } = this.state
|
if (objects && gridObject.object &&
|
||||||
|
gridObject.object.granblue_id === objects[props.fromPosition]?.object.granblue_id)
|
||||||
switch(this.props.object) {
|
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':
|
case 'weapons':
|
||||||
return this.renderWeaponSearchResults(results)
|
return renderWeaponSearchResults(results)
|
||||||
|
break
|
||||||
case 'summons':
|
case 'summons':
|
||||||
return this.renderSummonSearchResults(results)
|
return renderSummonSearchResults(results)
|
||||||
|
break
|
||||||
case 'characters':
|
case 'characters':
|
||||||
return this.renderCharacterSearchResults(results)
|
return renderCharacterSearchResults(results)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWeaponSearchResults = (results: { [key: string]: any }) => {
|
function renderWeaponSearchResults(results: { [key: string]: any }) {
|
||||||
return (
|
const elements = results.map((result: Weapon) => {
|
||||||
<ul id="results_container">
|
return <WeaponResult
|
||||||
{ results.map( (result: Weapon) => {
|
key={result.id}
|
||||||
return <WeaponResult key={result.id} data={result} onClick={() => { this.sendData(result) }} />
|
data={result}
|
||||||
})}
|
onClick={() => { sendData(result) }}
|
||||||
</ul>
|
/>
|
||||||
)
|
})
|
||||||
|
|
||||||
|
return (<ul id="results_container">{elements}</ul>)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSummonSearchResults = (results: { [key: string]: any }) => {
|
function renderSummonSearchResults(results: { [key: string]: any }) {
|
||||||
return (
|
const elements = results.map((result: Summon) => {
|
||||||
<ul id="results_container">
|
return <SummonResult
|
||||||
{ results.map( (result: Summon) => {
|
key={result.id}
|
||||||
return <SummonResult key={result.id} data={result} onClick={() => { this.sendData(result) }} />
|
data={result}
|
||||||
})}
|
onClick={() => { sendData(result) }}
|
||||||
</ul>
|
/>
|
||||||
)
|
})
|
||||||
|
|
||||||
|
return (<ul id="results_container">{elements}</ul>)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCharacterSearchResults = (results: { [key: string]: any }) => {
|
function renderCharacterSearchResults(results: { [key: string]: any }) {
|
||||||
return (
|
const elements = results.map((result: Character) => {
|
||||||
<ul id="results_container">
|
return <CharacterResult
|
||||||
{ results.map( (result: Character) => {
|
key={result.id}
|
||||||
return <CharacterResult key={result.id} data={result} onClick={() => { this.sendData(result) }} />
|
data={result}
|
||||||
})}
|
onClick={() => { sendData(result) }}
|
||||||
</ul>
|
/>
|
||||||
)
|
})
|
||||||
|
|
||||||
|
return (<ul id="results_container">{elements}</ul>)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmptyState = () => {
|
function renderEmptyState() {
|
||||||
let string = ''
|
let string = ''
|
||||||
|
|
||||||
if (this.state.query === '') {
|
if (query === '') {
|
||||||
string = `No ${this.props.object}`
|
string = `No ${props.object}`
|
||||||
|
} else if (query.length < 3) {
|
||||||
|
string = `Type at least 3 characters`
|
||||||
} else {
|
} else {
|
||||||
string = `No results found for '${this.state.query}'`
|
string = `No results found for '${query}'`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -153,48 +159,46 @@ class SearchModal extends React.Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
function resetAndClose() {
|
||||||
const { query, loading } = this.state
|
setQuery('')
|
||||||
|
setResults({})
|
||||||
let content: JSX.Element
|
setOpen(true)
|
||||||
if (Object.entries(this.state.results).length == 0) {
|
|
||||||
content = this.renderEmptyState()
|
|
||||||
} else {
|
|
||||||
content = this.renderSearchResults()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
createPortal(
|
|
||||||
<div>
|
|
||||||
<div className="ModalContainer">
|
|
||||||
<div className="Modal SearchModal" key="search_modal">
|
|
||||||
<div id="ModalTop">
|
|
||||||
<label className="search_label" htmlFor="search_input">
|
|
||||||
<input
|
|
||||||
autoComplete="off"
|
|
||||||
type="text"
|
|
||||||
name="query"
|
|
||||||
className="Input"
|
|
||||||
id="search_input"
|
|
||||||
ref={this.searchInput}
|
|
||||||
value={query}
|
|
||||||
placeholder={this.props.placeholderText}
|
|
||||||
onChange={this.inputChanged}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<PlusIcon onClick={this.props.close} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Overlay onClick={this.props.close} />
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
{props.children}
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<div className="ModalContainer">
|
||||||
|
<Dialog.Content className="Search Modal">
|
||||||
|
<div id="ModalHeader">
|
||||||
|
<label className="search_label" htmlFor="search_input">
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
type="text"
|
||||||
|
name="query"
|
||||||
|
className="Input"
|
||||||
|
id="search_input"
|
||||||
|
ref={searchInput}
|
||||||
|
value={query}
|
||||||
|
placeholder={props.placeholderText}
|
||||||
|
onChange={inputChanged}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Dialog.Close onClick={resetAndClose}>
|
||||||
|
<PlusIcon />
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
{ ((Object.entries(results).length == 0) ? renderEmptyState() : renderResults()) }
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
<Dialog.Overlay className="Overlay" />
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchModal
|
export default SearchModal
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
import { useModal as useModal } from '~utils/useModal'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import debounce from 'lodash.debounce'
|
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 SummonUnit from '~components/SummonUnit'
|
||||||
import ExtraSummons from '~components/ExtraSummons'
|
import ExtraSummons from '~components/ExtraSummons'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import state from '~utils/state'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -36,55 +34,37 @@ const SummonGrid = (props: Props) => {
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
// Set up state for view management
|
// Set up state for view management
|
||||||
|
const { party, grid } = useSnapshot(state)
|
||||||
|
|
||||||
|
const [slug, setSlug] = useState()
|
||||||
const [found, setFound] = useState(false)
|
const [found, setFound] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
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<GridArray<GridSummon>>({})
|
|
||||||
const [mainSummon, setMainSummon] = useState<GridSummon>()
|
|
||||||
const [friendSummon, setFriendSummon] = useState<GridSummon>()
|
|
||||||
|
|
||||||
// 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
|
// Create a temporary state to store previous weapon uncap value
|
||||||
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
||||||
|
|
||||||
// Create a state dictionary to store pure objects for Search
|
|
||||||
const [searchGrid, setSearchGrid] = useState<GridArray<Summon>>({})
|
|
||||||
|
|
||||||
// Fetch data from the server
|
// Fetch data from the server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shortcode = (props.slug) ? props.slug : slug
|
const shortcode = (props.slug) ? props.slug : slug
|
||||||
if (shortcode) fetchGrid(shortcode)
|
if (shortcode) fetchGrid(shortcode)
|
||||||
else setEditable(true)
|
else state.party.editable = true
|
||||||
}, [slug, props.slug])
|
}, [slug, props.slug])
|
||||||
|
|
||||||
// Initialize an array of current uncap values for each summon
|
// Initialize an array of current uncap values for each summon
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||||
if (mainSummon) initialPreviousUncapValues[-1] = mainSummon.uncap_level
|
|
||||||
if (friendSummon) initialPreviousUncapValues[6] = friendSummon.uncap_level
|
if (state.grid.summons.mainSummon)
|
||||||
Object.values(summons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
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)
|
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
|
// Methods: Fetching an object from the server
|
||||||
async function fetchGrid(shortcode: string) {
|
async function fetchGrid(shortcode: string) {
|
||||||
|
|
@ -102,11 +82,12 @@ const SummonGrid = (props: Props) => {
|
||||||
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
||||||
|
|
||||||
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
||||||
setEditable(true)
|
state.party.editable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the important party and state-keeping values
|
// Store the important party and state-keeping values
|
||||||
setId(party.id)
|
state.party.id = party.id
|
||||||
|
|
||||||
setFound(true)
|
setFound(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
|
@ -126,42 +107,34 @@ const SummonGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateSummons(list: [GridSummon]) {
|
function populateSummons(list: [GridSummon]) {
|
||||||
let summons: GridArray<GridSummon> = {}
|
list.forEach((gridObject: GridSummon) => {
|
||||||
|
if (gridObject.main)
|
||||||
list.forEach((object: GridSummon) => {
|
state.grid.summons.mainSummon = gridObject
|
||||||
if (object.main)
|
else if (gridObject.friend)
|
||||||
setMainSummon(object)
|
state.grid.summons.friendSummon = gridObject
|
||||||
else if (object.friend)
|
else if (!gridObject.main && !gridObject.friend && gridObject.position != null)
|
||||||
setFriendSummon(object)
|
state.grid.summons.allSummons[gridObject.position] = gridObject
|
||||||
else if (!object.main && !object.friend && object.position != null)
|
|
||||||
summons[object.position] = object
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setSummons(summons)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Adding an object from search
|
// Methods: Adding an object from search
|
||||||
function openSearchModal(position: number) {
|
|
||||||
setItemPositionForSearch(position)
|
|
||||||
openModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
|
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||||
const summon = object as Summon
|
const summon = object as Summon
|
||||||
|
|
||||||
if (!id) {
|
if (!party.id) {
|
||||||
props.createParty()
|
props.createParty()
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const party = response.data.party
|
const party = response.data.party
|
||||||
setId(party.id)
|
state.party.id = party.id
|
||||||
setSlug(party.shortcode)
|
setSlug(party.shortcode)
|
||||||
|
|
||||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||||
|
|
||||||
saveSummon(party.id, summon, position)
|
saveSummon(party.id, summon, position)
|
||||||
.then(response => storeGridSummon(response.data.grid_summon))
|
.then(response => storeGridSummon(response.data.grid_summon))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
saveSummon(id, summon, position)
|
saveSummon(party.id, summon, position)
|
||||||
.then(response => storeGridSummon(response.data.grid_summon))
|
.then(response => storeGridSummon(response.data.grid_summon))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,16 +157,12 @@ const SummonGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeGridSummon(gridSummon: GridSummon) {
|
function storeGridSummon(gridSummon: GridSummon) {
|
||||||
if (gridSummon.position == -1) {
|
if (gridSummon.position == -1)
|
||||||
setMainSummon(gridSummon)
|
state.grid.summons.mainSummon = gridSummon
|
||||||
} else if (gridSummon.position == 6) {
|
else if (gridSummon.position == 6)
|
||||||
setFriendSummon(gridSummon)
|
state.grid.summons.friendSummon = gridSummon
|
||||||
} else {
|
else
|
||||||
// Store the grid unit at the correct position
|
state.grid.summons.allSummons[gridSummon.position] = gridSummon
|
||||||
let newSummons = Object.assign({}, summons)
|
|
||||||
newSummons[gridSummon.position] = gridSummon
|
|
||||||
setSummons(newSummons)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Updating uncap level
|
// Methods: Updating uncap level
|
||||||
|
|
@ -218,40 +187,41 @@ const SummonGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateUncapUpdate = useCallback(
|
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||||
(id: string, position: number, uncapLevel: number) => {
|
memoizeAction(id, position, uncapLevel)
|
||||||
memoizeAction(id, position, uncapLevel)
|
|
||||||
|
|
||||||
// Optimistically update UI
|
// Optimistically update UI
|
||||||
updateUncapLevel(position, uncapLevel)
|
updateUncapLevel(position, uncapLevel)
|
||||||
}, [previousUncapValues, summons]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
const memoizeAction = useCallback(
|
const memoizeAction = useCallback(
|
||||||
(id: string, position: number, uncapLevel: number) => {
|
(id: string, position: number, uncapLevel: number) => {
|
||||||
debouncedAction(id, position, uncapLevel)
|
debouncedAction(id, position, uncapLevel)
|
||||||
}, [summons, mainSummon, friendSummon]
|
}, [props, previousUncapValues]
|
||||||
)
|
)
|
||||||
|
|
||||||
const debouncedAction = useMemo(() =>
|
const debouncedAction = useMemo(() =>
|
||||||
debounce((id, position, number) => {
|
debounce((id, position, number) => {
|
||||||
saveUncap(id, position, number)
|
saveUncap(id, position, number)
|
||||||
}, 500), [summons, mainSummon, friendSummon, saveUncap]
|
}, 500), [props, saveUncap]
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||||
let newSummons = Object.assign({}, summons)
|
if (state.grid.summons.mainSummon && position == -1)
|
||||||
newSummons[position].uncap_level = uncapLevel
|
state.grid.summons.mainSummon.uncap_level = uncapLevel
|
||||||
setSummons(newSummons)
|
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) {
|
function storePreviousUncapValue(position: number) {
|
||||||
// Save the current value in case of an unexpected result
|
// Save the current value in case of an unexpected result
|
||||||
let newPreviousValues = {...previousUncapValues}
|
let newPreviousValues = {...previousUncapValues}
|
||||||
|
|
||||||
if (mainSummon && position == -1) newPreviousValues[position] = mainSummon.uncap_level
|
if (state.grid.summons.mainSummon && position == -1) newPreviousValues[position] = state.grid.summons.mainSummon.uncap_level
|
||||||
else if (friendSummon && position == 6) newPreviousValues[position] = friendSummon.uncap_level
|
else if (state.grid.summons.friendSummon && position == 6) newPreviousValues[position] = state.grid.summons.friendSummon.uncap_level
|
||||||
else newPreviousValues[position] = summons[position].uncap_level
|
else newPreviousValues[position] = state.grid.summons.allSummons[position].uncap_level
|
||||||
|
|
||||||
setPreviousUncapValues(newPreviousValues)
|
setPreviousUncapValues(newPreviousValues)
|
||||||
}
|
}
|
||||||
|
|
@ -261,12 +231,12 @@ const SummonGrid = (props: Props) => {
|
||||||
<div className="LabeledUnit">
|
<div className="LabeledUnit">
|
||||||
<div className="Label">Main Summon</div>
|
<div className="Label">Main Summon</div>
|
||||||
<SummonUnit
|
<SummonUnit
|
||||||
gridSummon={mainSummon}
|
gridSummon={grid.summons.mainSummon}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
key="grid_main_summon"
|
key="grid_main_summon"
|
||||||
position={-1}
|
position={-1}
|
||||||
unitType={0}
|
unitType={0}
|
||||||
onClick={() => { openSearchModal(-1) }}
|
updateObject={receiveSummonFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -276,12 +246,12 @@ const SummonGrid = (props: Props) => {
|
||||||
<div className="LabeledUnit">
|
<div className="LabeledUnit">
|
||||||
<div className="Label">Friend Summon</div>
|
<div className="Label">Friend Summon</div>
|
||||||
<SummonUnit
|
<SummonUnit
|
||||||
gridSummon={friendSummon}
|
gridSummon={grid.summons.friendSummon}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
key="grid_friend_summon"
|
key="grid_friend_summon"
|
||||||
position={6}
|
position={6}
|
||||||
unitType={2}
|
unitType={2}
|
||||||
onClick={() => { openSearchModal(6) }}
|
updateObject={receiveSummonFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -293,11 +263,11 @@ const SummonGrid = (props: Props) => {
|
||||||
{Array.from(Array(numSummons)).map((x, i) => {
|
{Array.from(Array(numSummons)).map((x, i) => {
|
||||||
return (<li key={`grid_unit_${i}`} >
|
return (<li key={`grid_unit_${i}`} >
|
||||||
<SummonUnit
|
<SummonUnit
|
||||||
gridSummon={summons[i]}
|
gridSummon={grid.summons.allSummons[i]}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
position={i}
|
position={i}
|
||||||
unitType={1}
|
unitType={1}
|
||||||
onClick={() => { openSearchModal(i) }}
|
updateObject={receiveSummonFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
</li>)
|
</li>)
|
||||||
|
|
@ -307,11 +277,11 @@ const SummonGrid = (props: Props) => {
|
||||||
)
|
)
|
||||||
const subAuraSummonElement = (
|
const subAuraSummonElement = (
|
||||||
<ExtraSummons
|
<ExtraSummons
|
||||||
grid={summons}
|
grid={grid.summons.allSummons}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
exists={false}
|
exists={false}
|
||||||
offset={numSummons}
|
offset={numSummons}
|
||||||
onClick={openSearchModal}
|
updateObject={receiveSummonFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -324,17 +294,6 @@ const SummonGrid = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ subAuraSummonElement }
|
{ subAuraSummonElement }
|
||||||
|
|
||||||
{open ? (
|
|
||||||
<SearchModal
|
|
||||||
grid={searchGrid}
|
|
||||||
close={closeModal}
|
|
||||||
send={receiveSummonFromSearch}
|
|
||||||
fromPosition={itemPositionForSearch}
|
|
||||||
object="summons"
|
|
||||||
placeholderText="Search for a summon..."
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
background: #e9e9e9;
|
background: #e9e9e9;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 72px;
|
height: auto;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
import SearchModal from '~components/SearchModal'
|
||||||
import UncapIndicator from '~components/UncapIndicator'
|
import UncapIndicator from '~components/UncapIndicator'
|
||||||
import PlusIcon from '~public/icons/Add.svg'
|
import PlusIcon from '~public/icons/Add.svg'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick: () => void
|
|
||||||
updateUncap: (id: string, position: number, uncap: number) => void
|
|
||||||
gridSummon: GridSummon | undefined
|
gridSummon: GridSummon | undefined
|
||||||
|
unitType: 0 | 1 | 2
|
||||||
position: number
|
position: number
|
||||||
editable: boolean
|
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) => {
|
const SummonUnit = (props: Props) => {
|
||||||
|
|
@ -28,7 +29,7 @@ const SummonUnit = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const gridSummon = props.gridSummon
|
const gridSummon = props.gridSummon
|
||||||
const summon = gridSummon?.summon
|
const summon = gridSummon?.object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateImageUrl()
|
generateImageUrl()
|
||||||
|
|
@ -37,7 +38,7 @@ const SummonUnit = (props: Props) => {
|
||||||
function generateImageUrl() {
|
function generateImageUrl() {
|
||||||
let imgSrc = ""
|
let imgSrc = ""
|
||||||
if (props.gridSummon) {
|
if (props.gridSummon) {
|
||||||
const summon = props.gridSummon.summon!
|
const summon = props.gridSummon.object!
|
||||||
|
|
||||||
// Generate the correct source for the summon
|
// Generate the correct source for the summon
|
||||||
if (props.unitType == 0 || props.unitType == 2)
|
if (props.unitType == 0 || props.unitType == 2)
|
||||||
|
|
@ -57,19 +58,27 @@ const SummonUnit = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="SummonImage" onClick={ (props.editable) ? props.onClick : () => {} }>
|
<SearchModal
|
||||||
<img alt={summon?.name.en} className="grid_image" src={imageUrl} />
|
placeholderText="Search for a summon..."
|
||||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
fromPosition={props.position}
|
||||||
</div>
|
object="summons"
|
||||||
|
send={props.updateObject}>
|
||||||
|
<div className="SummonImage">
|
||||||
|
<img alt={summon?.name.en} className="grid_image" src={imageUrl} />
|
||||||
|
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||||
|
</div>
|
||||||
|
</SearchModal>
|
||||||
|
|
||||||
{ (gridSummon) ?
|
{ (gridSummon) ?
|
||||||
<UncapIndicator
|
<UncapIndicator
|
||||||
type="summon"
|
type="summon"
|
||||||
ulb={summon?.uncap.ulb || false}
|
ulb={gridSummon.object.uncap.ulb || false}
|
||||||
flb={summon?.uncap.flb || false}
|
flb={gridSummon.object.uncap.flb || false}
|
||||||
uncapLevel={gridSummon?.uncap_level}
|
uncapLevel={gridSummon.uncap_level}
|
||||||
updateUncap={passUncapData}
|
updateUncap={passUncapData}
|
||||||
special={false}
|
special={false}
|
||||||
/> : '' }
|
/> : ''
|
||||||
|
}
|
||||||
<h3 className="SummonName">{summon?.name.en}</h3>
|
<h3 className="SummonName">{summon?.name.en}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
0
components/TopHeader/index.scss
Normal file
0
components/TopHeader/index.scss
Normal file
89
components/TopHeader/index.tsx
Normal file
89
components/TopHeader/index.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="dropdown">
|
||||||
|
<Button icon="menu">Menu</Button>
|
||||||
|
{ (username) ?
|
||||||
|
<HeaderMenu authenticated={authenticated} username={username} logout={logout} /> :
|
||||||
|
<HeaderMenu authenticated={authenticated} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightNav = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ (router.route === '/p/[party]') ?
|
||||||
|
<Button icon="link" click={copyToClipboard}>Copy link</Button> : ''
|
||||||
|
}
|
||||||
|
<Button icon="new" click={newParty}>New</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header
|
||||||
|
position="top"
|
||||||
|
left={ leftNav() }
|
||||||
|
right={ rightNav() }
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopHeader
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
import { useCookies } from 'react-cookie'
|
import { useCookies } from 'react-cookie'
|
||||||
import { useModal as useModal } from '~utils/useModal'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import debounce from 'lodash.debounce'
|
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 WeaponUnit from '~components/WeaponUnit'
|
||||||
import ExtraWeapons from '~components/ExtraWeapons'
|
import ExtraWeapons from '~components/ExtraWeapons'
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
|
import state from '~utils/state'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
|
|
@ -36,58 +34,33 @@ const WeaponGrid = (props: Props) => {
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
// Set up state for view management
|
// Set up state for view management
|
||||||
|
const { party, grid } = useSnapshot(state)
|
||||||
|
|
||||||
|
const [slug, setSlug] = useState()
|
||||||
const [found, setFound] = useState(false)
|
const [found, setFound] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
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<GridArray<GridWeapon>>({})
|
|
||||||
const [mainWeapon, setMainWeapon] = useState<GridWeapon>()
|
|
||||||
|
|
||||||
// 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
|
// Create a temporary state to store previous weapon uncap values
|
||||||
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
|
||||||
|
|
||||||
// Create a state dictionary to store pure objects for Search
|
|
||||||
const [searchGrid, setSearchGrid] = useState<GridArray<Weapon>>({})
|
|
||||||
|
|
||||||
// Fetch data from the server
|
// Fetch data from the server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shortcode = (props.slug) ? props.slug : slug
|
const shortcode = (props.slug) ? props.slug : slug
|
||||||
if (shortcode) fetchGrid(shortcode)
|
if (shortcode) fetchGrid(shortcode)
|
||||||
else {
|
else state.party.editable = true
|
||||||
setEditable(true)
|
|
||||||
setAppEditable(true)
|
|
||||||
}
|
|
||||||
}, [slug, props.slug])
|
}, [slug, props.slug])
|
||||||
|
|
||||||
// Initialize an array of current uncap values for each weapon
|
// Initialize an array of current uncap values for each weapon
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
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)
|
setPreviousUncapValues(initialPreviousUncapValues)
|
||||||
}, [mainWeapon, weapons])
|
}, [state.grid.weapons.mainWeapon, state.grid.weapons.allWeapons])
|
||||||
|
|
||||||
// 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: Fetching an object from the server
|
// Methods: Fetching an object from the server
|
||||||
async function fetchGrid(shortcode: string) {
|
async function fetchGrid(shortcode: string) {
|
||||||
|
|
@ -105,13 +78,13 @@ const WeaponGrid = (props: Props) => {
|
||||||
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
||||||
|
|
||||||
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
||||||
setEditable(true)
|
state.party.editable = true
|
||||||
setAppEditable(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the important party and state-keeping values
|
// Store the important party and state-keeping values
|
||||||
setId(party.id)
|
state.party.id = party.id
|
||||||
setHasExtra(party.is_extra)
|
state.party.extra = party.is_extra
|
||||||
|
|
||||||
setFound(true)
|
setFound(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
|
@ -131,43 +104,36 @@ const WeaponGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateWeapons(list: [GridWeapon]) {
|
function populateWeapons(list: [GridWeapon]) {
|
||||||
let weapons: GridArray<GridWeapon> = {}
|
list.forEach((gridObject: GridWeapon) => {
|
||||||
|
if (gridObject.mainhand) {
|
||||||
list.forEach((object: GridWeapon) => {
|
state.grid.weapons.mainWeapon = gridObject
|
||||||
if (object.mainhand) {
|
state.party.element = gridObject.object.element
|
||||||
setMainWeapon(object)
|
} else if (!gridObject.mainhand && gridObject.position != null) {
|
||||||
setElement(object.weapon.element)
|
state.grid.weapons.allWeapons[gridObject.position] = gridObject
|
||||||
} else if (!object.mainhand && object.position != null) {
|
|
||||||
weapons[object.position] = object
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setWeapons(weapons)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Adding an object from search
|
// Methods: Adding an object from search
|
||||||
function openSearchModal(position: number) {
|
|
||||||
setItemPositionForSearch(position)
|
|
||||||
openModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
|
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||||
const weapon = object as Weapon
|
const weapon = object as Weapon
|
||||||
setElement(weapon.element)
|
if (position == 1)
|
||||||
|
state.party.element = weapon.element
|
||||||
|
|
||||||
if (!id) {
|
if (!party.id) {
|
||||||
props.createParty(hasExtra)
|
props.createParty(party.extra)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const party = response.data.party
|
const party = response.data.party
|
||||||
setId(party.id)
|
state.party.id = party.id
|
||||||
setSlug(party.shortcode)
|
setSlug(party.shortcode)
|
||||||
|
|
||||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||||
|
|
||||||
saveWeapon(party.id, weapon, position)
|
saveWeapon(party.id, weapon, position)
|
||||||
.then(response => storeGridWeapon(response.data.grid_weapon))
|
.then(response => storeGridWeapon(response.data.grid_weapon))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
saveWeapon(id, weapon, position)
|
saveWeapon(party.id, weapon, position)
|
||||||
.then(response => storeGridWeapon(response.data.grid_weapon))
|
.then(response => storeGridWeapon(response.data.grid_weapon))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,12 +156,11 @@ const WeaponGrid = (props: Props) => {
|
||||||
|
|
||||||
function storeGridWeapon(gridWeapon: GridWeapon) {
|
function storeGridWeapon(gridWeapon: GridWeapon) {
|
||||||
if (gridWeapon.position == -1) {
|
if (gridWeapon.position == -1) {
|
||||||
setMainWeapon(gridWeapon)
|
state.grid.weapons.mainWeapon = gridWeapon
|
||||||
|
state.party.element = gridWeapon.object.element
|
||||||
} else {
|
} else {
|
||||||
// Store the grid unit at the correct position
|
// Store the grid unit at the correct position
|
||||||
let newWeapons = Object.assign({}, weapons)
|
state.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||||
newWeapons[gridWeapon.position] = gridWeapon
|
|
||||||
setWeapons(newWeapons)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,32 +206,29 @@ const WeaponGrid = (props: Props) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||||
if (mainWeapon && position == -1) {
|
if (state.grid.weapons.mainWeapon && position == -1)
|
||||||
mainWeapon.uncap_level = uncapLevel
|
state.grid.weapons.mainWeapon.uncap_level = uncapLevel
|
||||||
setMainWeapon(mainWeapon)
|
else
|
||||||
} else {
|
state.grid.weapons.allWeapons[position].uncap_level = uncapLevel
|
||||||
let newWeapons = Object.assign({}, weapons)
|
|
||||||
newWeapons[position].uncap_level = uncapLevel
|
|
||||||
setWeapons(newWeapons)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function storePreviousUncapValue(position: number) {
|
function storePreviousUncapValue(position: number) {
|
||||||
// Save the current value in case of an unexpected result
|
// Save the current value in case of an unexpected result
|
||||||
let newPreviousValues = {...previousUncapValues}
|
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)
|
setPreviousUncapValues(newPreviousValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render: JSX components
|
// Render: JSX components
|
||||||
const mainhandElement = (
|
const mainhandElement = (
|
||||||
<WeaponUnit
|
<WeaponUnit
|
||||||
gridWeapon={mainWeapon}
|
gridWeapon={grid.weapons.mainWeapon}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
key="grid_mainhand"
|
key="grid_mainhand"
|
||||||
position={-1}
|
position={-1}
|
||||||
unitType={0}
|
unitType={0}
|
||||||
onClick={() => { openSearchModal(-1) }}
|
updateObject={receiveWeaponFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -276,11 +238,11 @@ const WeaponGrid = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<li key={`grid_unit_${i}`} >
|
<li key={`grid_unit_${i}`} >
|
||||||
<WeaponUnit
|
<WeaponUnit
|
||||||
gridWeapon={weapons[i]}
|
gridWeapon={grid.weapons.allWeapons[i]}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
position={i}
|
position={i}
|
||||||
unitType={1}
|
unitType={1}
|
||||||
onClick={() => { openSearchModal(i) }}
|
updateObject={receiveWeaponFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -290,10 +252,10 @@ const WeaponGrid = (props: Props) => {
|
||||||
|
|
||||||
const extraGridElement = (
|
const extraGridElement = (
|
||||||
<ExtraWeapons
|
<ExtraWeapons
|
||||||
grid={weapons}
|
grid={state.grid.weapons.allWeapons}
|
||||||
editable={editable}
|
editable={party.editable}
|
||||||
offset={numWeapons}
|
offset={numWeapons}
|
||||||
onClick={openSearchModal}
|
updateObject={receiveWeaponFromSearch}
|
||||||
updateUncap={initiateUncapUpdate}
|
updateUncap={initiateUncapUpdate}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -305,18 +267,7 @@ const WeaponGrid = (props: Props) => {
|
||||||
<ul className="grid_weapons">{ weaponGridElement }</ul>
|
<ul className="grid_weapons">{ weaponGridElement }</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ (() => { return (hasExtra) ? extraGridElement : '' })() }
|
{ (() => { return (party.extra) ? extraGridElement : '' })() }
|
||||||
|
|
||||||
{open ? (
|
|
||||||
<SearchModal
|
|
||||||
grid={searchGrid}
|
|
||||||
close={closeModal}
|
|
||||||
send={receiveWeaponFromSearch}
|
|
||||||
fromPosition={itemPositionForSearch}
|
|
||||||
object="weapons"
|
|
||||||
placeholderText="Search for a weapon..."
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
import SearchModal from '~components/SearchModal'
|
||||||
import UncapIndicator from '~components/UncapIndicator'
|
import UncapIndicator from '~components/UncapIndicator'
|
||||||
import PlusIcon from '~public/icons/Add.svg'
|
import PlusIcon from '~public/icons/Add.svg'
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ interface Props {
|
||||||
unitType: 0 | 1
|
unitType: 0 | 1
|
||||||
position: number
|
position: number
|
||||||
editable: boolean
|
editable: boolean
|
||||||
onClick: () => void
|
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||||
updateUncap: (id: string, position: number, uncap: number) => void
|
updateUncap: (id: string, position: number, uncap: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ const WeaponUnit = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const gridWeapon = props.gridWeapon
|
const gridWeapon = props.gridWeapon
|
||||||
const weapon = gridWeapon?.weapon
|
const weapon = gridWeapon?.object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateImageUrl()
|
generateImageUrl()
|
||||||
|
|
@ -36,7 +37,7 @@ const WeaponUnit = (props: Props) => {
|
||||||
function generateImageUrl() {
|
function generateImageUrl() {
|
||||||
let imgSrc = ""
|
let imgSrc = ""
|
||||||
if (props.gridWeapon) {
|
if (props.gridWeapon) {
|
||||||
const weapon = props.gridWeapon.weapon!
|
const weapon = props.gridWeapon.object!
|
||||||
|
|
||||||
if (props.unitType == 0)
|
if (props.unitType == 0)
|
||||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
|
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
|
||||||
|
|
@ -55,15 +56,22 @@ const WeaponUnit = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<div className="WeaponImage" onClick={ (props.editable) ? props.onClick : () => {} }>
|
<SearchModal
|
||||||
<img alt={weapon?.name.en} className="grid_image" src={imageUrl} />
|
placeholderText="Search for a weapon..."
|
||||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
fromPosition={props.position}
|
||||||
</div>
|
object="weapons"
|
||||||
|
send={props.updateObject}>
|
||||||
|
<div className="WeaponImage">
|
||||||
|
<img alt={weapon?.name.en} className="grid_image" src={imageUrl} />
|
||||||
|
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||||
|
</div>
|
||||||
|
</SearchModal>
|
||||||
|
|
||||||
{ (gridWeapon) ?
|
{ (gridWeapon) ?
|
||||||
<UncapIndicator
|
<UncapIndicator
|
||||||
type="weapon"
|
type="weapon"
|
||||||
ulb={gridWeapon.weapon.uncap.ulb || false}
|
ulb={gridWeapon.object.uncap.ulb || false}
|
||||||
flb={gridWeapon.weapon.uncap.flb || false}
|
flb={gridWeapon.object.uncap.flb || false}
|
||||||
uncapLevel={gridWeapon.uncap_level}
|
uncapLevel={gridWeapon.uncap_level}
|
||||||
updateUncap={passUncapData}
|
updateUncap={passUncapData}
|
||||||
special={false}
|
special={false}
|
||||||
|
|
|
||||||
98
package-lock.json
generated
98
package-lock.json
generated
|
|
@ -6,6 +6,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "grid-web",
|
"name": "grid-web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||||
"@radix-ui/react-dialog": "^0.1.5",
|
"@radix-ui/react-dialog": "^0.1.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
||||||
"@radix-ui/react-hover-card": "^0.1.3",
|
"@radix-ui/react-hover-card": "^0.1.3",
|
||||||
|
|
@ -22,7 +23,8 @@
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^11.15.3",
|
"react-i18next": "^11.15.3",
|
||||||
"sass": "^1.49.0"
|
"sass": "^1.49.0",
|
||||||
|
"valtio": "^1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
|
|
@ -31,6 +33,7 @@
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"eslint": "8.7.0",
|
"eslint": "8.7.0",
|
||||||
"eslint-config-next": "12.0.8",
|
"eslint-config-next": "12.0.8",
|
||||||
|
"eslint-plugin-valtio": "^0.4.1",
|
||||||
"typescript": "4.5.5"
|
"typescript": "4.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2284,6 +2287,24 @@
|
||||||
"@babel/runtime": "^7.13.10"
|
"@babel/runtime": "^7.13.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@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==",
|
||||||
|
"dependencies": {
|
||||||
|
"@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"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": "^16.8 || ^17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
|
"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"
|
"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": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
|
||||||
|
|
@ -5762,6 +5789,11 @@
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
|
|
@ -6667,6 +6699,37 @@
|
||||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/void-elements": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
|
@ -8284,6 +8347,20 @@
|
||||||
"@babel/runtime": "^7.13.10"
|
"@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": {
|
"@radix-ui/react-arrow": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
|
||||||
|
|
@ -9813,6 +9890,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"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": {
|
"eslint-scope": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
|
"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": {
|
"punycode": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
|
|
@ -11429,6 +11517,14 @@
|
||||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||||
"dev": true
|
"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": {
|
"void-elements": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||||
"@radix-ui/react-dialog": "^0.1.5",
|
"@radix-ui/react-dialog": "^0.1.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
||||||
"@radix-ui/react-hover-card": "^0.1.3",
|
"@radix-ui/react-hover-card": "^0.1.3",
|
||||||
|
|
@ -27,7 +28,8 @@
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^11.15.3",
|
"react-i18next": "^11.15.3",
|
||||||
"sass": "^1.49.0"
|
"sass": "^1.49.0",
|
||||||
|
"valtio": "^1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
|
|
@ -36,6 +38,7 @@
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"eslint": "8.7.0",
|
"eslint": "8.7.0",
|
||||||
"eslint-config-next": "12.0.8",
|
"eslint-config-next": "12.0.8",
|
||||||
|
"eslint-plugin-valtio": "^0.4.1",
|
||||||
"typescript": "4.5.5"
|
"typescript": "4.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import Party from '~components/Party'
|
import Party from '~components/Party'
|
||||||
|
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||||
|
|
||||||
const PartyRoute: React.FC = () => {
|
const PartyRoute: React.FC = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
|
||||||
3
public/icons/Cross.svg
Normal file
3
public/icons/Cross.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.9425 11.8186C11.1844 12.0605 11.5766 12.0605 11.8186 11.8186C12.0605 11.5766 12.0605 11.1844 11.8186 10.9425L7.8761 7L11.8186 3.05755C12.0605 2.81562 12.0605 2.42338 11.8186 2.18145C11.5766 1.93952 11.1844 1.93952 10.9425 2.18145L7 6.1239L3.05755 2.18145C2.81562 1.93952 2.42338 1.93952 2.18145 2.18145C1.93952 2.42338 1.93952 2.81562 2.18145 3.05755L6.1239 7L2.18145 10.9425C1.93952 11.1844 1.93952 11.5766 2.18145 11.8186C2.42338 12.0605 2.81562 12.0605 3.05755 11.8186L7 7.8761L10.9425 11.8186Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 583 B |
|
|
@ -3,25 +3,35 @@
|
||||||
html {
|
html {
|
||||||
background: $background-color;
|
background: $background-color;
|
||||||
font-size: 62.5%;
|
font-size: 62.5%;
|
||||||
padding: $unit * 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
-webkit-font-smoothing: antialiased;
|
-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;
|
font-size: 1.4rem;
|
||||||
|
height: 100vh;
|
||||||
|
padding: $unit * 2 !important;
|
||||||
|
|
||||||
&.no-scroll {
|
&.no-scroll {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#__next {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input {
|
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 {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
types/Character.d.ts
vendored
2
types/Character.d.ts
vendored
|
|
@ -1,4 +1,6 @@
|
||||||
interface Character {
|
interface Character {
|
||||||
|
type: 'character'
|
||||||
|
|
||||||
id: string
|
id: string
|
||||||
granblue_id: string
|
granblue_id: string
|
||||||
element: number
|
element: number
|
||||||
|
|
|
||||||
4
types/GridCharacter.d.ts
vendored
4
types/GridCharacter.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
interface GridCharacter {
|
interface GridCharacter {
|
||||||
id: string
|
id: string
|
||||||
position: number
|
position: number
|
||||||
character: Character
|
object: Character
|
||||||
uncap_level: number
|
uncap_level: number
|
||||||
}
|
}
|
||||||
4
types/GridSummon.d.ts
vendored
4
types/GridSummon.d.ts
vendored
|
|
@ -1,8 +1,8 @@
|
||||||
interface GridSummon {
|
interface GridSummon {
|
||||||
id: string
|
id: string
|
||||||
main: boolean
|
main: boolean
|
||||||
friend: boolean
|
friend: boolean
|
||||||
position: number
|
position: number
|
||||||
summon: Summon
|
object: Summon
|
||||||
uncap_level: number
|
uncap_level: number
|
||||||
}
|
}
|
||||||
2
types/GridWeapon.d.ts
vendored
2
types/GridWeapon.d.ts
vendored
|
|
@ -2,6 +2,6 @@ interface GridWeapon {
|
||||||
id: string
|
id: string
|
||||||
mainhand: boolean
|
mainhand: boolean
|
||||||
position: number
|
position: number
|
||||||
weapon: Weapon
|
object: Weapon
|
||||||
uncap_level: number
|
uncap_level: number
|
||||||
}
|
}
|
||||||
2
types/Summon.d.ts
vendored
2
types/Summon.d.ts
vendored
|
|
@ -1,4 +1,6 @@
|
||||||
interface Summon {
|
interface Summon {
|
||||||
|
type: 'summon'
|
||||||
|
|
||||||
id: string
|
id: string
|
||||||
granblue_id: number
|
granblue_id: number
|
||||||
element: number
|
element: number
|
||||||
|
|
|
||||||
2
types/Weapon.d.ts
vendored
2
types/Weapon.d.ts
vendored
|
|
@ -1,4 +1,6 @@
|
||||||
interface Weapon {
|
interface Weapon {
|
||||||
|
type: 'weapon'
|
||||||
|
|
||||||
id: string
|
id: string
|
||||||
granblue_id: number
|
granblue_id: number
|
||||||
element: number
|
element: number
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
export enum ButtonType {
|
||||||
|
Base,
|
||||||
|
Destructive
|
||||||
|
}
|
||||||
|
|
||||||
export enum GridType {
|
export enum GridType {
|
||||||
Class,
|
Class,
|
||||||
Character,
|
Character,
|
||||||
|
|
|
||||||
57
utils/state.tsx
Normal file
57
utils/state.tsx
Normal file
|
|
@ -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<GridWeapon>
|
||||||
|
},
|
||||||
|
summons: {
|
||||||
|
mainSummon: GridSummon | undefined,
|
||||||
|
friendSummon: GridSummon | undefined,
|
||||||
|
allSummons: GridArray<GridSummon>
|
||||||
|
},
|
||||||
|
characters: GridArray<GridCharacter>
|
||||||
|
},
|
||||||
|
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)
|
||||||
Loading…
Reference in a new issue