diff --git a/components/AboutModal/index.scss b/components/AboutModal/index.scss index 2a96b969..32f12aad 100644 --- a/components/AboutModal/index.scss +++ b/components/AboutModal/index.scss @@ -1,10 +1,21 @@ -.AboutModal p { - font-size: $font-regular; - line-height: 1.24; - margin: 0; - margin-bottom: $unit * 2; +.About.Dialog { + width: $unit * 60; - &:last-of-type { - margin-bottom: 0; + section { + margin-bottom: $unit; + + h2 { + margin-bottom: $unit * 3; + } + } + + .DialogDescription { + font-size: $font-regular; + line-height: 1.24; + margin-bottom: $unit; + + &:last-of-type { + margin-bottom: 0; + } } } diff --git a/components/AboutModal/index.tsx b/components/AboutModal/index.tsx index 6c01e75e..afcdbdba 100644 --- a/components/AboutModal/index.tsx +++ b/components/AboutModal/index.tsx @@ -1,35 +1,55 @@ import React from 'react' -import { createPortal } from 'react-dom' -import api from '~utils/api' - -import Modal from '~components/Modal' -import Overlay from '~components/Overlay' +import * as Dialog from '@radix-ui/react-dialog' +import CrossIcon from '~public/icons/Cross.svg' import './index.scss' -interface Props { - close: () => void -} - -const AboutModal = (props: Props) => { +const AboutModal = () => { return ( - createPortal( -
- {} } - > -
-

Siero is a tool to save and share parties for Granblue Fantasy.

-

All you need to do to get started is start adding things. Siero will make a URL and you can share your party with the world.

-

If you want to save your parties for safe keeping or to edit them later, you can make a free account.

+ + +
  • About
  • +
    + + event.preventDefault() }> +
    + About + + + + +
    - - -
    , - document.body - ) + +
    + + Granblue.team is a tool to save and share team compositions for Granblue Fantasy. + + + Start adding things to a team and a URL will be created for you to share it wherever you like, no account needed. + + + You can make an account to save any teams you find for future reference, or to keep all of your teams together in one place. + +
    + +
    + Credits + + Granblue.team was built by @jedmund with a lot of help from @lalalalinna and @tarngerine. + +
    + +
    + Open Source + + This app is open source. You can contribute on Github. + +
    + + + + ) } diff --git a/components/BottomHeader/index.tsx b/components/BottomHeader/index.tsx new file mode 100644 index 00000000..013b1f2a --- /dev/null +++ b/components/BottomHeader/index.tsx @@ -0,0 +1,86 @@ +import React, { MouseEventHandler, useContext, useEffect, useState } from 'react' +import { useCookies } from 'react-cookie' +import { useRouter } from 'next/router' + +import AppContext from '~context/AppContext' + +import * as AlertDialog from '@radix-ui/react-alert-dialog'; + +import Header from '~components/Header' +import Button from '~components/Button' + +import { ButtonType } from '~utils/enums' +import CrossIcon from '~public/icons/Cross.svg' + + +const BottomHeader = () => { + const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext) + + const [username, setUsername] = useState(undefined) + const [cookies, _, removeCookie] = useCookies(['user']) + + const router = useRouter() + + useEffect(() => { + if (cookies.user) { + setAuthenticated(true) + setUsername(cookies.user.username) + } else { + setAuthenticated(false) + } + }, [cookies, setUsername, setAuthenticated]) + + function deleteTeam(event: React.MouseEvent) { + // TODO: Implement deleting teams + console.log("Deleting team...") + } + + const leftNav = () => { + return ( + + ) + } + + const rightNav = () => { + if (editable && router.route === '/p/[party]') { + return ( + + + + + + Delete team + + + + + + Delete team + + + Are you sure you want to permanently delete this team? + +
    + Nevermind + deleteTeam(e)}>Yes, delete +
    +
    +
    +
    + ) + } else { + return (
    ) + } + } + + + return ( +
    + ) +} + +export default BottomHeader \ No newline at end of file diff --git a/components/Button/index.scss b/components/Button/index.scss index 818625be..666d1800 100644 --- a/components/Button/index.scss +++ b/components/Button/index.scss @@ -16,7 +16,33 @@ color: $grey-00; .icon svg { - fill: $grey-50; + fill: $grey-00; + } + + .icon.stroke svg { + fill: none; + stroke: $grey-00; + } + } + + &.destructive:hover { + background: $error; + color: white; + + .icon svg { + fill: white; + } + } + + &.modal:hover { + background: $grey-90; + } + + &.modal.destructive { + color: $error; + + &:hover { + color: darken($error, 10) } } @@ -28,6 +54,11 @@ height: 12px; width: 12px; } + + &.stroke svg { + fill: none; + stroke: $grey-50; + } } &.btn-blue { diff --git a/components/Button/index.tsx b/components/Button/index.tsx index b24b1550..d355730f 100644 --- a/components/Button/index.tsx +++ b/components/Button/index.tsx @@ -1,15 +1,22 @@ import React from 'react' import classNames from 'classnames' +import Link from 'next/link' + import AddIcon from '~public/icons/Add.svg' +import CrossIcon from '~public/icons/Cross.svg' +import EditIcon from '~public/icons/Edit.svg' +import LinkIcon from '~public/icons/Link.svg' import MenuIcon from '~public/icons/Menu.svg' import './index.scss' +import { ButtonType } from '~utils/enums' + interface Props { - color: string disabled: boolean - type: string | null + icon: string | null + type: ButtonType click: any } @@ -19,9 +26,9 @@ interface State { class Button extends React.Component { static defaultProps: Props = { - color: 'grey', disabled: false, - type: null, + icon: null, + type: ButtonType.Base, click: () => {} } @@ -34,17 +41,25 @@ class Button extends React.Component { render() { let icon - if (this.props.type === 'new') { + if (this.props.icon === 'new') { icon = - } else if (this.props.type === 'menu') { + } else if (this.props.icon === 'menu') { icon = - } else if (this.props.type === 'link') { + } else if (this.props.icon === 'link') { + icon = + + + } else if (this.props.icon === 'cross') { icon = - + + + } else if (this.props.icon === 'edit') { + icon = + } @@ -52,7 +67,7 @@ class Button extends React.Component { Button: true, 'btn-pressed': this.state.isPressed, 'btn-disabled': this.props.disabled, - [`btn-${this.props.color}`]: true + 'destructive': this.props.type == ButtonType.Destructive }) return
    ) diff --git a/components/CharacterUnit/index.tsx b/components/CharacterUnit/index.tsx index 0d07b81c..945c1e30 100644 --- a/components/CharacterUnit/index.tsx +++ b/components/CharacterUnit/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react' import classnames from 'classnames' +import SearchModal from '~components/SearchModal' import UncapIndicator from '~components/UncapIndicator' import PlusIcon from '~public/icons/Add.svg' @@ -10,7 +11,7 @@ interface Props { gridCharacter: GridCharacter | undefined position: number editable: boolean - onClick: () => void + updateObject: (object: Character | Weapon | Summon, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -24,7 +25,7 @@ const CharacterUnit = (props: Props) => { }) const gridCharacter = props.gridCharacter - const character = gridCharacter?.character + const character = gridCharacter?.object useEffect(() => { generateImageUrl() @@ -34,7 +35,7 @@ const CharacterUnit = (props: Props) => { let imgSrc = "" if (props.gridCharacter) { - const character = props.gridCharacter.character! + const character = props.gridCharacter.object! imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg` } @@ -49,10 +50,17 @@ const CharacterUnit = (props: Props) => { return (
    -
    {} }> - {character?.name.en} - { (props.editable) ? : '' } -
    + +
    + {character?.name.en} + { (props.editable) ? : '' } +
    +
    + { (gridCharacter && character) ? @@ -17,7 +9,7 @@ interface Props { exists: boolean found?: boolean offset: number - onClick: (position: number) => void + updateObject: (object: Character | Weapon | Summon, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -37,7 +29,7 @@ const ExtraSummons = (props: Props) => { position={props.offset + i} unitType={1} gridSummon={props.grid[props.offset + i]} - onClick={() => { props.onClick(props.offset + i) }} + updateObject={props.updateObject} updateUncap={props.updateUncap} /> diff --git a/components/ExtraWeapons/index.tsx b/components/ExtraWeapons/index.tsx index 71fcda5c..100d4ce6 100644 --- a/components/ExtraWeapons/index.tsx +++ b/components/ExtraWeapons/index.tsx @@ -3,21 +3,13 @@ import WeaponUnit from '~components/WeaponUnit' import './index.scss' -// GridType -export enum GridType { - Class, - Character, - Weapon, - Summon -} - // Props interface Props { grid: GridArray editable: boolean found?: boolean offset: number - onClick: (position: number) => void + updateObject: (object: Character | Weapon | Summon, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -37,7 +29,7 @@ const ExtraWeapons = (props: Props) => { position={props.offset + i} unitType={1} gridWeapon={props.grid[props.offset + i]} - onClick={() => { props.onClick(props.offset + i)}} + updateObject={props.updateObject} updateUncap={props.updateUncap} /> diff --git a/components/GridRep/index.tsx b/components/GridRep/index.tsx index 7d2b60b0..74a86f52 100644 --- a/components/GridRep/index.tsx +++ b/components/GridRep/index.tsx @@ -19,9 +19,9 @@ const GridRep = (props: Props) => { for (const [key, value] of Object.entries(props.grid)) { if (value.position == -1) - setMainhand(value.weapon) + setMainhand(value.object) else if (!value.mainhand && value.position != null) - newWeapons[value.position] = value.weapon + newWeapons[value.position] = value.object } setWeapons(newWeapons) diff --git a/components/Header/index.scss b/components/Header/index.scss index 5eb0d7cc..d5f5afea 100644 --- a/components/Header/index.scss +++ b/components/Header/index.scss @@ -1,8 +1,13 @@ -#Header { +.Header { display: flex; height: 34px; width: 100%; + &.bottom { + position: sticky; + bottom: $unit * 2; + } + #right { display: flex; gap: 8px; diff --git a/components/Header/index.tsx b/components/Header/index.tsx index 0ae20284..d82d83e3 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -1,82 +1,19 @@ -import React, { useContext, useEffect, useState } from 'react' -import { useCookies } from 'react-cookie' -import { useRouter } from 'next/router' - -import AppContext from '~context/AppContext' - -import Button from '~components/Button' -import HeaderMenu from '~components/HeaderMenu' +import React from 'react' import './index.scss' -interface Props {} +interface Props { + position: 'top' | 'bottom' + left: JSX.Element, + right: JSX.Element +} -const Header = (props: Props) => { - const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext) - - const [username, setUsername] = useState(undefined) - const [cookies, _, removeCookie] = useCookies(['user']) - - const router = useRouter() - - useEffect(() => { - if (cookies.user) { - setAuthenticated(true) - setUsername(cookies.user.username) - console.log(`Logged in as user "${cookies.user.username}"`) - } else { - setAuthenticated(false) - console.log('You are currently not logged in.') - } - }, [cookies, setUsername, setAuthenticated]) - - function copyToClipboard() { - const el = document.createElement('input') - el.value = window.location.href - el.id = 'url-input' - document.body.appendChild(el) - - el.select() - document.execCommand('copy') - el.remove() - } - - function newParty() { - router.push('/') - } - - function logout() { - removeCookie('user') - - setAuthenticated(false) - if (editable) setEditable(false) - - // How can we log out without navigating to root - router.push('/') - return false - } - +const Header = (props: Props) => { return ( -
  • - Teams + Teams
  • @@ -46,10 +46,7 @@ const HeaderMenu = (props: Props) => {
  • -
  • About
  • - {aboutOpen ? ( - - ) : null} +
  • Settings
  • Logout
  • @@ -62,14 +59,11 @@ const HeaderMenu = (props: Props) => { return (
      -
    • About
    • - {aboutOpen ? ( - - ) : null} +
    • - Teams + Teams
    • diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx index 3b78f2c8..f939c35a 100644 --- a/components/Layout/index.tsx +++ b/components/Layout/index.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from 'react' -import Header from '~components/Header' +import TopHeader from '~components/TopHeader' +import BottomHeader from '~components/BottomHeader' interface Props { children: ReactElement @@ -8,8 +9,9 @@ interface Props { const Layout = ({children}: Props) => { return ( <> -
      +
      {children}
      + ) } diff --git a/components/Modal/index.scss b/components/Modal/index.scss index cb93fc4b..acc22037 100644 --- a/components/Modal/index.scss +++ b/components/Modal/index.scss @@ -22,9 +22,9 @@ overflow-y: auto; padding: $unit * 3; position: relative; - z-index: 10; + z-index: 21; - #ModalTop { + #ModalHeader { display: flex; flex-direction: row; align-items: center; diff --git a/components/Overlay/index.scss b/components/Overlay/index.scss index 1895992e..2c6a0aa2 100644 --- a/components/Overlay/index.scss +++ b/components/Overlay/index.scss @@ -1,12 +1,12 @@ -.Overlay { - background: black; - position: absolute; - opacity: 0.28; +// .Overlay { +// background: black; +// position: absolute; +// opacity: 0.28; - height: 100%; - width: 100%; +// height: 100%; +// width: 100%; - top: 0; - left: 0; - z-index: 2; -} \ No newline at end of file +// top: 0; +// left: 0; + +// } \ No newline at end of file diff --git a/components/Party/index.tsx b/components/Party/index.tsx index fa20c988..2e6e2dce 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' +import { useSnapshot } from 'valtio' import { useCookies } from 'react-cookie' -import PartyContext from '~context/PartyContext' import PartySegmentedControl from '~components/PartySegmentedControl' import WeaponGrid from '~components/WeaponGrid' @@ -9,18 +9,11 @@ import SummonGrid from '~components/SummonGrid' import CharacterGrid from '~components/CharacterGrid' import api from '~utils/api' -import { TeamElement } from '~utils/enums' +import state from '~utils/state' +import { GridType, TeamElement } from '~utils/enums' import './index.scss' -// GridType -enum GridType { - Class, - Character, - Weapon, - Summon -} - // Props interface Props { slug?: string @@ -37,12 +30,8 @@ const Party = (props: Props) => { } : {} // Set up states + const { party } = useSnapshot(state) const [currentTab, setCurrentTab] = useState(GridType.Weapon) - const [id, setId] = useState('') - const [slug, setSlug] = useState('') - const [element, setElement] = useState(TeamElement.Any) - const [editable, setEditable] = useState(false) - const [hasExtra, setHasExtra] = useState(false) // Methods: Creating a new party async function createParty(extra: boolean = false) { @@ -58,10 +47,12 @@ const Party = (props: Props) => { // Methods: Updating the party's extra flag function checkboxChanged(event: React.ChangeEvent) { - setHasExtra(event.target.checked) - api.endpoints.parties.update(id, { - 'party': { 'is_extra': event.target.checked } - }, headers) + if (party.id) { + state.party.extra = event.target.checked + api.endpoints.parties.update(party.id, { + 'party': { 'is_extra': event.target.checked } + }, headers) + } } // Methods: Navigating with segmented control @@ -130,10 +121,8 @@ const Party = (props: Props) => { return (
      - - { navigation } - { currentGrid() } - + { navigation } + { currentGrid() }
      ) } diff --git a/components/PartySegmentedControl/index.tsx b/components/PartySegmentedControl/index.tsx index 3d30e8f4..ef448d68 100644 --- a/components/PartySegmentedControl/index.tsx +++ b/components/PartySegmentedControl/index.tsx @@ -1,19 +1,14 @@ import React, { useContext } from 'react' import './index.scss' -import PartyContext from '~context/PartyContext' +import state from '~utils/state' import SegmentedControl from '~components/SegmentedControl' import Segment from '~components/Segment' import ToggleSwitch from '~components/ToggleSwitch' -// GridType -export enum GridType { - Class, - Character, - Weapon, - Summon -} +import { GridType } from '~utils/enums' +import { useSnapshot } from 'valtio' interface Props { selectedTab: GridType @@ -22,10 +17,10 @@ interface Props { } const PartySegmentedControl = (props: Props) => { - const { editable, element, hasExtra } = useContext(PartyContext) + const { party } = useSnapshot(state) function getElement() { - switch(element) { + switch(party.element) { case 1: return "wind"; break case 2: return "fire"; break case 3: return "water"; break @@ -40,8 +35,8 @@ const PartySegmentedControl = (props: Props) => { Extra
    • @@ -80,7 +75,7 @@ const PartySegmentedControl = (props: Props) => { { (() => { - if (editable && props.selectedTab == GridType.Weapon) { + if (party.editable && props.selectedTab == GridType.Weapon) { return extraToggle } })() diff --git a/components/SearchModal/index.scss b/components/SearchModal/index.scss index 6882a290..ce57bae8 100644 --- a/components/SearchModal/index.scss +++ b/components/SearchModal/index.scss @@ -1,11 +1,11 @@ -.ModalContainer .Modal.SearchModal { +.Modal.Search { display: flex; flex-direction: column; min-height: 420px; min-width: 600px; padding: 0; - #ModalTop { + #ModalHeader { background: $grey-90; gap: $unit; margin: 0; @@ -13,12 +13,28 @@ position: sticky; top: 0; + button { + background: transparent; + border: none; + height: 42px; + padding: 0; + + svg { + height: 24px; + width: 24px; + vertical-align: middle; + } + } + label { width: 100%; .Input { + border: 1px solid $grey-70; + border-radius: $unit / 2; box-sizing: border-box; - padding: 12px 8px; + font-size: $font-regular; + padding: $unit * 1.5; text-align: left; width: 100%; } @@ -26,13 +42,13 @@ } } -.SearchModal #results_container { +.Search.Modal #results_container { margin: 0; max-height: 330px; padding: 0 12px 12px 12px; } -.SearchModal #NoResults { +.Search.Modal #NoResults { display: flex; flex-direction: column; align-items: center; @@ -40,7 +56,7 @@ flex-grow: 1; } -.SearchModal #NoResults h2 { +.Search.Modal #NoResults h2 { color: #ccc; font-size: $font-large; font-weight: 500; diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx index 29181049..a9b59964 100644 --- a/components/SearchModal/index.tsx +++ b/components/SearchModal/index.tsx @@ -1,10 +1,11 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' +import { useSnapshot } from 'valtio' -import { createPortal } from 'react-dom' +import state from '~utils/state' import api from '~utils/api' -import Modal from '~components/Modal' -import Overlay from '~components/Overlay' +import * as Dialog from '@radix-ui/react-dialog' + import CharacterResult from '~components/CharacterResult' import WeaponResult from '~components/WeaponResult' import SummonResult from '~components/SummonResult' @@ -13,138 +14,143 @@ import './index.scss' import PlusIcon from '~public/icons/Add.svg' interface Props { - close: () => void send: (object: Character | Weapon | Summon, position: number) => any - grid: GridArray placeholderText: string fromPosition: number - object: 'weapons' | 'characters' | 'summons' + object: 'weapons' | 'characters' | 'summons', + children: React.ReactNode } -interface State { - query: string, - results: { [key: string]: any } - loading: boolean - message: string - totalResults: number -} +const SearchModal = (props: Props) => { + let { grid } = useSnapshot(state) -class SearchModal extends React.Component { - searchInput: React.RefObject + let searchInput = React.createRef() - constructor(props: Props) { - super(props) - this.state = { - query: '', - results: {}, - loading: false, - message: '', - totalResults: 0 - } - this.searchInput = React.createRef() - } + const [objects, setObjects] = useState<{[id: number]: GridCharacter | GridWeapon | GridSummon}>() + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + const [results, setResults] = useState({}) + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [totalResults, setTotalResults] = useState(0) - componentDidMount() { - if (this.searchInput.current) { - this.searchInput.current.focus() - } - } + useEffect(() => { + setObjects(grid[props.object]) + }, [grid, props.object]) - filterExclusions = (o: Character | Weapon | Summon) => { - if (this.props.grid[this.props.fromPosition] && - o.granblue_id == this.props.grid[this.props.fromPosition].granblue_id) { - return null - } else return o - } + useEffect(() => { + if (searchInput.current) + searchInput.current.focus() + }, [searchInput]) - fetchResults = (query: string) => { - const excludes = Object.values(this.props.grid).filter(this.filterExclusions).map((o) => { return o.name.en }).join(',') + function inputChanged(event: React.ChangeEvent) { + const text = event.target.value + if (text.length) { + setQuery(text) + setLoading(true) + setMessage('') - api.search(this.props.object, query, excludes) - .then((response) => { - const data = response.data - const totalResults = data.length - this.setState({ - results: data, - totalResults: totalResults, - loading: false - }) - }, (error) => { - this.setState({ - loading: false, - message: error - }) - }) - } - - inputChanged = (event: React.ChangeEvent) => { - const query = event.target.value - if (query.length) { - this.setState({ query, loading: true, message: '' }, () => { - this.fetchResults(query) - }) + if (text.length > 2) + fetchResults() } else { - this.setState({ query, results: {}, message: '' }) + setQuery('') + setResults({}) + setMessage('') } } + + function fetchResults() { + // Exclude objects in grid from search results + // unless the object is in the position that the user clicked + // so that users can replace object versions with + // compatible other objects. + const excludes = (objects) ? Object.values(objects) + .filter(filterExclusions) + .map((o) => { return (o.object) ? o.object.name.en : undefined }).join(',') : '' - sendData = (result: Character | Weapon | Summon) => { - this.props.send(result, this.props.fromPosition) - this.props.close() + api.search(props.object, query, excludes) + .then(response => { + setResults(response.data) + setTotalResults(response.data.length) + setLoading(false) + }) + .catch(error => { + setMessage(error) + setLoading(false) + }) } - renderSearchResults = () => { - const { results } = this.state - - switch(this.props.object) { + function filterExclusions(gridObject: GridCharacter | GridWeapon | GridSummon) { + if (objects && gridObject.object && + gridObject.object.granblue_id === objects[props.fromPosition]?.object.granblue_id) + return null + else return gridObject + } + + function sendData(result: Character | Weapon | Summon) { + props.send(result, props.fromPosition) + setOpen(false) + } + + function renderResults() { + switch(props.object) { case 'weapons': - return this.renderWeaponSearchResults(results) - + return renderWeaponSearchResults(results) + break case 'summons': - return this.renderSummonSearchResults(results) - + return renderSummonSearchResults(results) + break case 'characters': - return this.renderCharacterSearchResults(results) + return renderCharacterSearchResults(results) + break } } - renderWeaponSearchResults = (results: { [key: string]: any }) => { - return ( -
        - { results.map( (result: Weapon) => { - return { this.sendData(result) }} /> - })} -
      - ) + function renderWeaponSearchResults(results: { [key: string]: any }) { + const elements = results.map((result: Weapon) => { + return { sendData(result) }} + /> + }) + + return (
        {elements}
      ) } - renderSummonSearchResults = (results: { [key: string]: any }) => { - return ( -
        - { results.map( (result: Summon) => { - return { this.sendData(result) }} /> - })} -
      - ) + function renderSummonSearchResults(results: { [key: string]: any }) { + const elements = results.map((result: Summon) => { + return { sendData(result) }} + /> + }) + + return (
        {elements}
      ) } - renderCharacterSearchResults = (results: { [key: string]: any }) => { - return ( -
        - { results.map( (result: Character) => { - return { this.sendData(result) }} /> - })} -
      - ) + function renderCharacterSearchResults(results: { [key: string]: any }) { + const elements = results.map((result: Character) => { + return { sendData(result) }} + /> + }) + + return (
        {elements}
      ) } - renderEmptyState = () => { + function renderEmptyState() { let string = '' - if (this.state.query === '') { - string = `No ${this.props.object}` + if (query === '') { + string = `No ${props.object}` + } else if (query.length < 3) { + string = `Type at least 3 characters` } else { - string = `No results found for '${this.state.query}'` + string = `No results found for '${query}'` } return ( @@ -153,48 +159,46 @@ class SearchModal extends React.Component {
    ) } - - render() { - const { query, loading } = this.state - - let content: JSX.Element - if (Object.entries(this.state.results).length == 0) { - content = this.renderEmptyState() - } else { - content = this.renderSearchResults() - } - - return ( - createPortal( -
    -
    -
    -
    - - -
    - - {content} -
    -
    - -
    , - document.body - ) - ) + + function resetAndClose() { + setQuery('') + setResults({}) + setOpen(true) } + + return ( + + + {props.children} + + +
    + +
    + + + + +
    + { ((Object.entries(results).length == 0) ? renderEmptyState() : renderResults()) } +
    +
    + +
    +
    + ) } export default SearchModal \ No newline at end of file diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx index c19ba415..ce53a204 100644 --- a/components/SummonGrid/index.tsx +++ b/components/SummonGrid/index.tsx @@ -1,19 +1,17 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useCookies } from 'react-cookie' -import { useModal as useModal } from '~utils/useModal' +import { useSnapshot } from 'valtio' import { AxiosResponse } from 'axios' import debounce from 'lodash.debounce' -import AppContext from '~context/AppContext' -import PartyContext from '~context/PartyContext' - -import SearchModal from '~components/SearchModal' import SummonUnit from '~components/SummonUnit' import ExtraSummons from '~components/ExtraSummons' import api from '~utils/api' +import state from '~utils/state' + import './index.scss' // Props @@ -36,55 +34,37 @@ const SummonGrid = (props: Props) => { } : {} // Set up state for view management + const { party, grid } = useSnapshot(state) + + const [slug, setSlug] = useState() const [found, setFound] = useState(false) const [loading, setLoading] = useState(true) - const { id, setId } = useContext(PartyContext) - const { slug, setSlug } = useContext(PartyContext) - const { editable, setEditable } = useContext(AppContext) - - // Set up states for Grid data - const [summons, setSummons] = useState>({}) - const [mainSummon, setMainSummon] = useState() - const [friendSummon, setFriendSummon] = useState() - - // Set up states for Search - const { open, openModal, closeModal } = useModal() - const [itemPositionForSearch, setItemPositionForSearch] = useState(0) // Create a temporary state to store previous weapon uncap value const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) - // Create a state dictionary to store pure objects for Search - const [searchGrid, setSearchGrid] = useState>({}) - // Fetch data from the server useEffect(() => { const shortcode = (props.slug) ? props.slug : slug if (shortcode) fetchGrid(shortcode) - else setEditable(true) + else state.party.editable = true }, [slug, props.slug]) // Initialize an array of current uncap values for each summon useEffect(() => { let initialPreviousUncapValues: {[key: number]: number} = {} - if (mainSummon) initialPreviousUncapValues[-1] = mainSummon.uncap_level - if (friendSummon) initialPreviousUncapValues[6] = friendSummon.uncap_level - Object.values(summons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + + if (state.grid.summons.mainSummon) + initialPreviousUncapValues[-1] = state.grid.summons.mainSummon.uncap_level + + if (state.grid.summons.friendSummon) + initialPreviousUncapValues[6] = state.grid.summons.friendSummon.uncap_level + + Object.values(state.grid.summons.allSummons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + setPreviousUncapValues(initialPreviousUncapValues) - }, [summons, mainSummon, friendSummon]) + }, [state.grid.summons.mainSummon, state.grid.summons.friendSummon, state.grid.summons.allSummons]) - // Update search grid whenever any summon is updated - useEffect(() => { - let newSearchGrid = Object.values(summons).map((o) => o.summon) - - if (mainSummon) - newSearchGrid.unshift(mainSummon.summon) - - if (friendSummon) - newSearchGrid.unshift(friendSummon.summon) - - setSearchGrid(newSearchGrid) - }, [summons, mainSummon, friendSummon]) // Methods: Fetching an object from the server async function fetchGrid(shortcode: string) { @@ -102,11 +82,12 @@ const SummonGrid = (props: Props) => { const loggedInUser = (cookies.user) ? cookies.user.user_id : '' if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) { - setEditable(true) + state.party.editable = true } // Store the important party and state-keeping values - setId(party.id) + state.party.id = party.id + setFound(true) setLoading(false) @@ -126,42 +107,34 @@ const SummonGrid = (props: Props) => { } function populateSummons(list: [GridSummon]) { - let summons: GridArray = {} - - list.forEach((object: GridSummon) => { - if (object.main) - setMainSummon(object) - else if (object.friend) - setFriendSummon(object) - else if (!object.main && !object.friend && object.position != null) - summons[object.position] = object + list.forEach((gridObject: GridSummon) => { + if (gridObject.main) + state.grid.summons.mainSummon = gridObject + else if (gridObject.friend) + state.grid.summons.friendSummon = gridObject + else if (!gridObject.main && !gridObject.friend && gridObject.position != null) + state.grid.summons.allSummons[gridObject.position] = gridObject }) - - setSummons(summons) } // Methods: Adding an object from search - function openSearchModal(position: number) { - setItemPositionForSearch(position) - openModal() - } - function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) { const summon = object as Summon - if (!id) { + if (!party.id) { props.createParty() .then(response => { const party = response.data.party - setId(party.id) + state.party.id = party.id setSlug(party.shortcode) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveSummon(party.id, summon, position) .then(response => storeGridSummon(response.data.grid_summon)) }) } else { - saveSummon(id, summon, position) + saveSummon(party.id, summon, position) .then(response => storeGridSummon(response.data.grid_summon)) } } @@ -184,16 +157,12 @@ const SummonGrid = (props: Props) => { } function storeGridSummon(gridSummon: GridSummon) { - if (gridSummon.position == -1) { - setMainSummon(gridSummon) - } else if (gridSummon.position == 6) { - setFriendSummon(gridSummon) - } else { - // Store the grid unit at the correct position - let newSummons = Object.assign({}, summons) - newSummons[gridSummon.position] = gridSummon - setSummons(newSummons) - } + if (gridSummon.position == -1) + state.grid.summons.mainSummon = gridSummon + else if (gridSummon.position == 6) + state.grid.summons.friendSummon = gridSummon + else + state.grid.summons.allSummons[gridSummon.position] = gridSummon } // Methods: Updating uncap level @@ -218,40 +187,41 @@ const SummonGrid = (props: Props) => { } } - const initiateUncapUpdate = useCallback( - (id: string, position: number, uncapLevel: number) => { - memoizeAction(id, position, uncapLevel) + function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { + memoizeAction(id, position, uncapLevel) - // Optimistically update UI - updateUncapLevel(position, uncapLevel) - }, [previousUncapValues, summons] - ) + // Optimistically update UI + updateUncapLevel(position, uncapLevel) + } const memoizeAction = useCallback( (id: string, position: number, uncapLevel: number) => { debouncedAction(id, position, uncapLevel) - }, [summons, mainSummon, friendSummon] + }, [props, previousUncapValues] ) const debouncedAction = useMemo(() => debounce((id, position, number) => { saveUncap(id, position, number) - }, 500), [summons, mainSummon, friendSummon, saveUncap] + }, 500), [props, saveUncap] ) const updateUncapLevel = (position: number, uncapLevel: number) => { - let newSummons = Object.assign({}, summons) - newSummons[position].uncap_level = uncapLevel - setSummons(newSummons) + if (state.grid.summons.mainSummon && position == -1) + state.grid.summons.mainSummon.uncap_level = uncapLevel + else if (state.grid.summons.friendSummon && position == 6) + state.grid.summons.friendSummon.uncap_level = uncapLevel + else + state.grid.summons.allSummons[position].uncap_level = uncapLevel } function storePreviousUncapValue(position: number) { // Save the current value in case of an unexpected result let newPreviousValues = {...previousUncapValues} - if (mainSummon && position == -1) newPreviousValues[position] = mainSummon.uncap_level - else if (friendSummon && position == 6) newPreviousValues[position] = friendSummon.uncap_level - else newPreviousValues[position] = summons[position].uncap_level + if (state.grid.summons.mainSummon && position == -1) newPreviousValues[position] = state.grid.summons.mainSummon.uncap_level + else if (state.grid.summons.friendSummon && position == 6) newPreviousValues[position] = state.grid.summons.friendSummon.uncap_level + else newPreviousValues[position] = state.grid.summons.allSummons[position].uncap_level setPreviousUncapValues(newPreviousValues) } @@ -261,12 +231,12 @@ const SummonGrid = (props: Props) => {
    Main Summon
    { openSearchModal(-1) }} + updateObject={receiveSummonFromSearch} updateUncap={initiateUncapUpdate} />
    @@ -276,12 +246,12 @@ const SummonGrid = (props: Props) => {
    Friend Summon
    { openSearchModal(6) }} + updateObject={receiveSummonFromSearch} updateUncap={initiateUncapUpdate} />
    @@ -293,11 +263,11 @@ const SummonGrid = (props: Props) => { {Array.from(Array(numSummons)).map((x, i) => { return (
  • { openSearchModal(i) }} + updateObject={receiveSummonFromSearch} updateUncap={initiateUncapUpdate} />
  • ) @@ -307,11 +277,11 @@ const SummonGrid = (props: Props) => { ) const subAuraSummonElement = ( ) @@ -324,17 +294,6 @@ const SummonGrid = (props: Props) => {
    { subAuraSummonElement } - - {open ? ( - - ) : null} ) } diff --git a/components/SummonResult/index.scss b/components/SummonResult/index.scss index ccf99f58..d92d790f 100644 --- a/components/SummonResult/index.scss +++ b/components/SummonResult/index.scss @@ -12,7 +12,7 @@ background: #e9e9e9; border-radius: 6px; display: inline-block; - height: 72px; + height: auto; width: 120px; } diff --git a/components/SummonUnit/index.tsx b/components/SummonUnit/index.tsx index 13246573..ce8cb3e5 100644 --- a/components/SummonUnit/index.tsx +++ b/components/SummonUnit/index.tsx @@ -1,18 +1,19 @@ import React, { useEffect, useState } from 'react' import classnames from 'classnames' +import SearchModal from '~components/SearchModal' import UncapIndicator from '~components/UncapIndicator' import PlusIcon from '~public/icons/Add.svg' import './index.scss' interface Props { - onClick: () => void - updateUncap: (id: string, position: number, uncap: number) => void gridSummon: GridSummon | undefined + unitType: 0 | 1 | 2 position: number editable: boolean - unitType: 0 | 1 | 2 + updateObject: (object: Character | Weapon | Summon, position: number) => void + updateUncap: (id: string, position: number, uncap: number) => void } const SummonUnit = (props: Props) => { @@ -28,7 +29,7 @@ const SummonUnit = (props: Props) => { }) const gridSummon = props.gridSummon - const summon = gridSummon?.summon + const summon = gridSummon?.object useEffect(() => { generateImageUrl() @@ -37,7 +38,7 @@ const SummonUnit = (props: Props) => { function generateImageUrl() { let imgSrc = "" if (props.gridSummon) { - const summon = props.gridSummon.summon! + const summon = props.gridSummon.object! // Generate the correct source for the summon if (props.unitType == 0 || props.unitType == 2) @@ -57,19 +58,27 @@ const SummonUnit = (props: Props) => { return (
    -
    {} }> - {summon?.name.en} - { (props.editable) ? : '' } -
    + +
    + {summon?.name.en} + { (props.editable) ? : '' } +
    +
    + { (gridSummon) ? - : '' } + : '' + }

    {summon?.name.en}

    diff --git a/components/TopHeader/index.scss b/components/TopHeader/index.scss new file mode 100644 index 00000000..e69de29b diff --git a/components/TopHeader/index.tsx b/components/TopHeader/index.tsx new file mode 100644 index 00000000..fa85365f --- /dev/null +++ b/components/TopHeader/index.tsx @@ -0,0 +1,89 @@ +import React, { useContext, useEffect, useState } from 'react' +import { useCookies } from 'react-cookie' +import { useRouter } from 'next/router' + +import AppContext from '~context/AppContext' + +import Header from '~components/Header' +import Button from '~components/Button' +import HeaderMenu from '~components/HeaderMenu' + +const TopHeader = () => { + const { editable, setEditable, authenticated, setAuthenticated } = useContext(AppContext) + + const [username, setUsername] = useState(undefined) + const [cookies, _, removeCookie] = useCookies(['user']) + + const router = useRouter() + + useEffect(() => { + if (cookies.user) { + setAuthenticated(true) + setUsername(cookies.user.username) + console.log(`Logged in as user "${cookies.user.username}"`) + } else { + setAuthenticated(false) + console.log('You are currently not logged in.') + } + }, [cookies, setUsername, setAuthenticated]) + + function copyToClipboard() { + const el = document.createElement('input') + el.value = window.location.href + el.id = 'url-input' + document.body.appendChild(el) + + el.select() + document.execCommand('copy') + el.remove() + } + + function newParty() { + router.push('/') + } + + function logout() { + removeCookie('user') + + setAuthenticated(false) + if (editable) setEditable(false) + + // TODO: How can we log out without navigating to root + router.push('/') + return false + } + + const leftNav = () => { + return ( +
    + + { (username) ? + : + + } +
    + ) + } + + const rightNav = () => { + return ( +
    + { (router.route === '/p/[party]') ? + : '' + } + +
    + ) + } + + + return ( +
    + ) +} + +export default TopHeader \ No newline at end of file diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index a15a6863..238636a1 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -1,19 +1,17 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useCookies } from 'react-cookie' -import { useModal as useModal } from '~utils/useModal' +import { useSnapshot } from 'valtio' import { AxiosResponse } from 'axios' import debounce from 'lodash.debounce' -import AppContext from '~context/AppContext' -import PartyContext from '~context/PartyContext' - -import SearchModal from '~components/SearchModal' import WeaponUnit from '~components/WeaponUnit' import ExtraWeapons from '~components/ExtraWeapons' import api from '~utils/api' +import state from '~utils/state' + import './index.scss' // Props @@ -36,58 +34,33 @@ const WeaponGrid = (props: Props) => { } : {} // Set up state for view management + const { party, grid } = useSnapshot(state) + + const [slug, setSlug] = useState() const [found, setFound] = useState(false) const [loading, setLoading] = useState(true) - - // Set up the party context - const { setEditable: setAppEditable } = useContext(AppContext) - const { id, setId } = useContext(PartyContext) - const { slug, setSlug } = useContext(PartyContext) - const { editable, setEditable } = useContext(PartyContext) - const { hasExtra, setHasExtra } = useContext(PartyContext) - const { setElement } = useContext(PartyContext) - - // Set up states for Grid data - const [weapons, setWeapons] = useState>({}) - const [mainWeapon, setMainWeapon] = useState() - - // Set up states for Search - const { open, openModal, closeModal } = useModal() - const [itemPositionForSearch, setItemPositionForSearch] = useState(0) // Create a temporary state to store previous weapon uncap values const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) - // Create a state dictionary to store pure objects for Search - const [searchGrid, setSearchGrid] = useState>({}) - // Fetch data from the server useEffect(() => { const shortcode = (props.slug) ? props.slug : slug if (shortcode) fetchGrid(shortcode) - else { - setEditable(true) - setAppEditable(true) - } + else state.party.editable = true }, [slug, props.slug]) // Initialize an array of current uncap values for each weapon useEffect(() => { let initialPreviousUncapValues: {[key: number]: number} = {} - if (mainWeapon) initialPreviousUncapValues[-1] = mainWeapon.uncap_level - Object.values(weapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + + if (state.grid.weapons.mainWeapon) + initialPreviousUncapValues[-1] = state.grid.weapons.mainWeapon.uncap_level + + Object.values(state.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + setPreviousUncapValues(initialPreviousUncapValues) - }, [mainWeapon, weapons]) - - // Update search grid whenever weapons or the mainhand are updated - useEffect(() => { - let newSearchGrid = Object.values(weapons).map((o) => o.weapon) - - if (mainWeapon) - newSearchGrid.unshift(mainWeapon.weapon) - - setSearchGrid(newSearchGrid) - }, [weapons, mainWeapon]) + }, [state.grid.weapons.mainWeapon, state.grid.weapons.allWeapons]) // Methods: Fetching an object from the server async function fetchGrid(shortcode: string) { @@ -105,13 +78,13 @@ const WeaponGrid = (props: Props) => { const loggedInUser = (cookies.user) ? cookies.user.user_id : '' if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) { - setEditable(true) - setAppEditable(true) + state.party.editable = true } // Store the important party and state-keeping values - setId(party.id) - setHasExtra(party.is_extra) + state.party.id = party.id + state.party.extra = party.is_extra + setFound(true) setLoading(false) @@ -131,43 +104,36 @@ const WeaponGrid = (props: Props) => { } function populateWeapons(list: [GridWeapon]) { - let weapons: GridArray = {} - - list.forEach((object: GridWeapon) => { - if (object.mainhand) { - setMainWeapon(object) - setElement(object.weapon.element) - } else if (!object.mainhand && object.position != null) { - weapons[object.position] = object + list.forEach((gridObject: GridWeapon) => { + if (gridObject.mainhand) { + state.grid.weapons.mainWeapon = gridObject + state.party.element = gridObject.object.element + } else if (!gridObject.mainhand && gridObject.position != null) { + state.grid.weapons.allWeapons[gridObject.position] = gridObject } }) - - setWeapons(weapons) } - + // Methods: Adding an object from search - function openSearchModal(position: number) { - setItemPositionForSearch(position) - openModal() - } - function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) { const weapon = object as Weapon - setElement(weapon.element) + if (position == 1) + state.party.element = weapon.element - if (!id) { - props.createParty(hasExtra) + if (!party.id) { + props.createParty(party.extra) .then(response => { const party = response.data.party - setId(party.id) + state.party.id = party.id setSlug(party.shortcode) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveWeapon(party.id, weapon, position) .then(response => storeGridWeapon(response.data.grid_weapon)) }) } else { - saveWeapon(id, weapon, position) + saveWeapon(party.id, weapon, position) .then(response => storeGridWeapon(response.data.grid_weapon)) } } @@ -190,12 +156,11 @@ const WeaponGrid = (props: Props) => { function storeGridWeapon(gridWeapon: GridWeapon) { if (gridWeapon.position == -1) { - setMainWeapon(gridWeapon) + state.grid.weapons.mainWeapon = gridWeapon + state.party.element = gridWeapon.object.element } else { // Store the grid unit at the correct position - let newWeapons = Object.assign({}, weapons) - newWeapons[gridWeapon.position] = gridWeapon - setWeapons(newWeapons) + state.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon } } @@ -241,32 +206,29 @@ const WeaponGrid = (props: Props) => { ) const updateUncapLevel = (position: number, uncapLevel: number) => { - if (mainWeapon && position == -1) { - mainWeapon.uncap_level = uncapLevel - setMainWeapon(mainWeapon) - } else { - let newWeapons = Object.assign({}, weapons) - newWeapons[position].uncap_level = uncapLevel - setWeapons(newWeapons) - } + if (state.grid.weapons.mainWeapon && position == -1) + state.grid.weapons.mainWeapon.uncap_level = uncapLevel + else + state.grid.weapons.allWeapons[position].uncap_level = uncapLevel } function storePreviousUncapValue(position: number) { // Save the current value in case of an unexpected result let newPreviousValues = {...previousUncapValues} - newPreviousValues[position] = (mainWeapon && position == -1) ? mainWeapon.uncap_level : weapons[position].uncap_level + newPreviousValues[position] = (state.grid.weapons.mainWeapon && position == -1) ? + state.grid.weapons.mainWeapon.uncap_level : state.grid.weapons.allWeapons[position].uncap_level setPreviousUncapValues(newPreviousValues) } // Render: JSX components const mainhandElement = ( { openSearchModal(-1) }} + updateObject={receiveWeaponFromSearch} updateUncap={initiateUncapUpdate} /> ) @@ -276,11 +238,11 @@ const WeaponGrid = (props: Props) => { return (
  • { openSearchModal(i) }} + updateObject={receiveWeaponFromSearch} updateUncap={initiateUncapUpdate} />
  • @@ -290,10 +252,10 @@ const WeaponGrid = (props: Props) => { const extraGridElement = ( ) @@ -305,18 +267,7 @@ const WeaponGrid = (props: Props) => {
      { weaponGridElement }
    - { (() => { return (hasExtra) ? extraGridElement : '' })() } - - {open ? ( - - ) : null} + { (() => { return (party.extra) ? extraGridElement : '' })() } ) } diff --git a/components/WeaponUnit/index.tsx b/components/WeaponUnit/index.tsx index 396ceeaa..12f83fa8 100644 --- a/components/WeaponUnit/index.tsx +++ b/components/WeaponUnit/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react' import classnames from 'classnames' +import SearchModal from '~components/SearchModal' import UncapIndicator from '~components/UncapIndicator' import PlusIcon from '~public/icons/Add.svg' @@ -11,7 +12,7 @@ interface Props { unitType: 0 | 1 position: number editable: boolean - onClick: () => void + updateObject: (object: Character | Weapon | Summon, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -27,7 +28,7 @@ const WeaponUnit = (props: Props) => { }) const gridWeapon = props.gridWeapon - const weapon = gridWeapon?.weapon + const weapon = gridWeapon?.object useEffect(() => { generateImageUrl() @@ -36,7 +37,7 @@ const WeaponUnit = (props: Props) => { function generateImageUrl() { let imgSrc = "" if (props.gridWeapon) { - const weapon = props.gridWeapon.weapon! + const weapon = props.gridWeapon.object! if (props.unitType == 0) imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg` @@ -55,15 +56,22 @@ const WeaponUnit = (props: Props) => { return (
    -
    {} }> - {weapon?.name.en} - { (props.editable) ? : '' } -
    + +
    + {weapon?.name.en} + { (props.editable) ? : '' } +
    +
    + { (gridWeapon) ? =16.8", + "react-dom": "^16.8 || ^17.0" + } + }, "node_modules/@radix-ui/react-arrow": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz", @@ -4293,6 +4314,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-valtio": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-valtio/-/eslint-plugin-valtio-0.4.1.tgz", + "integrity": "sha512-mORVREchU66YRWa0svret65i9U6gSliNThPH2GJEJlNHE/J1sYdcEcuobKAAMKlz5WpflC38nslkRxBKpiU/rA==", + "dev": true + }, "node_modules/eslint-scope": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", @@ -5762,6 +5789,11 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/proxy-compare": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz", + "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A==" + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6667,6 +6699,37 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/valtio": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.3.0.tgz", + "integrity": "sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==", + "dependencies": { + "proxy-compare": "2.0.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@babel/helper-module-imports": ">=7.12", + "@babel/types": ">=7.13", + "babel-plugin-macros": ">=3.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@babel/helper-module-imports": { + "optional": true + }, + "@babel/types": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -8284,6 +8347,20 @@ "@babel/runtime": "^7.13.10" } }, + "@radix-ui/react-alert-dialog": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.1.5.tgz", + "integrity": "sha512-Lq9h3GSvw752e7dFll3UWvm4uWiTlYAXLFX6wr/VQPRoa7XaQO8/1NBu4ikLHAecGEd/uDGZLY3aP7ovGPQYtg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "0.1.0", + "@radix-ui/react-compose-refs": "0.1.0", + "@radix-ui/react-context": "0.1.1", + "@radix-ui/react-dialog": "0.1.5", + "@radix-ui/react-primitive": "0.1.3", + "@radix-ui/react-slot": "0.1.2" + } + }, "@radix-ui/react-arrow": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.3.tgz", @@ -9813,6 +9890,12 @@ "dev": true, "requires": {} }, + "eslint-plugin-valtio": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-valtio/-/eslint-plugin-valtio-0.4.1.tgz", + "integrity": "sha512-mORVREchU66YRWa0svret65i9U6gSliNThPH2GJEJlNHE/J1sYdcEcuobKAAMKlz5WpflC38nslkRxBKpiU/rA==", + "dev": true + }, "eslint-scope": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", @@ -10820,6 +10903,11 @@ } } }, + "proxy-compare": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.0.2.tgz", + "integrity": "sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A==" + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -11429,6 +11517,14 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "valtio": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.3.0.tgz", + "integrity": "sha512-wsE6EDIkt+CNZPNHOxNVzoi026Fyt6ZRT750etZCAvrndcdT3N7Z+SSV4kJQdCwl5gNxsnU4BhP1wFS7cu21oA==", + "requires": { + "proxy-compare": "2.0.2" + } + }, "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/package.json b/package.json index 898e39c7..f6b2f8b1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^0.1.5", "@radix-ui/react-dialog": "^0.1.5", "@radix-ui/react-dropdown-menu": "^0.1.4", "@radix-ui/react-hover-card": "^0.1.3", @@ -27,7 +28,8 @@ "react-cookie": "^4.1.1", "react-dom": "^17.0.2", "react-i18next": "^11.15.3", - "sass": "^1.49.0" + "sass": "^1.49.0", + "valtio": "^1.3.0" }, "devDependencies": { "@types/lodash.debounce": "^4.0.6", @@ -36,6 +38,7 @@ "@types/react-dom": "^17.0.11", "eslint": "8.7.0", "eslint-config-next": "12.0.8", + "eslint-plugin-valtio": "^0.4.1", "typescript": "4.5.5" } } diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index a317520c..e43e2b07 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react' import { useRouter } from 'next/router' import Party from '~components/Party' +import * as AlertDialog from '@radix-ui/react-alert-dialog' const PartyRoute: React.FC = () => { const router = useRouter() diff --git a/public/icons/Cross.svg b/public/icons/Cross.svg new file mode 100644 index 00000000..0b1bbd9f --- /dev/null +++ b/public/icons/Cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/styles/globals.scss b/styles/globals.scss index 295a5a80..2d5e5894 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -3,25 +3,35 @@ html { background: $background-color; font-size: 62.5%; - padding: $unit * 2; } body { -webkit-font-smoothing: antialiased; - font-family: system-ui, -apple-system, Helvetica Neue, Helvetica, Arial, sans-serif; + box-sizing: border-box; + font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.4rem; + height: 100vh; + padding: $unit * 2 !important; &.no-scroll { overflow: hidden; } } +#__next { + height: 100%; +} + +main { + min-height: 90%; +} + a { text-decoration: none; } button, input { - font-family: system-ui, -apple-system, Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif; } h1, h2, h3, p { @@ -48,3 +58,80 @@ h1 { } +.Overlay { + background: rgba(0, 0, 0, 0.6); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 20; +} + +.Dialog { + animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running openModal; + background: white; + border-radius: $unit; + display: flex; + flex-direction: column; + gap: $unit * 3; + height: auto; + min-width: $unit * 48; + min-height: $unit * 12; + padding: $unit * 3; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 21; + + .DialogHeader { + display: flex; + } + + .DialogClose { + background: transparent; + height: 21px; + width: 21px; + + &:hover { + cursor: pointer; + + svg { + fill: $grey-00; + } + } + + svg { + fill: $grey-10; + } + } + + .DialogTitle { + font-size: $font-large; + flex-grow: 1; + } + + .DialogDescription { + flex-grow: 1; + } + + .actions { + display: flex; + justify-content: flex-end; + width: 100%; + } +} + +@keyframes openModal { + 0% { + opacity: 0; + transform: translate(-50%, -48%) scale(0.96); + } + + 100% { + // opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + diff --git a/types/Character.d.ts b/types/Character.d.ts index e004581e..01037ff1 100644 --- a/types/Character.d.ts +++ b/types/Character.d.ts @@ -1,4 +1,6 @@ interface Character { + type: 'character' + id: string granblue_id: string element: number diff --git a/types/GridCharacter.d.ts b/types/GridCharacter.d.ts index 47784b17..cb3481e8 100644 --- a/types/GridCharacter.d.ts +++ b/types/GridCharacter.d.ts @@ -1,6 +1,6 @@ -interface GridCharacter { +interface GridCharacter { id: string position: number - character: Character + object: Character uncap_level: number } \ No newline at end of file diff --git a/types/GridSummon.d.ts b/types/GridSummon.d.ts index 0ce8c2fc..e02aafdf 100644 --- a/types/GridSummon.d.ts +++ b/types/GridSummon.d.ts @@ -1,8 +1,8 @@ -interface GridSummon { +interface GridSummon { id: string main: boolean friend: boolean position: number - summon: Summon + object: Summon uncap_level: number } \ No newline at end of file diff --git a/types/GridWeapon.d.ts b/types/GridWeapon.d.ts index 3a6c2aa4..f2d7dd37 100644 --- a/types/GridWeapon.d.ts +++ b/types/GridWeapon.d.ts @@ -2,6 +2,6 @@ interface GridWeapon { id: string mainhand: boolean position: number - weapon: Weapon + object: Weapon uncap_level: number } \ No newline at end of file diff --git a/types/Summon.d.ts b/types/Summon.d.ts index 8b03037e..fdd961ac 100644 --- a/types/Summon.d.ts +++ b/types/Summon.d.ts @@ -1,4 +1,6 @@ interface Summon { + type: 'summon' + id: string granblue_id: number element: number diff --git a/types/Weapon.d.ts b/types/Weapon.d.ts index 295543ef..0f3c1ead 100644 --- a/types/Weapon.d.ts +++ b/types/Weapon.d.ts @@ -1,4 +1,6 @@ interface Weapon { + type: 'weapon' + id: string granblue_id: number element: number diff --git a/utils/enums.tsx b/utils/enums.tsx index 86e2af6d..ca8289b1 100644 --- a/utils/enums.tsx +++ b/utils/enums.tsx @@ -1,3 +1,8 @@ +export enum ButtonType { + Base, + Destructive +} + export enum GridType { Class, Character, diff --git a/utils/state.tsx b/utils/state.tsx new file mode 100644 index 00000000..42f66db4 --- /dev/null +++ b/utils/state.tsx @@ -0,0 +1,57 @@ +import { proxy } from "valtio"; + +interface State { + app: { + authenticated: boolean + }, + party: { + id: string | undefined, + editable: boolean, + element: number, + extra: boolean + }, + grid: { + weapons: { + mainWeapon: GridWeapon | undefined, + allWeapons: GridArray + }, + summons: { + mainSummon: GridSummon | undefined, + friendSummon: GridSummon | undefined, + allSummons: GridArray + }, + characters: GridArray + }, + search: { + sourceItem: GridCharacter | GridWeapon | GridSummon | undefined + } +} + +const state: State = { + app: { + authenticated: false + }, + party: { + id: undefined, + editable: false, + element: 0, + extra: false + }, + grid: { + weapons: { + mainWeapon: undefined, + allWeapons: {} + }, + summons: { + mainSummon: undefined, + friendSummon: undefined, + allSummons: {} + }, + characters: {} + }, + search: { + sourceItem: undefined + } +} + +export default proxy(state) \ No newline at end of file