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 {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin: 0;
|
||||
margin-bottom: $unit * 2;
|
||||
.About.Dialog {
|
||||
width: $unit * 60;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
|
||||
h2 {
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
font-size: $font-regular;
|
||||
line-height: 1.24;
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,55 @@
|
|||
import React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import api from '~utils/api'
|
||||
|
||||
import Modal from '~components/Modal'
|
||||
import Overlay from '~components/Overlay'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const AboutModal = (props: Props) => {
|
||||
const AboutModal = () => {
|
||||
return (
|
||||
createPortal(
|
||||
<div>
|
||||
<Modal
|
||||
title="About"
|
||||
styleName="AboutModal"
|
||||
close={ () => {} }
|
||||
>
|
||||
<div>
|
||||
<p>Siero is a tool to save and share parties for <a href="https://game.granbluefantasy.jp">Granblue Fantasy.</a></p>
|
||||
<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>
|
||||
<p>If you want to save your parties for safe keeping or to edit them later, you can make a free account.</p>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<li className="MenuItem">About</li>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content className="About Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
|
||||
<div className="DialogHeader">
|
||||
<Dialog.Title className="DialogTitle">About</Dialog.Title>
|
||||
<Dialog.Close className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</Modal>
|
||||
<Overlay onClick={props.close} />
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
|
||||
<section>
|
||||
<Dialog.Description className="DialogDescription">
|
||||
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;
|
||||
|
||||
.icon svg {
|
||||
fill: $grey-50;
|
||||
fill: $grey-00;
|
||||
}
|
||||
|
||||
.icon.stroke svg {
|
||||
fill: none;
|
||||
stroke: $grey-00;
|
||||
}
|
||||
}
|
||||
|
||||
&.destructive:hover {
|
||||
background: $error;
|
||||
color: white;
|
||||
|
||||
.icon svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.modal:hover {
|
||||
background: $grey-90;
|
||||
}
|
||||
|
||||
&.modal.destructive {
|
||||
color: $error;
|
||||
|
||||
&:hover {
|
||||
color: darken($error, 10)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +54,11 @@
|
|||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&.stroke svg {
|
||||
fill: none;
|
||||
stroke: $grey-50;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-blue {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import AddIcon from '~public/icons/Add.svg'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import EditIcon from '~public/icons/Edit.svg'
|
||||
import LinkIcon from '~public/icons/Link.svg'
|
||||
import MenuIcon from '~public/icons/Menu.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { ButtonType } from '~utils/enums'
|
||||
|
||||
interface Props {
|
||||
color: string
|
||||
disabled: boolean
|
||||
type: string | null
|
||||
icon: string | null
|
||||
type: ButtonType
|
||||
click: any
|
||||
}
|
||||
|
||||
|
|
@ -19,9 +26,9 @@ interface State {
|
|||
|
||||
class Button extends React.Component<Props, State> {
|
||||
static defaultProps: Props = {
|
||||
color: 'grey',
|
||||
disabled: false,
|
||||
type: null,
|
||||
icon: null,
|
||||
type: ButtonType.Base,
|
||||
click: () => {}
|
||||
}
|
||||
|
||||
|
|
@ -34,17 +41,25 @@ class Button extends React.Component<Props, State> {
|
|||
|
||||
render() {
|
||||
let icon
|
||||
if (this.props.type === 'new') {
|
||||
if (this.props.icon === 'new') {
|
||||
icon = <span className='icon'>
|
||||
<AddIcon />
|
||||
</span>
|
||||
} else if (this.props.type === 'menu') {
|
||||
} else if (this.props.icon === 'menu') {
|
||||
icon = <span className='icon'>
|
||||
<MenuIcon />
|
||||
</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'>
|
||||
<img alt="" src="/icons/Link.svg" />
|
||||
<CrossIcon />
|
||||
</span>
|
||||
} else if (this.props.icon === 'edit') {
|
||||
icon = <span className='icon'>
|
||||
<EditIcon />
|
||||
</span>
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +67,7 @@ class Button extends React.Component<Props, State> {
|
|||
Button: true,
|
||||
'btn-pressed': this.state.isPressed,
|
||||
'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}>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useModal as useModal } from '~utils/useModal'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import AppContext from '~context/AppContext'
|
||||
import PartyContext from '~context/PartyContext'
|
||||
|
||||
import CharacterUnit from '~components/CharacterUnit'
|
||||
import SearchModal from '~components/SearchModal'
|
||||
|
||||
import api from '~utils/api'
|
||||
import state from '~utils/state'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
|
|
@ -35,44 +34,28 @@ const CharacterGrid = (props: Props) => {
|
|||
} : {}
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(state)
|
||||
|
||||
const [slug, setSlug] = useState()
|
||||
const [found, setFound] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { id, setId } = useContext(PartyContext)
|
||||
const { slug, setSlug } = useContext(PartyContext)
|
||||
const { editable, setEditable } = useContext(AppContext)
|
||||
|
||||
// Set up states for Grid data
|
||||
const [characters, setCharacters] = useState<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
|
||||
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
|
||||
useEffect(() => {
|
||||
const shortcode = (props.slug) ? props.slug : slug
|
||||
if (shortcode) fetchGrid(shortcode)
|
||||
else setEditable(true)
|
||||
else state.party.editable = true
|
||||
}, [slug, props.slug])
|
||||
|
||||
// Initialize an array of current uncap values for each characters
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||
Object.values(characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
Object.values(state.grid.characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [characters])
|
||||
|
||||
// Update search grid whenever characters are updated
|
||||
useEffect(() => {
|
||||
let newSearchGrid = Object.values(characters).map((o) => o.character)
|
||||
setSearchGrid(newSearchGrid)
|
||||
}, [characters])
|
||||
}, [state.grid.characters])
|
||||
|
||||
// Methods: Fetching an object from the server
|
||||
async function fetchGrid(shortcode: string) {
|
||||
|
|
@ -90,11 +73,12 @@ const CharacterGrid = (props: Props) => {
|
|||
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
||||
|
||||
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
||||
setEditable(true)
|
||||
party.editable = true
|
||||
}
|
||||
|
||||
// Store the important party and state-keeping values
|
||||
setId(party.id)
|
||||
state.party.id = party.id
|
||||
|
||||
setFound(true)
|
||||
setLoading(false)
|
||||
|
||||
|
|
@ -114,60 +98,48 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
function populateCharacters(list: [GridCharacter]) {
|
||||
let characters: GridArray<GridCharacter> = {}
|
||||
|
||||
list.forEach((object: GridCharacter) => {
|
||||
if (object.position != null)
|
||||
characters[object.position] = object
|
||||
state.grid.characters[object.position] = object
|
||||
})
|
||||
|
||||
setCharacters(characters)
|
||||
}
|
||||
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function openSearchModal(position: number) {
|
||||
setItemPositionForSearch(position)
|
||||
openModal()
|
||||
}
|
||||
|
||||
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||
const character = object as Character
|
||||
|
||||
if (!id) {
|
||||
if (!party.id) {
|
||||
props.createParty()
|
||||
.then(response => {
|
||||
const party = response.data.party
|
||||
setId(party.id)
|
||||
state.party.id = party.id
|
||||
setSlug(party.shortcode)
|
||||
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then(response => storeGridCharacter(response.data.grid_character))
|
||||
.catch(error => console.error(error))
|
||||
})
|
||||
} else {
|
||||
saveCharacter(id, character, position)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then(response => storeGridCharacter(response.data.grid_character))
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCharacter(partyId: string, character: Character, position: number) {
|
||||
async function saveCharacter(partyId: string, character: Character, position: number) {
|
||||
return await api.endpoints.characters.create({
|
||||
'character': {
|
||||
'party_id': partyId,
|
||||
'character_id': character.id,
|
||||
'position': position,
|
||||
'mainhand': (position == -1),
|
||||
'uncap_level': characterUncapLevel(character)
|
||||
}
|
||||
}, headers)
|
||||
}
|
||||
|
||||
function storeGridCharacter(gridCharacter: GridCharacter) {
|
||||
// Store the grid unit at the correct position
|
||||
let newCharacters = Object.assign({}, characters)
|
||||
newCharacters[gridCharacter.position] = gridCharacter
|
||||
setCharacters(newCharacters)
|
||||
state.grid.characters[gridCharacter.position] = gridCharacter
|
||||
}
|
||||
|
||||
// Methods: Helpers
|
||||
|
|
@ -209,39 +181,37 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const initiateUncapUpdate = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}, [previousUncapValues, characters]
|
||||
)
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
}, [characters]
|
||||
}, [props, previousUncapValues]
|
||||
)
|
||||
|
||||
const debouncedAction = useMemo(() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500), [characters, saveUncap]
|
||||
}, 500), [props, saveUncap]
|
||||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
let newCharacters = {...characters}
|
||||
newCharacters[position].uncap_level = uncapLevel
|
||||
setCharacters(newCharacters)
|
||||
state.grid.characters[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
newPreviousValues[position] = characters[position].uncap_level
|
||||
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
if (grid.characters[position]) {
|
||||
newPreviousValues[position] = grid.characters[position].uncap_level
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
|
|
@ -252,26 +222,15 @@ const CharacterGrid = (props: Props) => {
|
|||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<CharacterUnit
|
||||
gridCharacter={characters[i]}
|
||||
editable={editable}
|
||||
gridCharacter={grid.characters[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
onClick={() => { openSearchModal(i) }}
|
||||
updateObject={receiveCharacterFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
{open ? (
|
||||
<SearchModal
|
||||
grid={searchGrid}
|
||||
close={closeModal}
|
||||
send={receiveCharacterFromSearch}
|
||||
fromPosition={itemPositionForSearch}
|
||||
object="characters"
|
||||
placeholderText="Search for a character..."
|
||||
/>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ interface Props {
|
|||
gridCharacter: GridCharacter | undefined
|
||||
position: number
|
||||
editable: boolean
|
||||
onClick: () => void
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ const CharacterUnit = (props: Props) => {
|
|||
})
|
||||
|
||||
const gridCharacter = props.gridCharacter
|
||||
const character = gridCharacter?.character
|
||||
const character = gridCharacter?.object
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
|
|
@ -34,7 +35,7 @@ const CharacterUnit = (props: Props) => {
|
|||
let imgSrc = ""
|
||||
|
||||
if (props.gridCharacter) {
|
||||
const character = props.gridCharacter.character!
|
||||
const character = props.gridCharacter.object!
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg`
|
||||
}
|
||||
|
||||
|
|
@ -49,10 +50,17 @@ const CharacterUnit = (props: Props) => {
|
|||
return (
|
||||
<div>
|
||||
<div className={classes}>
|
||||
<div className="CharacterImage" onClick={ (props.editable) ? props.onClick : () => {} }>
|
||||
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
|
||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||
</div>
|
||||
<SearchModal
|
||||
placeholderText="Search for a character..."
|
||||
fromPosition={props.position}
|
||||
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) ?
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,6 @@ import React from 'react'
|
|||
import SummonUnit from '~components/SummonUnit'
|
||||
import './index.scss'
|
||||
|
||||
// GridType
|
||||
export enum GridType {
|
||||
Class,
|
||||
Character,
|
||||
Weapon,
|
||||
Summon
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
grid: GridArray<GridSummon>
|
||||
|
|
@ -17,7 +9,7 @@ interface Props {
|
|||
exists: boolean
|
||||
found?: boolean
|
||||
offset: number
|
||||
onClick: (position: number) => void
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +29,7 @@ const ExtraSummons = (props: Props) => {
|
|||
position={props.offset + i}
|
||||
unitType={1}
|
||||
gridSummon={props.grid[props.offset + i]}
|
||||
onClick={() => { props.onClick(props.offset + i) }}
|
||||
updateObject={props.updateObject}
|
||||
updateUncap={props.updateUncap}
|
||||
/>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -3,21 +3,13 @@ import WeaponUnit from '~components/WeaponUnit'
|
|||
|
||||
import './index.scss'
|
||||
|
||||
// GridType
|
||||
export enum GridType {
|
||||
Class,
|
||||
Character,
|
||||
Weapon,
|
||||
Summon
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
grid: GridArray<GridWeapon>
|
||||
editable: boolean
|
||||
found?: boolean
|
||||
offset: number
|
||||
onClick: (position: number) => void
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +29,7 @@ const ExtraWeapons = (props: Props) => {
|
|||
position={props.offset + i}
|
||||
unitType={1}
|
||||
gridWeapon={props.grid[props.offset + i]}
|
||||
onClick={() => { props.onClick(props.offset + i)}}
|
||||
updateObject={props.updateObject}
|
||||
updateUncap={props.updateUncap}
|
||||
/>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ const GridRep = (props: Props) => {
|
|||
|
||||
for (const [key, value] of Object.entries(props.grid)) {
|
||||
if (value.position == -1)
|
||||
setMainhand(value.weapon)
|
||||
setMainhand(value.object)
|
||||
else if (!value.mainhand && value.position != null)
|
||||
newWeapons[value.position] = value.weapon
|
||||
newWeapons[value.position] = value.object
|
||||
}
|
||||
|
||||
setWeapons(newWeapons)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
#Header {
|
||||
.Header {
|
||||
display: flex;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
|
||||
&.bottom {
|
||||
position: sticky;
|
||||
bottom: $unit * 2;
|
||||
}
|
||||
|
||||
#right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
|
|
|||
|
|
@ -1,82 +1,19 @@
|
|||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import AppContext from '~context/AppContext'
|
||||
|
||||
import Button from '~components/Button'
|
||||
import HeaderMenu from '~components/HeaderMenu'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
interface Props {
|
||||
position: 'top' | 'bottom'
|
||||
left: JSX.Element,
|
||||
right: JSX.Element
|
||||
}
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext)
|
||||
|
||||
const [username, setUsername] = useState(undefined)
|
||||
const [cookies, _, removeCookie] = useCookies(['user'])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (cookies.user) {
|
||||
setAuthenticated(true)
|
||||
setUsername(cookies.user.username)
|
||||
console.log(`Logged in as user "${cookies.user.username}"`)
|
||||
} else {
|
||||
setAuthenticated(false)
|
||||
console.log('You are currently not logged in.')
|
||||
}
|
||||
}, [cookies, setUsername, setAuthenticated])
|
||||
|
||||
function copyToClipboard() {
|
||||
const el = document.createElement('input')
|
||||
el.value = window.location.href
|
||||
el.id = 'url-input'
|
||||
document.body.appendChild(el)
|
||||
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
el.remove()
|
||||
}
|
||||
|
||||
function newParty() {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
function logout() {
|
||||
removeCookie('user')
|
||||
|
||||
setAuthenticated(false)
|
||||
if (editable) setEditable(false)
|
||||
|
||||
// How can we log out without navigating to root
|
||||
router.push('/')
|
||||
return false
|
||||
}
|
||||
|
||||
const Header = (props: Props) => {
|
||||
return (
|
||||
<nav id="Header">
|
||||
<div id="left">
|
||||
<div className="dropdown">
|
||||
<Button type="menu">Menu</Button>
|
||||
{ (username) ?
|
||||
<HeaderMenu authenticated={authenticated} username={username} logout={logout} /> :
|
||||
<HeaderMenu authenticated={authenticated} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<nav className={`Header ${props.position}`}>
|
||||
<div id="left">{ props.left }</div>
|
||||
<div className="push" />
|
||||
<div id="right">
|
||||
{ (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>
|
||||
<div id="right">{ props.right }</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const HeaderMenu = (props: Props) => {
|
|||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href='/teans'>Teams</Link>
|
||||
<Link href='/teams'>Teams</Link>
|
||||
</li>
|
||||
|
||||
<li className="MenuItem">
|
||||
|
|
@ -46,10 +46,7 @@ const HeaderMenu = (props: Props) => {
|
|||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem" onClick={openAboutModal}>About</li>
|
||||
{aboutOpen ? (
|
||||
<AboutModal close={closeAboutModal} />
|
||||
) : null}
|
||||
<AboutModal />
|
||||
<li className="MenuItem">Settings</li>
|
||||
<li className="MenuItem" onClick={props.logout}>Logout</li>
|
||||
</div>
|
||||
|
|
@ -62,14 +59,11 @@ const HeaderMenu = (props: Props) => {
|
|||
return (
|
||||
<ul className="Menu unauth">
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem" onClick={openAboutModal}>About</li>
|
||||
{aboutOpen ? (
|
||||
<AboutModal close={closeAboutModal} />
|
||||
) : null}
|
||||
<AboutModal />
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<li className="MenuItem">
|
||||
<Link href='/teans'>Teams</Link>
|
||||
<Link href='/teams'>Teams</Link>
|
||||
</li>
|
||||
|
||||
<li className="MenuItem">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { ReactElement } from 'react'
|
||||
import Header from '~components/Header'
|
||||
import TopHeader from '~components/TopHeader'
|
||||
import BottomHeader from '~components/BottomHeader'
|
||||
|
||||
interface Props {
|
||||
children: ReactElement
|
||||
|
|
@ -8,8 +9,9 @@ interface Props {
|
|||
const Layout = ({children}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<TopHeader />
|
||||
<main>{children}</main>
|
||||
<BottomHeader />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@
|
|||
overflow-y: auto;
|
||||
padding: $unit * 3;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
z-index: 21;
|
||||
|
||||
#ModalTop {
|
||||
#ModalHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.Overlay {
|
||||
background: black;
|
||||
position: absolute;
|
||||
opacity: 0.28;
|
||||
// .Overlay {
|
||||
// background: black;
|
||||
// position: absolute;
|
||||
// opacity: 0.28;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
|
||||
// }
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useCookies } from 'react-cookie'
|
||||
|
||||
import PartyContext from '~context/PartyContext'
|
||||
|
||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||
import WeaponGrid from '~components/WeaponGrid'
|
||||
|
|
@ -9,18 +9,11 @@ import SummonGrid from '~components/SummonGrid'
|
|||
import CharacterGrid from '~components/CharacterGrid'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { TeamElement } from '~utils/enums'
|
||||
import state from '~utils/state'
|
||||
import { GridType, TeamElement } from '~utils/enums'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// GridType
|
||||
enum GridType {
|
||||
Class,
|
||||
Character,
|
||||
Weapon,
|
||||
Summon
|
||||
}
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
slug?: string
|
||||
|
|
@ -37,12 +30,8 @@ const Party = (props: Props) => {
|
|||
} : {}
|
||||
|
||||
// Set up states
|
||||
const { party } = useSnapshot(state)
|
||||
const [currentTab, setCurrentTab] = useState<GridType>(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
|
||||
async function createParty(extra: boolean = false) {
|
||||
|
|
@ -58,10 +47,12 @@ const Party = (props: Props) => {
|
|||
|
||||
// Methods: Updating the party's extra flag
|
||||
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setHasExtra(event.target.checked)
|
||||
api.endpoints.parties.update(id, {
|
||||
'party': { 'is_extra': event.target.checked }
|
||||
}, headers)
|
||||
if (party.id) {
|
||||
state.party.extra = event.target.checked
|
||||
api.endpoints.parties.update(party.id, {
|
||||
'party': { 'is_extra': event.target.checked }
|
||||
}, headers)
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Navigating with segmented control
|
||||
|
|
@ -130,10 +121,8 @@ const Party = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<PartyContext.Provider value={{ id, setId, slug, setSlug, element, setElement, editable, setEditable, hasExtra, setHasExtra }}>
|
||||
{ navigation }
|
||||
{ currentGrid() }
|
||||
</PartyContext.Provider>
|
||||
{ navigation }
|
||||
{ currentGrid() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
import React, { useContext } from 'react'
|
||||
import './index.scss'
|
||||
|
||||
import PartyContext from '~context/PartyContext'
|
||||
import state from '~utils/state'
|
||||
|
||||
import SegmentedControl from '~components/SegmentedControl'
|
||||
import Segment from '~components/Segment'
|
||||
import ToggleSwitch from '~components/ToggleSwitch'
|
||||
|
||||
// GridType
|
||||
export enum GridType {
|
||||
Class,
|
||||
Character,
|
||||
Weapon,
|
||||
Summon
|
||||
}
|
||||
import { GridType } from '~utils/enums'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
interface Props {
|
||||
selectedTab: GridType
|
||||
|
|
@ -22,10 +17,10 @@ interface Props {
|
|||
}
|
||||
|
||||
const PartySegmentedControl = (props: Props) => {
|
||||
const { editable, element, hasExtra } = useContext(PartyContext)
|
||||
const { party } = useSnapshot(state)
|
||||
|
||||
function getElement() {
|
||||
switch(element) {
|
||||
switch(party.element) {
|
||||
case 1: return "wind"; break
|
||||
case 2: return "fire"; break
|
||||
case 3: return "water"; break
|
||||
|
|
@ -40,8 +35,8 @@ const PartySegmentedControl = (props: Props) => {
|
|||
Extra
|
||||
<ToggleSwitch
|
||||
name="ExtraSwitch"
|
||||
editable={editable}
|
||||
checked={hasExtra}
|
||||
editable={party.editable}
|
||||
checked={party.extra}
|
||||
onChange={props.onCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -80,7 +75,7 @@ const PartySegmentedControl = (props: Props) => {
|
|||
|
||||
{
|
||||
(() => {
|
||||
if (editable && props.selectedTab == GridType.Weapon) {
|
||||
if (party.editable && props.selectedTab == GridType.Weapon) {
|
||||
return extraToggle
|
||||
}
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
.ModalContainer .Modal.SearchModal {
|
||||
.Modal.Search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 420px;
|
||||
min-width: 600px;
|
||||
padding: 0;
|
||||
|
||||
#ModalTop {
|
||||
#ModalHeader {
|
||||
background: $grey-90;
|
||||
gap: $unit;
|
||||
margin: 0;
|
||||
|
|
@ -13,12 +13,28 @@
|
|||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
|
||||
.Input {
|
||||
border: 1px solid $grey-70;
|
||||
border-radius: $unit / 2;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 8px;
|
||||
font-size: $font-regular;
|
||||
padding: $unit * 1.5;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -26,13 +42,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.SearchModal #results_container {
|
||||
.Search.Modal #results_container {
|
||||
margin: 0;
|
||||
max-height: 330px;
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
|
||||
.SearchModal #NoResults {
|
||||
.Search.Modal #NoResults {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -40,7 +56,7 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.SearchModal #NoResults h2 {
|
||||
.Search.Modal #NoResults h2 {
|
||||
color: #ccc;
|
||||
font-size: $font-large;
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import { createPortal } from 'react-dom'
|
||||
import state from '~utils/state'
|
||||
import api from '~utils/api'
|
||||
|
||||
import Modal from '~components/Modal'
|
||||
import Overlay from '~components/Overlay'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
|
||||
import CharacterResult from '~components/CharacterResult'
|
||||
import WeaponResult from '~components/WeaponResult'
|
||||
import SummonResult from '~components/SummonResult'
|
||||
|
|
@ -13,138 +14,143 @@ import './index.scss'
|
|||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
interface Props {
|
||||
close: () => void
|
||||
send: (object: Character | Weapon | Summon, position: number) => any
|
||||
grid: GridArray<Character|Weapon|Summon>
|
||||
placeholderText: string
|
||||
fromPosition: number
|
||||
object: 'weapons' | 'characters' | 'summons'
|
||||
object: 'weapons' | 'characters' | 'summons',
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
query: string,
|
||||
results: { [key: string]: any }
|
||||
loading: boolean
|
||||
message: string
|
||||
totalResults: number
|
||||
}
|
||||
const SearchModal = (props: Props) => {
|
||||
let { grid } = useSnapshot(state)
|
||||
|
||||
class SearchModal extends React.Component<Props, State> {
|
||||
searchInput: React.RefObject<HTMLInputElement>
|
||||
let searchInput = React.createRef<HTMLInputElement>()
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
query: '',
|
||||
results: {},
|
||||
loading: false,
|
||||
message: '',
|
||||
totalResults: 0
|
||||
}
|
||||
this.searchInput = React.createRef<HTMLInputElement>()
|
||||
}
|
||||
const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [totalResults, setTotalResults] = useState(0)
|
||||
|
||||
componentDidMount() {
|
||||
if (this.searchInput.current) {
|
||||
this.searchInput.current.focus()
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
setObjects(grid[props.object])
|
||||
}, [grid, props.object])
|
||||
|
||||
filterExclusions = (o: Character | Weapon | Summon) => {
|
||||
if (this.props.grid[this.props.fromPosition] &&
|
||||
o.granblue_id == this.props.grid[this.props.fromPosition].granblue_id) {
|
||||
return null
|
||||
} else return o
|
||||
}
|
||||
useEffect(() => {
|
||||
if (searchInput.current)
|
||||
searchInput.current.focus()
|
||||
}, [searchInput])
|
||||
|
||||
fetchResults = (query: string) => {
|
||||
const excludes = Object.values(this.props.grid).filter(this.filterExclusions).map((o) => { return o.name.en }).join(',')
|
||||
function inputChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const text = event.target.value
|
||||
if (text.length) {
|
||||
setQuery(text)
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
|
||||
api.search(this.props.object, query, excludes)
|
||||
.then((response) => {
|
||||
const data = response.data
|
||||
const totalResults = data.length
|
||||
this.setState({
|
||||
results: data,
|
||||
totalResults: totalResults,
|
||||
loading: false
|
||||
})
|
||||
}, (error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
message: error
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
inputChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const query = event.target.value
|
||||
if (query.length) {
|
||||
this.setState({ query, loading: true, message: '' }, () => {
|
||||
this.fetchResults(query)
|
||||
})
|
||||
if (text.length > 2)
|
||||
fetchResults()
|
||||
} else {
|
||||
this.setState({ query, results: {}, message: '' })
|
||||
setQuery('')
|
||||
setResults({})
|
||||
setMessage('')
|
||||
}
|
||||
}
|
||||
|
||||
function fetchResults() {
|
||||
// Exclude objects in grid from search results
|
||||
// unless the object is in the position that the user clicked
|
||||
// so that users can replace object versions with
|
||||
// compatible other objects.
|
||||
const excludes = (objects) ? Object.values(objects)
|
||||
.filter(filterExclusions)
|
||||
.map((o) => { return (o.object) ? o.object.name.en : undefined }).join(',') : ''
|
||||
|
||||
sendData = (result: Character | Weapon | Summon) => {
|
||||
this.props.send(result, this.props.fromPosition)
|
||||
this.props.close()
|
||||
api.search(props.object, query, excludes)
|
||||
.then(response => {
|
||||
setResults(response.data)
|
||||
setTotalResults(response.data.length)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(error => {
|
||||
setMessage(error)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
renderSearchResults = () => {
|
||||
const { results } = this.state
|
||||
|
||||
switch(this.props.object) {
|
||||
function filterExclusions(gridObject: GridCharacter | GridWeapon | GridSummon) {
|
||||
if (objects && gridObject.object &&
|
||||
gridObject.object.granblue_id === objects[props.fromPosition]?.object.granblue_id)
|
||||
return null
|
||||
else return gridObject
|
||||
}
|
||||
|
||||
function sendData(result: Character | Weapon | Summon) {
|
||||
props.send(result, props.fromPosition)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
switch(props.object) {
|
||||
case 'weapons':
|
||||
return this.renderWeaponSearchResults(results)
|
||||
|
||||
return renderWeaponSearchResults(results)
|
||||
break
|
||||
case 'summons':
|
||||
return this.renderSummonSearchResults(results)
|
||||
|
||||
return renderSummonSearchResults(results)
|
||||
break
|
||||
case 'characters':
|
||||
return this.renderCharacterSearchResults(results)
|
||||
return renderCharacterSearchResults(results)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
renderWeaponSearchResults = (results: { [key: string]: any }) => {
|
||||
return (
|
||||
<ul id="results_container">
|
||||
{ results.map( (result: Weapon) => {
|
||||
return <WeaponResult key={result.id} data={result} onClick={() => { this.sendData(result) }} />
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
function renderWeaponSearchResults(results: { [key: string]: any }) {
|
||||
const elements = results.map((result: Weapon) => {
|
||||
return <WeaponResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
|
||||
return (<ul id="results_container">{elements}</ul>)
|
||||
}
|
||||
|
||||
renderSummonSearchResults = (results: { [key: string]: any }) => {
|
||||
return (
|
||||
<ul id="results_container">
|
||||
{ results.map( (result: Summon) => {
|
||||
return <SummonResult key={result.id} data={result} onClick={() => { this.sendData(result) }} />
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
function renderSummonSearchResults(results: { [key: string]: any }) {
|
||||
const elements = results.map((result: Summon) => {
|
||||
return <SummonResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
|
||||
return (<ul id="results_container">{elements}</ul>)
|
||||
}
|
||||
|
||||
renderCharacterSearchResults = (results: { [key: string]: any }) => {
|
||||
return (
|
||||
<ul id="results_container">
|
||||
{ results.map( (result: Character) => {
|
||||
return <CharacterResult key={result.id} data={result} onClick={() => { this.sendData(result) }} />
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
function renderCharacterSearchResults(results: { [key: string]: any }) {
|
||||
const elements = results.map((result: Character) => {
|
||||
return <CharacterResult
|
||||
key={result.id}
|
||||
data={result}
|
||||
onClick={() => { sendData(result) }}
|
||||
/>
|
||||
})
|
||||
|
||||
return (<ul id="results_container">{elements}</ul>)
|
||||
}
|
||||
|
||||
renderEmptyState = () => {
|
||||
function renderEmptyState() {
|
||||
let string = ''
|
||||
|
||||
if (this.state.query === '') {
|
||||
string = `No ${this.props.object}`
|
||||
if (query === '') {
|
||||
string = `No ${props.object}`
|
||||
} else if (query.length < 3) {
|
||||
string = `Type at least 3 characters`
|
||||
} else {
|
||||
string = `No results found for '${this.state.query}'`
|
||||
string = `No results found for '${query}'`
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -153,48 +159,46 @@ class SearchModal extends React.Component<Props, State> {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { query, loading } = this.state
|
||||
|
||||
let content: JSX.Element
|
||||
if (Object.entries(this.state.results).length == 0) {
|
||||
content = this.renderEmptyState()
|
||||
} else {
|
||||
content = this.renderSearchResults()
|
||||
}
|
||||
|
||||
return (
|
||||
createPortal(
|
||||
<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
|
||||
)
|
||||
)
|
||||
|
||||
function resetAndClose() {
|
||||
setQuery('')
|
||||
setResults({})
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useModal as useModal } from '~utils/useModal'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import AppContext from '~context/AppContext'
|
||||
import PartyContext from '~context/PartyContext'
|
||||
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import SummonUnit from '~components/SummonUnit'
|
||||
import ExtraSummons from '~components/ExtraSummons'
|
||||
|
||||
import api from '~utils/api'
|
||||
import state from '~utils/state'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
|
|
@ -36,55 +34,37 @@ const SummonGrid = (props: Props) => {
|
|||
} : {}
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(state)
|
||||
|
||||
const [slug, setSlug] = useState()
|
||||
const [found, setFound] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { id, setId } = useContext(PartyContext)
|
||||
const { slug, setSlug } = useContext(PartyContext)
|
||||
const { editable, setEditable } = useContext(AppContext)
|
||||
|
||||
// Set up states for Grid data
|
||||
const [summons, setSummons] = useState<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
|
||||
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
|
||||
useEffect(() => {
|
||||
const shortcode = (props.slug) ? props.slug : slug
|
||||
if (shortcode) fetchGrid(shortcode)
|
||||
else setEditable(true)
|
||||
else state.party.editable = true
|
||||
}, [slug, props.slug])
|
||||
|
||||
// Initialize an array of current uncap values for each summon
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||
if (mainSummon) initialPreviousUncapValues[-1] = mainSummon.uncap_level
|
||||
if (friendSummon) initialPreviousUncapValues[6] = friendSummon.uncap_level
|
||||
Object.values(summons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
|
||||
if (state.grid.summons.mainSummon)
|
||||
initialPreviousUncapValues[-1] = state.grid.summons.mainSummon.uncap_level
|
||||
|
||||
if (state.grid.summons.friendSummon)
|
||||
initialPreviousUncapValues[6] = state.grid.summons.friendSummon.uncap_level
|
||||
|
||||
Object.values(state.grid.summons.allSummons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [summons, mainSummon, friendSummon])
|
||||
}, [state.grid.summons.mainSummon, state.grid.summons.friendSummon, state.grid.summons.allSummons])
|
||||
|
||||
// Update search grid whenever any summon is updated
|
||||
useEffect(() => {
|
||||
let newSearchGrid = Object.values(summons).map((o) => o.summon)
|
||||
|
||||
if (mainSummon)
|
||||
newSearchGrid.unshift(mainSummon.summon)
|
||||
|
||||
if (friendSummon)
|
||||
newSearchGrid.unshift(friendSummon.summon)
|
||||
|
||||
setSearchGrid(newSearchGrid)
|
||||
}, [summons, mainSummon, friendSummon])
|
||||
|
||||
// Methods: Fetching an object from the server
|
||||
async function fetchGrid(shortcode: string) {
|
||||
|
|
@ -102,11 +82,12 @@ const SummonGrid = (props: Props) => {
|
|||
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
||||
|
||||
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
||||
setEditable(true)
|
||||
state.party.editable = true
|
||||
}
|
||||
|
||||
// Store the important party and state-keeping values
|
||||
setId(party.id)
|
||||
state.party.id = party.id
|
||||
|
||||
setFound(true)
|
||||
setLoading(false)
|
||||
|
||||
|
|
@ -126,42 +107,34 @@ const SummonGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
function populateSummons(list: [GridSummon]) {
|
||||
let summons: GridArray<GridSummon> = {}
|
||||
|
||||
list.forEach((object: GridSummon) => {
|
||||
if (object.main)
|
||||
setMainSummon(object)
|
||||
else if (object.friend)
|
||||
setFriendSummon(object)
|
||||
else if (!object.main && !object.friend && object.position != null)
|
||||
summons[object.position] = object
|
||||
list.forEach((gridObject: GridSummon) => {
|
||||
if (gridObject.main)
|
||||
state.grid.summons.mainSummon = gridObject
|
||||
else if (gridObject.friend)
|
||||
state.grid.summons.friendSummon = gridObject
|
||||
else if (!gridObject.main && !gridObject.friend && gridObject.position != null)
|
||||
state.grid.summons.allSummons[gridObject.position] = gridObject
|
||||
})
|
||||
|
||||
setSummons(summons)
|
||||
}
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function openSearchModal(position: number) {
|
||||
setItemPositionForSearch(position)
|
||||
openModal()
|
||||
}
|
||||
|
||||
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||
const summon = object as Summon
|
||||
|
||||
if (!id) {
|
||||
if (!party.id) {
|
||||
props.createParty()
|
||||
.then(response => {
|
||||
const party = response.data.party
|
||||
setId(party.id)
|
||||
state.party.id = party.id
|
||||
setSlug(party.shortcode)
|
||||
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
|
||||
saveSummon(party.id, summon, position)
|
||||
.then(response => storeGridSummon(response.data.grid_summon))
|
||||
})
|
||||
} else {
|
||||
saveSummon(id, summon, position)
|
||||
saveSummon(party.id, summon, position)
|
||||
.then(response => storeGridSummon(response.data.grid_summon))
|
||||
}
|
||||
}
|
||||
|
|
@ -184,16 +157,12 @@ const SummonGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
function storeGridSummon(gridSummon: GridSummon) {
|
||||
if (gridSummon.position == -1) {
|
||||
setMainSummon(gridSummon)
|
||||
} else if (gridSummon.position == 6) {
|
||||
setFriendSummon(gridSummon)
|
||||
} else {
|
||||
// Store the grid unit at the correct position
|
||||
let newSummons = Object.assign({}, summons)
|
||||
newSummons[gridSummon.position] = gridSummon
|
||||
setSummons(newSummons)
|
||||
}
|
||||
if (gridSummon.position == -1)
|
||||
state.grid.summons.mainSummon = gridSummon
|
||||
else if (gridSummon.position == 6)
|
||||
state.grid.summons.friendSummon = gridSummon
|
||||
else
|
||||
state.grid.summons.allSummons[gridSummon.position] = gridSummon
|
||||
}
|
||||
|
||||
// Methods: Updating uncap level
|
||||
|
|
@ -218,40 +187,41 @@ const SummonGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const initiateUncapUpdate = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
|
||||
memoizeAction(id, position, uncapLevel)
|
||||
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}, [previousUncapValues, summons]
|
||||
)
|
||||
// Optimistically update UI
|
||||
updateUncapLevel(position, uncapLevel)
|
||||
}
|
||||
|
||||
const memoizeAction = useCallback(
|
||||
(id: string, position: number, uncapLevel: number) => {
|
||||
debouncedAction(id, position, uncapLevel)
|
||||
}, [summons, mainSummon, friendSummon]
|
||||
}, [props, previousUncapValues]
|
||||
)
|
||||
|
||||
const debouncedAction = useMemo(() =>
|
||||
debounce((id, position, number) => {
|
||||
saveUncap(id, position, number)
|
||||
}, 500), [summons, mainSummon, friendSummon, saveUncap]
|
||||
}, 500), [props, saveUncap]
|
||||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
let newSummons = Object.assign({}, summons)
|
||||
newSummons[position].uncap_level = uncapLevel
|
||||
setSummons(newSummons)
|
||||
if (state.grid.summons.mainSummon && position == -1)
|
||||
state.grid.summons.mainSummon.uncap_level = uncapLevel
|
||||
else if (state.grid.summons.friendSummon && position == 6)
|
||||
state.grid.summons.friendSummon.uncap_level = uncapLevel
|
||||
else
|
||||
state.grid.summons.allSummons[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
|
||||
if (mainSummon && position == -1) newPreviousValues[position] = mainSummon.uncap_level
|
||||
else if (friendSummon && position == 6) newPreviousValues[position] = friendSummon.uncap_level
|
||||
else newPreviousValues[position] = summons[position].uncap_level
|
||||
if (state.grid.summons.mainSummon && position == -1) newPreviousValues[position] = state.grid.summons.mainSummon.uncap_level
|
||||
else if (state.grid.summons.friendSummon && position == 6) newPreviousValues[position] = state.grid.summons.friendSummon.uncap_level
|
||||
else newPreviousValues[position] = state.grid.summons.allSummons[position].uncap_level
|
||||
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
|
|
@ -261,12 +231,12 @@ const SummonGrid = (props: Props) => {
|
|||
<div className="LabeledUnit">
|
||||
<div className="Label">Main Summon</div>
|
||||
<SummonUnit
|
||||
gridSummon={mainSummon}
|
||||
editable={editable}
|
||||
gridSummon={grid.summons.mainSummon}
|
||||
editable={party.editable}
|
||||
key="grid_main_summon"
|
||||
position={-1}
|
||||
unitType={0}
|
||||
onClick={() => { openSearchModal(-1) }}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -276,12 +246,12 @@ const SummonGrid = (props: Props) => {
|
|||
<div className="LabeledUnit">
|
||||
<div className="Label">Friend Summon</div>
|
||||
<SummonUnit
|
||||
gridSummon={friendSummon}
|
||||
editable={editable}
|
||||
gridSummon={grid.summons.friendSummon}
|
||||
editable={party.editable}
|
||||
key="grid_friend_summon"
|
||||
position={6}
|
||||
unitType={2}
|
||||
onClick={() => { openSearchModal(6) }}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -293,11 +263,11 @@ const SummonGrid = (props: Props) => {
|
|||
{Array.from(Array(numSummons)).map((x, i) => {
|
||||
return (<li key={`grid_unit_${i}`} >
|
||||
<SummonUnit
|
||||
gridSummon={summons[i]}
|
||||
editable={editable}
|
||||
gridSummon={grid.summons.allSummons[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
unitType={1}
|
||||
onClick={() => { openSearchModal(i) }}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>)
|
||||
|
|
@ -307,11 +277,11 @@ const SummonGrid = (props: Props) => {
|
|||
)
|
||||
const subAuraSummonElement = (
|
||||
<ExtraSummons
|
||||
grid={summons}
|
||||
editable={editable}
|
||||
grid={grid.summons.allSummons}
|
||||
editable={party.editable}
|
||||
exists={false}
|
||||
offset={numSummons}
|
||||
onClick={openSearchModal}
|
||||
updateObject={receiveSummonFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
|
@ -324,17 +294,6 @@ const SummonGrid = (props: Props) => {
|
|||
</div>
|
||||
|
||||
{ subAuraSummonElement }
|
||||
|
||||
{open ? (
|
||||
<SearchModal
|
||||
grid={searchGrid}
|
||||
close={closeModal}
|
||||
send={receiveSummonFromSearch}
|
||||
fromPosition={itemPositionForSearch}
|
||||
object="summons"
|
||||
placeholderText="Search for a summon..."
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
background: #e9e9e9;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
height: 72px;
|
||||
height: auto;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
onClick: () => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
gridSummon: GridSummon | undefined
|
||||
unitType: 0 | 1 | 2
|
||||
position: number
|
||||
editable: boolean
|
||||
unitType: 0 | 1 | 2
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
const SummonUnit = (props: Props) => {
|
||||
|
|
@ -28,7 +29,7 @@ const SummonUnit = (props: Props) => {
|
|||
})
|
||||
|
||||
const gridSummon = props.gridSummon
|
||||
const summon = gridSummon?.summon
|
||||
const summon = gridSummon?.object
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
|
|
@ -37,7 +38,7 @@ const SummonUnit = (props: Props) => {
|
|||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
if (props.gridSummon) {
|
||||
const summon = props.gridSummon.summon!
|
||||
const summon = props.gridSummon.object!
|
||||
|
||||
// Generate the correct source for the summon
|
||||
if (props.unitType == 0 || props.unitType == 2)
|
||||
|
|
@ -57,19 +58,27 @@ const SummonUnit = (props: Props) => {
|
|||
return (
|
||||
<div>
|
||||
<div className={classes}>
|
||||
<div className="SummonImage" onClick={ (props.editable) ? props.onClick : () => {} }>
|
||||
<img alt={summon?.name.en} className="grid_image" src={imageUrl} />
|
||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||
</div>
|
||||
<SearchModal
|
||||
placeholderText="Search for a summon..."
|
||||
fromPosition={props.position}
|
||||
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) ?
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={summon?.uncap.ulb || false}
|
||||
flb={summon?.uncap.flb || false}
|
||||
uncapLevel={gridSummon?.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
/> : '' }
|
||||
<UncapIndicator
|
||||
type="summon"
|
||||
ulb={gridSummon.object.uncap.ulb || false}
|
||||
flb={gridSummon.object.uncap.flb || false}
|
||||
uncapLevel={gridSummon.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
/> : ''
|
||||
}
|
||||
<h3 className="SummonName">{summon?.name.en}</h3>
|
||||
</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 */
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useCookies } from 'react-cookie'
|
||||
import { useModal as useModal } from '~utils/useModal'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import AppContext from '~context/AppContext'
|
||||
import PartyContext from '~context/PartyContext'
|
||||
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import WeaponUnit from '~components/WeaponUnit'
|
||||
import ExtraWeapons from '~components/ExtraWeapons'
|
||||
|
||||
import api from '~utils/api'
|
||||
import state from '~utils/state'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
// Props
|
||||
|
|
@ -36,58 +34,33 @@ const WeaponGrid = (props: Props) => {
|
|||
} : {}
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(state)
|
||||
|
||||
const [slug, setSlug] = useState()
|
||||
const [found, setFound] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Set up the party context
|
||||
const { setEditable: setAppEditable } = useContext(AppContext)
|
||||
const { id, setId } = useContext(PartyContext)
|
||||
const { slug, setSlug } = useContext(PartyContext)
|
||||
const { editable, setEditable } = useContext(PartyContext)
|
||||
const { hasExtra, setHasExtra } = useContext(PartyContext)
|
||||
const { setElement } = useContext(PartyContext)
|
||||
|
||||
// Set up states for Grid data
|
||||
const [weapons, setWeapons] = useState<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
|
||||
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
|
||||
useEffect(() => {
|
||||
const shortcode = (props.slug) ? props.slug : slug
|
||||
if (shortcode) fetchGrid(shortcode)
|
||||
else {
|
||||
setEditable(true)
|
||||
setAppEditable(true)
|
||||
}
|
||||
else state.party.editable = true
|
||||
}, [slug, props.slug])
|
||||
|
||||
// Initialize an array of current uncap values for each weapon
|
||||
useEffect(() => {
|
||||
let initialPreviousUncapValues: {[key: number]: number} = {}
|
||||
if (mainWeapon) initialPreviousUncapValues[-1] = mainWeapon.uncap_level
|
||||
Object.values(weapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
|
||||
if (state.grid.weapons.mainWeapon)
|
||||
initialPreviousUncapValues[-1] = state.grid.weapons.mainWeapon.uncap_level
|
||||
|
||||
Object.values(state.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level)
|
||||
|
||||
setPreviousUncapValues(initialPreviousUncapValues)
|
||||
}, [mainWeapon, weapons])
|
||||
|
||||
// Update search grid whenever weapons or the mainhand are updated
|
||||
useEffect(() => {
|
||||
let newSearchGrid = Object.values(weapons).map((o) => o.weapon)
|
||||
|
||||
if (mainWeapon)
|
||||
newSearchGrid.unshift(mainWeapon.weapon)
|
||||
|
||||
setSearchGrid(newSearchGrid)
|
||||
}, [weapons, mainWeapon])
|
||||
}, [state.grid.weapons.mainWeapon, state.grid.weapons.allWeapons])
|
||||
|
||||
// Methods: Fetching an object from the server
|
||||
async function fetchGrid(shortcode: string) {
|
||||
|
|
@ -105,13 +78,13 @@ const WeaponGrid = (props: Props) => {
|
|||
const loggedInUser = (cookies.user) ? cookies.user.user_id : ''
|
||||
|
||||
if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) {
|
||||
setEditable(true)
|
||||
setAppEditable(true)
|
||||
state.party.editable = true
|
||||
}
|
||||
|
||||
// Store the important party and state-keeping values
|
||||
setId(party.id)
|
||||
setHasExtra(party.is_extra)
|
||||
state.party.id = party.id
|
||||
state.party.extra = party.is_extra
|
||||
|
||||
setFound(true)
|
||||
setLoading(false)
|
||||
|
||||
|
|
@ -131,43 +104,36 @@ const WeaponGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
function populateWeapons(list: [GridWeapon]) {
|
||||
let weapons: GridArray<GridWeapon> = {}
|
||||
|
||||
list.forEach((object: GridWeapon) => {
|
||||
if (object.mainhand) {
|
||||
setMainWeapon(object)
|
||||
setElement(object.weapon.element)
|
||||
} else if (!object.mainhand && object.position != null) {
|
||||
weapons[object.position] = object
|
||||
list.forEach((gridObject: GridWeapon) => {
|
||||
if (gridObject.mainhand) {
|
||||
state.grid.weapons.mainWeapon = gridObject
|
||||
state.party.element = gridObject.object.element
|
||||
} else if (!gridObject.mainhand && gridObject.position != null) {
|
||||
state.grid.weapons.allWeapons[gridObject.position] = gridObject
|
||||
}
|
||||
})
|
||||
|
||||
setWeapons(weapons)
|
||||
}
|
||||
|
||||
|
||||
// Methods: Adding an object from search
|
||||
function openSearchModal(position: number) {
|
||||
setItemPositionForSearch(position)
|
||||
openModal()
|
||||
}
|
||||
|
||||
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
|
||||
const weapon = object as Weapon
|
||||
setElement(weapon.element)
|
||||
if (position == 1)
|
||||
state.party.element = weapon.element
|
||||
|
||||
if (!id) {
|
||||
props.createParty(hasExtra)
|
||||
if (!party.id) {
|
||||
props.createParty(party.extra)
|
||||
.then(response => {
|
||||
const party = response.data.party
|
||||
setId(party.id)
|
||||
state.party.id = party.id
|
||||
setSlug(party.shortcode)
|
||||
|
||||
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
|
||||
|
||||
saveWeapon(party.id, weapon, position)
|
||||
.then(response => storeGridWeapon(response.data.grid_weapon))
|
||||
})
|
||||
} else {
|
||||
saveWeapon(id, weapon, position)
|
||||
saveWeapon(party.id, weapon, position)
|
||||
.then(response => storeGridWeapon(response.data.grid_weapon))
|
||||
}
|
||||
}
|
||||
|
|
@ -190,12 +156,11 @@ const WeaponGrid = (props: Props) => {
|
|||
|
||||
function storeGridWeapon(gridWeapon: GridWeapon) {
|
||||
if (gridWeapon.position == -1) {
|
||||
setMainWeapon(gridWeapon)
|
||||
state.grid.weapons.mainWeapon = gridWeapon
|
||||
state.party.element = gridWeapon.object.element
|
||||
} else {
|
||||
// Store the grid unit at the correct position
|
||||
let newWeapons = Object.assign({}, weapons)
|
||||
newWeapons[gridWeapon.position] = gridWeapon
|
||||
setWeapons(newWeapons)
|
||||
state.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,32 +206,29 @@ const WeaponGrid = (props: Props) => {
|
|||
)
|
||||
|
||||
const updateUncapLevel = (position: number, uncapLevel: number) => {
|
||||
if (mainWeapon && position == -1) {
|
||||
mainWeapon.uncap_level = uncapLevel
|
||||
setMainWeapon(mainWeapon)
|
||||
} else {
|
||||
let newWeapons = Object.assign({}, weapons)
|
||||
newWeapons[position].uncap_level = uncapLevel
|
||||
setWeapons(newWeapons)
|
||||
}
|
||||
if (state.grid.weapons.mainWeapon && position == -1)
|
||||
state.grid.weapons.mainWeapon.uncap_level = uncapLevel
|
||||
else
|
||||
state.grid.weapons.allWeapons[position].uncap_level = uncapLevel
|
||||
}
|
||||
|
||||
function storePreviousUncapValue(position: number) {
|
||||
// Save the current value in case of an unexpected result
|
||||
let newPreviousValues = {...previousUncapValues}
|
||||
newPreviousValues[position] = (mainWeapon && position == -1) ? mainWeapon.uncap_level : weapons[position].uncap_level
|
||||
newPreviousValues[position] = (state.grid.weapons.mainWeapon && position == -1) ?
|
||||
state.grid.weapons.mainWeapon.uncap_level : state.grid.weapons.allWeapons[position].uncap_level
|
||||
setPreviousUncapValues(newPreviousValues)
|
||||
}
|
||||
|
||||
// Render: JSX components
|
||||
const mainhandElement = (
|
||||
<WeaponUnit
|
||||
gridWeapon={mainWeapon}
|
||||
editable={editable}
|
||||
gridWeapon={grid.weapons.mainWeapon}
|
||||
editable={party.editable}
|
||||
key="grid_mainhand"
|
||||
position={-1}
|
||||
unitType={0}
|
||||
onClick={() => { openSearchModal(-1) }}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
|
@ -276,11 +238,11 @@ const WeaponGrid = (props: Props) => {
|
|||
return (
|
||||
<li key={`grid_unit_${i}`} >
|
||||
<WeaponUnit
|
||||
gridWeapon={weapons[i]}
|
||||
editable={editable}
|
||||
gridWeapon={grid.weapons.allWeapons[i]}
|
||||
editable={party.editable}
|
||||
position={i}
|
||||
unitType={1}
|
||||
onClick={() => { openSearchModal(i) }}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
</li>
|
||||
|
|
@ -290,10 +252,10 @@ const WeaponGrid = (props: Props) => {
|
|||
|
||||
const extraGridElement = (
|
||||
<ExtraWeapons
|
||||
grid={weapons}
|
||||
editable={editable}
|
||||
grid={state.grid.weapons.allWeapons}
|
||||
editable={party.editable}
|
||||
offset={numWeapons}
|
||||
onClick={openSearchModal}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
updateUncap={initiateUncapUpdate}
|
||||
/>
|
||||
)
|
||||
|
|
@ -305,18 +267,7 @@ const WeaponGrid = (props: Props) => {
|
|||
<ul className="grid_weapons">{ weaponGridElement }</ul>
|
||||
</div>
|
||||
|
||||
{ (() => { return (hasExtra) ? extraGridElement : '' })() }
|
||||
|
||||
{open ? (
|
||||
<SearchModal
|
||||
grid={searchGrid}
|
||||
close={closeModal}
|
||||
send={receiveWeaponFromSearch}
|
||||
fromPosition={itemPositionForSearch}
|
||||
object="weapons"
|
||||
placeholderText="Search for a weapon..."
|
||||
/>
|
||||
) : null}
|
||||
{ (() => { return (party.extra) ? extraGridElement : '' })() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import SearchModal from '~components/SearchModal'
|
||||
import UncapIndicator from '~components/UncapIndicator'
|
||||
import PlusIcon from '~public/icons/Add.svg'
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ interface Props {
|
|||
unitType: 0 | 1
|
||||
position: number
|
||||
editable: boolean
|
||||
onClick: () => void
|
||||
updateObject: (object: Character | Weapon | Summon, position: number) => void
|
||||
updateUncap: (id: string, position: number, uncap: number) => void
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ const WeaponUnit = (props: Props) => {
|
|||
})
|
||||
|
||||
const gridWeapon = props.gridWeapon
|
||||
const weapon = gridWeapon?.weapon
|
||||
const weapon = gridWeapon?.object
|
||||
|
||||
useEffect(() => {
|
||||
generateImageUrl()
|
||||
|
|
@ -36,7 +37,7 @@ const WeaponUnit = (props: Props) => {
|
|||
function generateImageUrl() {
|
||||
let imgSrc = ""
|
||||
if (props.gridWeapon) {
|
||||
const weapon = props.gridWeapon.weapon!
|
||||
const weapon = props.gridWeapon.object!
|
||||
|
||||
if (props.unitType == 0)
|
||||
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
|
||||
|
|
@ -55,15 +56,22 @@ const WeaponUnit = (props: Props) => {
|
|||
return (
|
||||
<div>
|
||||
<div className={classes}>
|
||||
<div className="WeaponImage" onClick={ (props.editable) ? props.onClick : () => {} }>
|
||||
<img alt={weapon?.name.en} className="grid_image" src={imageUrl} />
|
||||
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
|
||||
</div>
|
||||
<SearchModal
|
||||
placeholderText="Search for a weapon..."
|
||||
fromPosition={props.position}
|
||||
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) ?
|
||||
<UncapIndicator
|
||||
type="weapon"
|
||||
ulb={gridWeapon.weapon.uncap.ulb || false}
|
||||
flb={gridWeapon.weapon.uncap.flb || false}
|
||||
ulb={gridWeapon.object.uncap.ulb || false}
|
||||
flb={gridWeapon.object.uncap.flb || false}
|
||||
uncapLevel={gridWeapon.uncap_level}
|
||||
updateUncap={passUncapData}
|
||||
special={false}
|
||||
|
|
|
|||
98
package-lock.json
generated
98
package-lock.json
generated
|
|
@ -6,6 +6,7 @@
|
|||
"": {
|
||||
"name": "grid-web",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
||||
"@radix-ui/react-hover-card": "^0.1.3",
|
||||
|
|
@ -22,7 +23,8 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.3",
|
||||
"sass": "^1.49.0"
|
||||
"sass": "^1.49.0",
|
||||
"valtio": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
"@types/react-dom": "^17.0.11",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-config-next": "12.0.8",
|
||||
"eslint-plugin-valtio": "^0.4.1",
|
||||
"typescript": "4.5.5"
|
||||
}
|
||||
},
|
||||
|
|
@ -2284,6 +2287,24 @@
|
|||
"@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": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
|
||||
|
|
@ -4293,6 +4314,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-valtio": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-valtio/-/eslint-plugin-valtio-0.4.1.tgz",
|
||||
"integrity": "sha512-mORVREchU66YRWa0svret65i9U6gSliNThPH2GJEJlNHE/J1sYdcEcuobKAAMKlz5WpflC38nslkRxBKpiU/rA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
|
||||
|
|
@ -5762,6 +5789,11 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
|
||||
"integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
|
|
@ -6667,6 +6699,37 @@
|
|||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.3.0.tgz",
|
||||
"integrity": "sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==",
|
||||
"dependencies": {
|
||||
"proxy-compare": "2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/helper-module-imports": ">=7.12",
|
||||
"@babel/types": ">=7.13",
|
||||
"babel-plugin-macros": ">=3.0",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/helper-module-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
|
|
@ -8284,6 +8347,20 @@
|
|||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-alert-dialog": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.1.5.tgz",
|
||||
"integrity": "sha512-Lq9h3GSvw752e7dFll3UWvm4uWiTlYAXLFX6wr/VQPRoa7XaQO8/1NBu4ikLHAecGEd/uDGZLY3aP7ovGPQYtg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "0.1.0",
|
||||
"@radix-ui/react-compose-refs": "0.1.0",
|
||||
"@radix-ui/react-context": "0.1.1",
|
||||
"@radix-ui/react-dialog": "0.1.5",
|
||||
"@radix-ui/react-primitive": "0.1.3",
|
||||
"@radix-ui/react-slot": "0.1.2"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-arrow": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz",
|
||||
|
|
@ -9813,6 +9890,12 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-valtio": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-valtio/-/eslint-plugin-valtio-0.4.1.tgz",
|
||||
"integrity": "sha512-mORVREchU66YRWa0svret65i9U6gSliNThPH2GJEJlNHE/J1sYdcEcuobKAAMKlz5WpflC38nslkRxBKpiU/rA==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
|
||||
|
|
@ -10820,6 +10903,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"proxy-compare": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz",
|
||||
"integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
|
|
@ -11429,6 +11517,14 @@
|
|||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||
"dev": true
|
||||
},
|
||||
"valtio": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.3.0.tgz",
|
||||
"integrity": "sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==",
|
||||
"requires": {
|
||||
"proxy-compare": "2.0.2"
|
||||
}
|
||||
},
|
||||
"void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.4",
|
||||
"@radix-ui/react-hover-card": "^0.1.3",
|
||||
|
|
@ -27,7 +28,8 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.3",
|
||||
"sass": "^1.49.0"
|
||||
"sass": "^1.49.0",
|
||||
"valtio": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
|
|
@ -36,6 +38,7 @@
|
|||
"@types/react-dom": "^17.0.11",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-config-next": "12.0.8",
|
||||
"eslint-plugin-valtio": "^0.4.1",
|
||||
"typescript": "4.5.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'
|
|||
import { useRouter } from 'next/router'
|
||||
|
||||
import Party from '~components/Party'
|
||||
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
|
||||
const PartyRoute: React.FC = () => {
|
||||
const router = useRouter()
|
||||
|
|
|
|||
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 {
|
||||
background: $background-color;
|
||||
font-size: 62.5%;
|
||||
padding: $unit * 2;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: system-ui, -apple-system, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 1.4rem;
|
||||
height: 100vh;
|
||||
padding: $unit * 2 !important;
|
||||
|
||||
&.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#__next {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 90%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button, input {
|
||||
font-family: system-ui, -apple-system, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, p {
|
||||
|
|
@ -48,3 +58,80 @@ h1 {
|
|||
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.Dialog {
|
||||
animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running openModal;
|
||||
background: white;
|
||||
border-radius: $unit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit * 3;
|
||||
height: auto;
|
||||
min-width: $unit * 48;
|
||||
min-height: $unit * 12;
|
||||
padding: $unit * 3;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 21;
|
||||
|
||||
.DialogHeader {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.DialogClose {
|
||||
background: transparent;
|
||||
height: 21px;
|
||||
width: 21px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
fill: $grey-00;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $grey-10;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogTitle {
|
||||
font-size: $font-large;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.DialogDescription {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes openModal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -48%) scale(0.96);
|
||||
}
|
||||
|
||||
100% {
|
||||
// opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
types/Character.d.ts
vendored
2
types/Character.d.ts
vendored
|
|
@ -1,4 +1,6 @@
|
|||
interface Character {
|
||||
type: 'character'
|
||||
|
||||
id: string
|
||||
granblue_id: string
|
||||
element: number
|
||||
|
|
|
|||
4
types/GridCharacter.d.ts
vendored
4
types/GridCharacter.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
interface GridCharacter {
|
||||
interface GridCharacter {
|
||||
id: string
|
||||
position: number
|
||||
character: Character
|
||||
object: Character
|
||||
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
|
||||
main: boolean
|
||||
friend: boolean
|
||||
position: number
|
||||
summon: Summon
|
||||
object: Summon
|
||||
uncap_level: number
|
||||
}
|
||||
2
types/GridWeapon.d.ts
vendored
2
types/GridWeapon.d.ts
vendored
|
|
@ -2,6 +2,6 @@ interface GridWeapon {
|
|||
id: string
|
||||
mainhand: boolean
|
||||
position: number
|
||||
weapon: Weapon
|
||||
object: Weapon
|
||||
uncap_level: number
|
||||
}
|
||||
2
types/Summon.d.ts
vendored
2
types/Summon.d.ts
vendored
|
|
@ -1,4 +1,6 @@
|
|||
interface Summon {
|
||||
type: 'summon'
|
||||
|
||||
id: string
|
||||
granblue_id: number
|
||||
element: number
|
||||
|
|
|
|||
2
types/Weapon.d.ts
vendored
2
types/Weapon.d.ts
vendored
|
|
@ -1,4 +1,6 @@
|
|||
interface Weapon {
|
||||
type: 'weapon'
|
||||
|
||||
id: string
|
||||
granblue_id: number
|
||||
element: number
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
export enum ButtonType {
|
||||
Base,
|
||||
Destructive
|
||||
}
|
||||
|
||||
export enum GridType {
|
||||
Class,
|
||||
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