commit
aa20d0d4ac
26 changed files with 609 additions and 29 deletions
|
|
@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
|||
import { useCookies } from 'react-cookie'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import * as Scroll from 'react-scroll'
|
||||
|
||||
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ const BottomHeader = () => {
|
|||
const app = useSnapshot(appState)
|
||||
|
||||
const router = useRouter()
|
||||
const scroll = Scroll.animateScroll;
|
||||
|
||||
// Cookies
|
||||
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>) {
|
||||
if (appState.party.editable && appState.party.id) {
|
||||
api.endpoints.parties.destroy(appState.party.id, headers)
|
||||
|
|
@ -53,9 +64,11 @@ const BottomHeader = () => {
|
|||
}
|
||||
|
||||
const leftNav = () => {
|
||||
return (
|
||||
<Button icon="edit" click={() => {}}>Add more info</Button>
|
||||
)
|
||||
if (app.party.detailsVisible) {
|
||||
return (<Button icon="edit" active={true} click={toggleDetails}>Hide info</Button>)
|
||||
} else {
|
||||
return (<Button icon="edit" click={toggleDetails}>Edit info</Button>)
|
||||
}
|
||||
}
|
||||
|
||||
const rightNav = () => {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.Active {
|
||||
background: white;
|
||||
}
|
||||
|
||||
&.btn-blue {
|
||||
background: $blue;
|
||||
color: #8b8b8b;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import './index.scss'
|
|||
import { ButtonType } from '~utils/enums'
|
||||
|
||||
interface Props {
|
||||
active: boolean
|
||||
disabled: boolean
|
||||
icon: string | null
|
||||
type: ButtonType
|
||||
|
|
@ -26,6 +27,7 @@ interface State {
|
|||
|
||||
class Button extends React.Component<Props, State> {
|
||||
static defaultProps: Props = {
|
||||
active: false,
|
||||
disabled: false,
|
||||
icon: null,
|
||||
type: ButtonType.Base,
|
||||
|
|
@ -65,6 +67,7 @@ class Button extends React.Component<Props, State> {
|
|||
|
||||
const classes = classNames({
|
||||
Button: true,
|
||||
'Active': this.props.active,
|
||||
'btn-pressed': this.state.isPressed,
|
||||
'btn-disabled': this.props.disabled,
|
||||
'destructive': this.props.type == ButtonType.Destructive
|
||||
|
|
|
|||
29
components/CharLimitedFieldset/index.scss
Normal file
29
components/CharLimitedFieldset/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
components/CharLimitedFieldset/index.tsx
Normal file
54
components/CharLimitedFieldset/index.tsx
Normal 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
|
||||
|
|
@ -4,6 +4,7 @@ import './index.scss'
|
|||
interface Props {
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
error: string
|
||||
onBlur?: (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
|
||||
autoComplete="off"
|
||||
className="Input"
|
||||
name={props.fieldName}
|
||||
type={fieldType}
|
||||
name={props.fieldName}
|
||||
placeholder={props.placeholder}
|
||||
defaultValue={props.value || ''}
|
||||
onBlur={props.onBlur}
|
||||
onChange={props.onChange}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
.Extra {
|
||||
#Party {
|
||||
margin-bottom: $unit * 4;
|
||||
}
|
||||
#Party .Extra {
|
||||
color: #888;
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { useCookies } from 'react-cookie'
|
||||
|
||||
|
||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||
import PartyDetails from '~components/PartyDetails'
|
||||
import WeaponGrid from '~components/WeaponGrid'
|
||||
import SummonGrid from '~components/SummonGrid'
|
||||
import CharacterGrid from '~components/CharacterGrid'
|
||||
|
|
@ -13,6 +13,7 @@ import { appState } from '~utils/appState'
|
|||
import { GridType, TeamElement } from '~utils/enums'
|
||||
|
||||
import './index.scss'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -33,29 +34,60 @@ const Party = (props: Props) => {
|
|||
const { party } = useSnapshot(appState)
|
||||
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
|
||||
async function createParty(extra: boolean = false) {
|
||||
let body = {
|
||||
party: {
|
||||
...(cookies.user) && { user_id: cookies.user.user_id },
|
||||
is_extra: extra
|
||||
extra: extra
|
||||
}
|
||||
}
|
||||
|
||||
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>) {
|
||||
appState.party.extra = event.target.checked
|
||||
|
||||
if (party.id) {
|
||||
api.endpoints.parties.update(party.id, {
|
||||
'party': { 'is_extra': event.target.checked }
|
||||
'party': { 'extra': event.target.checked }
|
||||
}, 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
|
||||
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
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
|
||||
const navigation = (
|
||||
<PartySegmentedControl
|
||||
|
|
@ -123,7 +182,13 @@ const Party = (props: Props) => {
|
|||
return (
|
||||
<div>
|
||||
{ navigation }
|
||||
{ currentGrid() }
|
||||
<section id="Party">
|
||||
{ currentGrid() }
|
||||
</section>
|
||||
{ <PartyDetails
|
||||
editable={party.editable}
|
||||
updateCallback={updateDetails}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
87
components/PartyDetails/index.scss
Normal file
87
components/PartyDetails/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
components/PartyDetails/index.tsx
Normal file
114
components/PartyDetails/index.tsx
Normal 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 Fediel’s 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
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
gap: 58px;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
margin-bottom: $unit * 3;
|
||||
max-width: 760px;
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
|||
0
components/RaidDropdown/index.scss
Normal file
0
components/RaidDropdown/index.scss
Normal file
93
components/RaidDropdown/index.tsx
Normal file
93
components/RaidDropdown/index.tsx
Normal 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
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
.SegmentedControlWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
|
||||
.SegmentedControl {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
setErrors(newErrors)
|
||||
}, (error) => {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
props.close()
|
||||
}, (error) => {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
components/TextFieldset/index.scss
Normal file
5
components/TextFieldset/index.scss
Normal 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;
|
||||
}
|
||||
33
components/TextFieldset/index.tsx
Normal file
33
components/TextFieldset/index.tsx
Normal 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
|
||||
|
|
@ -21,6 +21,10 @@
|
|||
margin-bottom: $unit * 2;
|
||||
margin-right: $unit * 2;
|
||||
}
|
||||
|
||||
&:nth-last-child(-n+3) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.grid_weapons > *:nth-child(3n+3) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ const WeaponGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
function processResult(response: AxiosResponse) {
|
||||
console.log("Retrieved data from server...")
|
||||
|
||||
// Store the response
|
||||
const party = response.data.party
|
||||
|
||||
|
|
@ -252,7 +254,7 @@ const WeaponGrid = (props: Props) => {
|
|||
|
||||
const extraGridElement = (
|
||||
<ExtraWeapons
|
||||
grid={appState.grid.weapons.allWeapons}
|
||||
grid={grid.weapons.allWeapons}
|
||||
editable={party.editable}
|
||||
offset={numWeapons}
|
||||
updateObject={receiveWeaponFromSearch}
|
||||
|
|
|
|||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.3",
|
||||
"react-scroll": "^1.8.5",
|
||||
"sass": "^1.49.0",
|
||||
"valtio": "^1.3.0"
|
||||
},
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
"@types/node": "17.0.11",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-scroll": "^1.8.3",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-config-next": "12.0.8",
|
||||
"eslint-plugin-valtio": "^0.4.1",
|
||||
|
|
@ -3093,6 +3095,15 @@
|
|||
"@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": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
|
|
@ -5330,6 +5341,11 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
|
@ -5792,7 +5808,6 @@
|
|||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
|
|
@ -5802,8 +5817,7 @@
|
|||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
|
||||
|
|
@ -8943,6 +8970,15 @@
|
|||
"@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": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||
|
|
@ -10594,6 +10630,11 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
|
@ -10918,7 +10959,6 @@
|
|||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
|
|
@ -10928,8 +10968,7 @@
|
|||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -11020,6 +11059,15 @@
|
|||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.3",
|
||||
"react-scroll": "^1.8.5",
|
||||
"sass": "^1.49.0",
|
||||
"valtio": "^1.3.0"
|
||||
},
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
"@types/node": "17.0.11",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-scroll": "^1.8.3",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-config-next": "12.0.8",
|
||||
"eslint-plugin-valtio": "^0.4.1",
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@ import React 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()
|
||||
const { party: slug } = router.query
|
||||
const { party: slug } = useRouter().query
|
||||
|
||||
return (
|
||||
<div id="Content">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<svg viewBox="0 0 14 14" fill="none" 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" >
|
||||
<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" />
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 495 B |
10
types/Raid.d.ts
vendored
Normal file
10
types/Raid.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface Raid {
|
||||
id: string
|
||||
name: {
|
||||
en: string
|
||||
jp: string
|
||||
}
|
||||
level: number
|
||||
group: number
|
||||
element: TeamElement
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ interface Entity {
|
|||
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 IdWithObjectEndpoint = ({ id, object }: { id: string, object: string }) => Promise<AxiosResponse<any>>
|
||||
type PostEndpoint = (object: {}, headers?: {}) => Promise<AxiosResponse<any>>
|
||||
|
|
@ -41,7 +41,7 @@ class Api {
|
|||
const resourceUrl = `${this.url}/${name}`
|
||||
|
||||
return {
|
||||
getAll: ({ query }: { query: AxiosRequestConfig }) => axios.get(resourceUrl, { params: { query }}),
|
||||
getAll: (headers?: {}) => axios.get(resourceUrl, headers),
|
||||
getOne: ({ id }: { id: string }) => axios.get(`${resourceUrl}/${id}/`),
|
||||
getOneWithObject: ({ id, object }: { id: string, object: string }) => axios.get(`${resourceUrl}/${id}/${object}`),
|
||||
create: (object: {}, headers?: {}) => axios.post(resourceUrl, object, headers),
|
||||
|
|
@ -88,5 +88,6 @@ api.createEntity( { name: 'parties' })
|
|||
api.createEntity( { name: 'characters' })
|
||||
api.createEntity( { name: 'weapons' })
|
||||
api.createEntity( { name: 'summons' })
|
||||
api.createEntity( { name: 'raids' })
|
||||
|
||||
export default api
|
||||
|
|
@ -6,6 +6,10 @@ interface AppState {
|
|||
party: {
|
||||
id: string | undefined,
|
||||
editable: boolean,
|
||||
detailsVisible: boolean,
|
||||
name: string | undefined,
|
||||
description: string | undefined,
|
||||
raid: Raid | undefined,
|
||||
element: number,
|
||||
extra: boolean
|
||||
},
|
||||
|
|
@ -23,13 +27,18 @@ interface AppState {
|
|||
},
|
||||
search: {
|
||||
sourceItem: GridCharacter | GridWeapon | GridSummon | undefined
|
||||
}
|
||||
},
|
||||
raids: Raid[]
|
||||
}
|
||||
|
||||
export const initialAppState: AppState = {
|
||||
party: {
|
||||
id: undefined,
|
||||
editable: false,
|
||||
detailsVisible: false,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
raid: undefined,
|
||||
element: 0,
|
||||
extra: false
|
||||
},
|
||||
|
|
@ -47,7 +56,8 @@ export const initialAppState: AppState = {
|
|||
},
|
||||
search: {
|
||||
sourceItem: undefined
|
||||
}
|
||||
},
|
||||
raids: []
|
||||
}
|
||||
|
||||
export const appState = proxy(initialAppState)
|
||||
Loading…
Reference in a new issue