Merge pull request #11 from jedmund/team-details

Add details to teams
This commit is contained in:
Justin Edmund 2022-02-26 16:00:09 -08:00 committed by GitHub
commit aa20d0d4ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 609 additions and 29 deletions

View file

@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import * as Scroll from 'react-scroll'
import * as AlertDialog from '@radix-ui/react-alert-dialog' import * as AlertDialog from '@radix-ui/react-alert-dialog'
@ -21,6 +22,7 @@ const BottomHeader = () => {
const app = useSnapshot(appState) const app = useSnapshot(appState)
const router = useRouter() const router = useRouter()
const scroll = Scroll.animateScroll;
// Cookies // Cookies
const [cookies] = useCookies(['user']) const [cookies] = useCookies(['user'])
@ -30,6 +32,15 @@ const BottomHeader = () => {
} }
} : {} } : {}
function toggleDetails() {
appState.party.detailsVisible = !appState.party.detailsVisible
if (appState.party.detailsVisible)
scroll.scrollToBottom()
else
scroll.scrollToTop()
}
function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { function deleteTeam(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (appState.party.editable && appState.party.id) { if (appState.party.editable && appState.party.id) {
api.endpoints.parties.destroy(appState.party.id, headers) api.endpoints.parties.destroy(appState.party.id, headers)
@ -53,9 +64,11 @@ const BottomHeader = () => {
} }
const leftNav = () => { const leftNav = () => {
return ( if (app.party.detailsVisible) {
<Button icon="edit" click={() => {}}>Add more info</Button> return (<Button icon="edit" active={true} click={toggleDetails}>Hide info</Button>)
) } else {
return (<Button icon="edit" click={toggleDetails}>Edit info</Button>)
}
} }
const rightNav = () => { const rightNav = () => {

View file

@ -61,6 +61,10 @@
} }
} }
&.Active {
background: white;
}
&.btn-blue { &.btn-blue {
background: $blue; background: $blue;
color: #8b8b8b; color: #8b8b8b;

View file

@ -14,6 +14,7 @@ import './index.scss'
import { ButtonType } from '~utils/enums' import { ButtonType } from '~utils/enums'
interface Props { interface Props {
active: boolean
disabled: boolean disabled: boolean
icon: string | null icon: string | null
type: ButtonType type: ButtonType
@ -26,6 +27,7 @@ interface State {
class Button extends React.Component<Props, State> { class Button extends React.Component<Props, State> {
static defaultProps: Props = { static defaultProps: Props = {
active: false,
disabled: false, disabled: false,
icon: null, icon: null,
type: ButtonType.Base, type: ButtonType.Base,
@ -65,6 +67,7 @@ class Button extends React.Component<Props, State> {
const classes = classNames({ const classes = classNames({
Button: true, Button: true,
'Active': this.props.active,
'btn-pressed': this.state.isPressed, 'btn-pressed': this.state.isPressed,
'btn-disabled': this.props.disabled, 'btn-disabled': this.props.disabled,
'destructive': this.props.type == ButtonType.Destructive 'destructive': this.props.type == ButtonType.Destructive

View file

@ -0,0 +1,29 @@
.Limited {
background: white;
border-radius: 6px;
border: 2px solid transparent;
box-sizing: border-box;
display: flex;
gap: $unit;
padding-right: $unit * 2;
&:focus-within {
border: 2px solid #275DC5;
box-shadow: 0 2px rgba(255, 255, 255, 1);
}
.Counter {
color: $grey-50;
font-weight: $bold;
line-height: 42px;
}
.Input {
background: transparent;
border-radius: 0;
&:focus {
outline: none;
}
}
}

View file

@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react'
import './index.scss'
interface Props {
fieldName: string
placeholder: string
value?: string
limit: number
error: string
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(function fieldSet(props, ref) {
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text'
const [currentCount, setCurrentCount] = useState(0)
useEffect(() => {
setCurrentCount((props.value) ? props.limit - props.value.length : props.limit)
}, [props.limit, props.value])
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
setCurrentCount(props.limit - event.currentTarget.value.length)
if (props.onChange) props.onChange(event)
}
return (
<fieldset className="Fieldset">
<div className="Limited">
<input
autoComplete="off"
className="Input"
type={fieldType}
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ''}
onBlur={props.onBlur}
onChange={onChange}
maxLength={props.limit}
ref={ref}
formNoValidate
/>
<span className="Counter">{currentCount}</span>
</div>
{
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset>
)
})
export default CharLimitedFieldset

View file

@ -4,6 +4,7 @@ import './index.scss'
interface Props { interface Props {
fieldName: string fieldName: string
placeholder: string placeholder: string
value?: string
error: string error: string
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
@ -17,9 +18,10 @@ const Fieldset = React.forwardRef<HTMLInputElement, Props>(function fieldSet(pro
<input <input
autoComplete="off" autoComplete="off"
className="Input" className="Input"
name={props.fieldName}
type={fieldType} type={fieldType}
name={props.fieldName}
placeholder={props.placeholder} placeholder={props.placeholder}
defaultValue={props.value || ''}
onBlur={props.onBlur} onBlur={props.onBlur}
onChange={props.onChange} onChange={props.onChange}
ref={ref} ref={ref}

View file

@ -1,4 +1,7 @@
.Extra { #Party {
margin-bottom: $unit * 4;
}
#Party .Extra {
color: #888; color: #888;
display: flex; display: flex;
font-weight: 500; font-weight: 500;

View file

@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import PartySegmentedControl from '~components/PartySegmentedControl' import PartySegmentedControl from '~components/PartySegmentedControl'
import PartyDetails from '~components/PartyDetails'
import WeaponGrid from '~components/WeaponGrid' import WeaponGrid from '~components/WeaponGrid'
import SummonGrid from '~components/SummonGrid' import SummonGrid from '~components/SummonGrid'
import CharacterGrid from '~components/CharacterGrid' import CharacterGrid from '~components/CharacterGrid'
@ -13,6 +13,7 @@ import { appState } from '~utils/appState'
import { GridType, TeamElement } from '~utils/enums' import { GridType, TeamElement } from '~utils/enums'
import './index.scss' import './index.scss'
import { AxiosResponse } from 'axios'
// Props // Props
interface Props { interface Props {
@ -33,29 +34,60 @@ const Party = (props: Props) => {
const { party } = useSnapshot(appState) const { party } = useSnapshot(appState)
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon) const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
// Fetch data from the server
useEffect(() => {
const shortcode = (props.slug) ? props.slug : undefined
if (shortcode)
fetchDetails(shortcode)
else
appState.party.editable = true
}, [props.slug])
// Methods: Creating a new party // Methods: Creating a new party
async function createParty(extra: boolean = false) { async function createParty(extra: boolean = false) {
let body = { let body = {
party: { party: {
...(cookies.user) && { user_id: cookies.user.user_id }, ...(cookies.user) && { user_id: cookies.user.user_id },
is_extra: extra extra: extra
} }
} }
return await api.endpoints.parties.create(body, headers) return await api.endpoints.parties.create(body, headers)
} }
// Methods: Updating the party's extra flag // Methods: Updating the party's details
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) { function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
appState.party.extra = event.target.checked appState.party.extra = event.target.checked
if (party.id) { if (party.id) {
api.endpoints.parties.update(party.id, { api.endpoints.parties.update(party.id, {
'party': { 'is_extra': event.target.checked } 'party': { 'extra': event.target.checked }
}, headers) }, headers)
} }
} }
function updateDetails(name?: string, description?: string, raid?: Raid) {
if (appState.party.name !== name ||
appState.party.description !== description ||
appState.party.raid?.id !== raid?.id) {
if (appState.party.id)
api.endpoints.parties.update(appState.party.id, {
'party': {
'name': name,
'description': description,
'raid_id': raid?.id
}
}, headers)
.then(() => {
appState.party.name = name
appState.party.description = description
appState.party.raid = raid
})
}
}
// Methods: Navigating with segmented control // Methods: Navigating with segmented control
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) { function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
switch(event.target.value) { switch(event.target.value) {
@ -76,6 +108,33 @@ const Party = (props: Props) => {
} }
} }
// Methods: Fetch party details
function fetchDetails(shortcode: string) {
return api.endpoints.parties.getOne({ id: shortcode })
.then(response => processResult(response))
.catch(error => processError(error))
}
function processResult(response: AxiosResponse) {
appState.party.id = response.data.party.id
// Store the party's user-generated details
appState.party.name = response.data.party.name
appState.party.description = response.data.party.description
appState.party.raid = response.data.party.raid
}
function processError(error: any) {
if (error.response != null) {
if (error.response.status == 404) {
// setFound(false)
// setLoading(false)
}
} else {
console.error(error)
}
}
// Render: JSX components // Render: JSX components
const navigation = ( const navigation = (
<PartySegmentedControl <PartySegmentedControl
@ -123,7 +182,13 @@ const Party = (props: Props) => {
return ( return (
<div> <div>
{ navigation } { navigation }
{ currentGrid() } <section id="Party">
{ currentGrid() }
</section>
{ <PartyDetails
editable={party.editable}
updateCallback={updateDetails}
/>}
</div> </div>
) )
} }

View file

@ -0,0 +1,87 @@
.Details {
display: none; // This breaks transition, find a workaround
opacity: 0;
margin: 0 auto;
max-width: $unit * 95;
position: relative;
&.Editable {
top: $unit;
height: 0;
z-index: 2;
transition: opacity 0.2s ease-in-out,
top 0.2s ease-in-out;
&.Visible {
display: block;
height: auto;
margin-bottom: 40vh;
opacity: 1;
top: 0;
}
fieldset {
display: block;
width: 100%;
textarea {
min-height: $unit * 20;
width: 100%;
}
}
select {
appearance: none;
background-image: url('/icons/Arrow.svg');
background-repeat: no-repeat;
background-position-y: center;
background-position-x: 98%;
background-size: $unit * 1.5;
border: none;
border-radius: 6px;
color: $grey-00;
margin-bottom: $unit;
font-size: $font-regular;
padding: 0 ($unit * 2);
height: $unit * 6;
width: 100%;
&:hover {
cursor: pointer;
}
}
}
&.ReadOnly {
top: $unit * -1;
transition: opacity 0.2s ease-in-out,
top 0.2s ease-in-out;
&.Visible {
display: block;
height: auto;
opacity: 1;
top: 0;
}
h1 {
font-size: $font-xlarge;
font-weight: $normal;
text-align: left;
margin-bottom: $unit;
}
.Raid {
color: $grey-50;
font-size: $font-regular;
font-weight: $medium;
margin-bottom: $unit * 2;
}
p {
font-size: $font-regular;
line-height: $font-regular * 1.2;
}
}
}

View file

@ -0,0 +1,114 @@
import React, { useState } from 'react'
import { useSnapshot } from 'valtio'
import classNames from 'classnames'
import CharLimitedFieldset from '~components/CharLimitedFieldset'
import TextFieldset from '~components/TextFieldset'
import { appState } from '~utils/appState'
import './index.scss'
import RaidDropdown from '~components/RaidDropdown'
// Props
interface Props {
editable: boolean
updateCallback: (name?: string, description?: string, raid?: Raid) => void
}
const PartyDetails = (props: Props) => {
const appSnapshot = useSnapshot(appState)
const nameInput = React.createRef<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>()
const raidSelect = React.createRef<HTMLSelectElement>()
const readOnlyClasses = classNames({
'Details': true,
'ReadOnly': true,
'Visible': !appSnapshot.party.detailsVisible
})
const editableClasses = classNames({
'Details': true,
'Editable': true,
'Visible': appSnapshot.party.detailsVisible
})
const [errors, setErrors] = useState<{ [key: string]: string }>({
name: '',
description: ''
})
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault()
const { name, value } = event.target
let newErrors = errors
setErrors(newErrors)
}
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
event.preventDefault()
const { name, value } = event.target
let newErrors = errors
setErrors(newErrors)
}
function updateDetails(event: React.ChangeEvent) {
const nameValue = nameInput.current?.value
const descriptionValue = descriptionInput.current?.value
const raid = appSnapshot.raids.find(raid => raid.id == raidSelect.current?.value)
props.updateCallback(nameValue, descriptionValue, raid)
}
const editable = (
<section className={editableClasses}>
<CharLimitedFieldset
fieldName="name"
placeholder="Name your team"
value={appSnapshot.party.name}
limit={50}
onBlur={updateDetails}
onChange={handleInputChange}
error={errors.name}
ref={nameInput}
/>
<RaidDropdown
selected={appSnapshot.party.raid?.id || ''}
onBlur={updateDetails}
ref={raidSelect}
/>
<TextFieldset
fieldName="name"
placeholder={"Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 1 first\nGood luck with RNG!"}
value={appSnapshot.party.description}
onBlur={updateDetails}
onChange={handleTextAreaChange}
error={errors.description}
ref={descriptionInput}
/>
</section>
)
const readOnly = (
<section className={readOnlyClasses}>
<h1>{ (appSnapshot.party.name) ? appSnapshot.party.name : 'No title' }</h1>
{ (appSnapshot.party.raid) ? <div className="Raid">{appSnapshot.party.raid.name.en}</div> : '' }
<p>{ (appSnapshot.party.description) ? appSnapshot.party.description : '' }</p>
</section>
)
return (
<div>
{readOnly}
{editable}
</div>
)
}
export default PartyDetails

View file

@ -3,6 +3,7 @@
gap: 58px; gap: 58px;
justify-content: center; justify-content: center;
margin: 0 auto; margin: 0 auto;
margin-bottom: $unit * 3;
max-width: 760px; max-width: 760px;
position: relative; position: relative;
} }

View file

View file

@ -0,0 +1,93 @@
import React, { useEffect, useState } from 'react'
import { useCookies } from 'react-cookie'
import { appState } from '~utils/appState'
import api from '~utils/api'
import './index.scss'
// Props
interface Props {
selected?: string
onBlur: (event: React.ChangeEvent<HTMLSelectElement>) => void
}
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function fieldSet(props, ref) {
const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? {
headers: { 'Authorization': `Bearer ${cookies.user.access_token}` }
} : {}
const [raids, setRaids] = useState<Raid[][]>()
const [flatRaids, setFlatRaids] = useState<Raid[]>()
const raidGroups = [
'Assorted',
'Omega',
'T1 Summons',
'T2 Summons',
'Primarchs',
'Nightmare',
'Omega (Impossible)',
'Omega II',
'Tier 1 Summons (Impossible)',
'Tier 3 Summons',
'Ennead',
'Malice',
'6-Star Raids',
'Six-Dragons',
'Nightmare (Impossible)',
'Astral',
'Super Ultimate'
]
useEffect(() => {
fetchRaids()
}, [])
function fetchRaids() {
api.endpoints.raids.getAll(headers)
.then((response) => {
const raids = response.data.map((r: any) => r.raid)
appState.raids = raids
organizeRaids(raids)
})
}
function organizeRaids(raids: Raid[]) {
const numGroups = Math.max.apply(Math, raids.map(raid => raid.group))
let groupedRaids = []
for (let i = 0; i <= numGroups; i++) {
groupedRaids[i] = raids.filter(raid => raid.group == i)
}
setRaids(groupedRaids)
}
function raidGroup(index: number) {
const options = raids && raids.length > 0 && raids[index].length > 0 &&
raids[index].sort((a, b) => a.element - b.element).map((item, i) => {
return (
<option key={i} value={item.id}>{item.name.en}</option>
)
})
return (
<optgroup key={index} label={raidGroups[index]}>
{options}
</optgroup>
)
}
return (
<select key={props.selected} defaultValue={props.selected} onBlur={props.onBlur} ref={ref}>
{ Array.from(Array(raids?.length)).map((x, i) => {
return raidGroup(i)
})}
</select>
)
})
export default RaidDropdown

View file

@ -1,7 +1,6 @@
.SegmentedControlWrapper { .SegmentedControlWrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-bottom: $unit * 3;
} }
.SegmentedControl { .SegmentedControl {

View file

@ -57,7 +57,7 @@ const SignupModal = (props: Props) => {
setErrors(newErrors) setErrors(newErrors)
}, (error) => { }, (error) => {
console.log(error) console.error(error)
}) })
} }
} }
@ -88,7 +88,7 @@ const SignupModal = (props: Props) => {
props.close() props.close()
}, (error) => { }, (error) => {
console.log(error) console.error(error)
}) })
} }
} }

View file

@ -0,0 +1,5 @@
.Fieldset textarea {
color: $grey-00;
font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 21px;
}

View file

@ -0,0 +1,33 @@
import React from 'react'
import './index.scss'
interface Props {
fieldName: string
placeholder: string
value?: string
error: string
onBlur?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
}
const TextFieldset = React.forwardRef<HTMLTextAreaElement, Props>(function fieldSet(props, ref) {
return (
<fieldset className="Fieldset">
<textarea
className="Input"
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ''}
onBlur={props.onBlur}
onChange={props.onChange}
ref={ref}
/>
{
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset>
)
})
export default TextFieldset

View file

@ -21,6 +21,10 @@
margin-bottom: $unit * 2; margin-bottom: $unit * 2;
margin-right: $unit * 2; margin-right: $unit * 2;
} }
&:nth-last-child(-n+3) {
margin-bottom: 0;
}
} }
.grid_weapons > *:nth-child(3n+3) { .grid_weapons > *:nth-child(3n+3) {

View file

@ -70,6 +70,8 @@ const WeaponGrid = (props: Props) => {
} }
function processResult(response: AxiosResponse) { function processResult(response: AxiosResponse) {
console.log("Retrieved data from server...")
// Store the response // Store the response
const party = response.data.party const party = response.data.party
@ -252,7 +254,7 @@ const WeaponGrid = (props: Props) => {
const extraGridElement = ( const extraGridElement = (
<ExtraWeapons <ExtraWeapons
grid={appState.grid.weapons.allWeapons} grid={grid.weapons.allWeapons}
editable={party.editable} editable={party.editable}
offset={numWeapons} offset={numWeapons}
updateObject={receiveWeaponFromSearch} updateObject={receiveWeaponFromSearch}

60
package-lock.json generated
View file

@ -24,6 +24,7 @@
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^11.15.3", "react-i18next": "^11.15.3",
"react-scroll": "^1.8.5",
"sass": "^1.49.0", "sass": "^1.49.0",
"valtio": "^1.3.0" "valtio": "^1.3.0"
}, },
@ -33,6 +34,7 @@
"@types/node": "17.0.11", "@types/node": "17.0.11",
"@types/react": "17.0.38", "@types/react": "17.0.38",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-scroll": "^1.8.3",
"eslint": "8.7.0", "eslint": "8.7.0",
"eslint-config-next": "12.0.8", "eslint-config-next": "12.0.8",
"eslint-plugin-valtio": "^0.4.1", "eslint-plugin-valtio": "^0.4.1",
@ -3093,6 +3095,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-scroll": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@types/react-scroll/-/react-scroll-1.8.3.tgz",
"integrity": "sha512-Xt0+Y58pwrIv+vRFxcyKDoo0gBWBR2eNMJ4XRMhQpZdGUtmjnszPi/wFEJSAY+5r83ypJsSA+hOpSc3hRpYlWw==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": { "node_modules/@types/scheduler": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -5330,6 +5341,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -5792,7 +5808,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -5802,8 +5817,7 @@
"node_modules/prop-types/node_modules/react-is": { "node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"dev": true
}, },
"node_modules/proxy-compare": { "node_modules/proxy-compare": {
"version": "2.0.2", "version": "2.0.2",
@ -5957,6 +5971,19 @@
} }
} }
}, },
"node_modules/react-scroll": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.8.5.tgz",
"integrity": "sha512-VcYFWDV2yGeuqeVCt3vxWTGWT4yCcefXOgvNZ16hSD0QTFzNNWiyZKWAVEgmz22PTKJlwRkspALaFI5+cr73OQ==",
"dependencies": {
"lodash.throttle": "^4.1.1",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^15.5.4 || ^16.0.0 || ^17.0.0",
"react-dom": "^15.5.4 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
@ -8943,6 +8970,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-scroll": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@types/react-scroll/-/react-scroll-1.8.3.tgz",
"integrity": "sha512-Xt0+Y58pwrIv+vRFxcyKDoo0gBWBR2eNMJ4XRMhQpZdGUtmjnszPi/wFEJSAY+5r83ypJsSA+hOpSc3hRpYlWw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/scheduler": { "@types/scheduler": {
"version": "0.16.2", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
@ -10594,6 +10630,11 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
},
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -10918,7 +10959,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"requires": { "requires": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -10928,8 +10968,7 @@
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"dev": true
} }
} }
}, },
@ -11020,6 +11059,15 @@
"tslib": "^1.0.0" "tslib": "^1.0.0"
} }
}, },
"react-scroll": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.8.5.tgz",
"integrity": "sha512-VcYFWDV2yGeuqeVCt3vxWTGWT4yCcefXOgvNZ16hSD0QTFzNNWiyZKWAVEgmz22PTKJlwRkspALaFI5+cr73OQ==",
"requires": {
"lodash.throttle": "^4.1.1",
"prop-types": "^15.7.2"
}
},
"react-style-singleton": { "react-style-singleton": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",

View file

@ -29,6 +29,7 @@
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^11.15.3", "react-i18next": "^11.15.3",
"react-scroll": "^1.8.5",
"sass": "^1.49.0", "sass": "^1.49.0",
"valtio": "^1.3.0" "valtio": "^1.3.0"
}, },
@ -38,6 +39,7 @@
"@types/node": "17.0.11", "@types/node": "17.0.11",
"@types/react": "17.0.38", "@types/react": "17.0.38",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-scroll": "^1.8.3",
"eslint": "8.7.0", "eslint": "8.7.0",
"eslint-config-next": "12.0.8", "eslint-config-next": "12.0.8",
"eslint-plugin-valtio": "^0.4.1", "eslint-plugin-valtio": "^0.4.1",

View file

@ -2,11 +2,9 @@ import React from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Party from '~components/Party' import Party from '~components/Party'
import * as AlertDialog from '@radix-ui/react-alert-dialog'
const PartyRoute: React.FC = () => { const PartyRoute: React.FC = () => {
const router = useRouter() const { party: slug } = useRouter().query
const { party: slug } = router.query
return ( return (
<div id="Content"> <div id="Content">

View file

@ -1,3 +1,3 @@
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 14 14" fill="#444" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.3121 4.60937C2.09636 4.43701 1.78173 4.47219 1.60937 4.68793C1.43701 4.90368 1.47219 5.2183 1.68793 5.39066L6.68107 9.37966C6.75234 9.4366 6.8344 9.47089 6.91856 9.48351C7.05774 9.51055 7.20746 9.47854 7.32678 9.38296L12.311 5.39025C12.5265 5.21761 12.5613 4.90294 12.3886 4.68742C12.216 4.4719 11.9013 4.43714 11.6858 4.60979L7.00557 8.35897L2.3121 4.60937Z" > <path fill-rule="evenodd" clip-rule="evenodd" d="M2.3121 4.60937C2.09636 4.43701 1.78173 4.47219 1.60937 4.68793C1.43701 4.90368 1.47219 5.2183 1.68793 5.39066L6.68107 9.37966C6.75234 9.4366 6.8344 9.47089 6.91856 9.48351C7.05774 9.51055 7.20746 9.47854 7.32678 9.38296L12.311 5.39025C12.5265 5.21761 12.5613 4.90294 12.3886 4.68742C12.216 4.4719 11.9013 4.43714 11.6858 4.60979L7.00557 8.35897L2.3121 4.60937Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 495 B

10
types/Raid.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
interface Raid {
id: string
name: {
en: string
jp: string
}
level: number
group: number
element: TeamElement
}

View file

@ -4,7 +4,7 @@ interface Entity {
name: string name: string
} }
type CollectionEndpoint = ({ query }: { query: AxiosRequestConfig }) => Promise<AxiosResponse<any>> type CollectionEndpoint = (headers?: {}) => Promise<AxiosResponse<any>>
type IdEndpoint = ({ id }: { id: string }) => Promise<AxiosResponse<any>> type IdEndpoint = ({ id }: { id: string }) => Promise<AxiosResponse<any>>
type IdWithObjectEndpoint = ({ id, object }: { id: string, object: string }) => Promise<AxiosResponse<any>> type IdWithObjectEndpoint = ({ id, object }: { id: string, object: string }) => Promise<AxiosResponse<any>>
type PostEndpoint = (object: {}, headers?: {}) => Promise<AxiosResponse<any>> type PostEndpoint = (object: {}, headers?: {}) => Promise<AxiosResponse<any>>
@ -41,7 +41,7 @@ class Api {
const resourceUrl = `${this.url}/${name}` const resourceUrl = `${this.url}/${name}`
return { return {
getAll: ({ query }: { query: AxiosRequestConfig }) => axios.get(resourceUrl, { params: { query }}), getAll: (headers?: {}) => axios.get(resourceUrl, headers),
getOne: ({ id }: { id: string }) => axios.get(`${resourceUrl}/${id}/`), getOne: ({ id }: { id: string }) => axios.get(`${resourceUrl}/${id}/`),
getOneWithObject: ({ id, object }: { id: string, object: string }) => axios.get(`${resourceUrl}/${id}/${object}`), getOneWithObject: ({ id, object }: { id: string, object: string }) => axios.get(`${resourceUrl}/${id}/${object}`),
create: (object: {}, headers?: {}) => axios.post(resourceUrl, object, headers), create: (object: {}, headers?: {}) => axios.post(resourceUrl, object, headers),
@ -88,5 +88,6 @@ api.createEntity( { name: 'parties' })
api.createEntity( { name: 'characters' }) api.createEntity( { name: 'characters' })
api.createEntity( { name: 'weapons' }) api.createEntity( { name: 'weapons' })
api.createEntity( { name: 'summons' }) api.createEntity( { name: 'summons' })
api.createEntity( { name: 'raids' })
export default api export default api

View file

@ -6,6 +6,10 @@ interface AppState {
party: { party: {
id: string | undefined, id: string | undefined,
editable: boolean, editable: boolean,
detailsVisible: boolean,
name: string | undefined,
description: string | undefined,
raid: Raid | undefined,
element: number, element: number,
extra: boolean extra: boolean
}, },
@ -23,13 +27,18 @@ interface AppState {
}, },
search: { search: {
sourceItem: GridCharacter | GridWeapon | GridSummon | undefined sourceItem: GridCharacter | GridWeapon | GridSummon | undefined
} },
raids: Raid[]
} }
export const initialAppState: AppState = { export const initialAppState: AppState = {
party: { party: {
id: undefined, id: undefined,
editable: false, editable: false,
detailsVisible: false,
name: undefined,
description: undefined,
raid: undefined,
element: 0, element: 0,
extra: false extra: false
}, },
@ -47,7 +56,8 @@ export const initialAppState: AppState = {
}, },
search: { search: {
sourceItem: undefined sourceItem: undefined
} },
raids: []
} }
export const appState = proxy(initialAppState) export const appState = proxy(initialAppState)