Add new fields to parties
I forgot to commit
This commit is contained in:
parent
9e2b7f2dd7
commit
54dd3feba7
26 changed files with 929 additions and 99 deletions
|
|
@ -8,43 +8,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit * 2;
|
gap: $unit * 2;
|
||||||
|
|
||||||
.Switch {
|
|
||||||
$height: 34px;
|
|
||||||
background: $grey-70;
|
|
||||||
border-radius: calc($height / 2);
|
|
||||||
border: none;
|
|
||||||
position: relative;
|
|
||||||
width: 58px;
|
|
||||||
height: $height;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0 2px $grey-15;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-state='checked'] {
|
|
||||||
background: $grey-15;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Thumb {
|
|
||||||
background: $grey-100;
|
|
||||||
border-radius: 13px;
|
|
||||||
display: block;
|
|
||||||
height: 26px;
|
|
||||||
width: 26px;
|
|
||||||
transition: transform 100ms;
|
|
||||||
transform: translateX(-1px);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-state='checked'] {
|
|
||||||
background: $grey-100;
|
|
||||||
transform: translateX(21px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.DialogDescription {
|
.DialogDescription {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { ForwardedRef, useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import Input from '~components/Input'
|
import Input from '~components/LabelledInput'
|
||||||
import Select from '~components/Select'
|
import Select from '~components/Select'
|
||||||
import SelectItem from '~components/SelectItem'
|
import SelectItem from '~components/SelectItem'
|
||||||
|
|
||||||
|
|
@ -187,7 +187,7 @@ const AwakeningSelect = (props: Props) => {
|
||||||
max={maxValue}
|
max={maxValue}
|
||||||
step="1"
|
step="1"
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
visible={`${awakeningType !== -1}`}
|
visible={awakeningType !== -1 ? true : false}
|
||||||
ref={awakeningLevelInput}
|
ref={awakeningLevelInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,6 @@
|
||||||
// box-shadow: 0 2px rgba(255, 255, 255, 1);
|
// box-shadow: 0 2px rgba(255, 255, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Input {
|
|
||||||
padding: $unit * 1.5 $unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Counter {
|
.Counter {
|
||||||
color: $grey-55;
|
color: $grey-55;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
|
|
@ -29,10 +25,13 @@
|
||||||
|
|
||||||
.Input {
|
.Input {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
padding: $unit * 1.5 $unit-2x;
|
||||||
padding-left: calc($unit-2x - $offset);
|
padding-left: calc($unit-2x - $offset);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
components/DurationInput/index.scss
Normal file
0
components/DurationInput/index.scss
Normal file
166
components/DurationInput/index.tsx
Normal file
166
components/DurationInput/index.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
import React, { useState, ChangeEvent, KeyboardEvent, useEffect } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import Input from '~components/Input'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> {
|
||||||
|
value: number
|
||||||
|
onValueChange: (value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DurationInput = React.forwardRef<HTMLInputElement, Props>(
|
||||||
|
function DurationInput(
|
||||||
|
{ className, placeholder, value, onValueChange },
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
const [duration, setDuration] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value > 0) setDuration(convertSecondsToString(value))
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
function convertStringToSeconds(string: string) {
|
||||||
|
const parts = string.split(':')
|
||||||
|
const minutes = parseInt(parts[0])
|
||||||
|
const seconds = parseInt(parts[1])
|
||||||
|
|
||||||
|
return minutes * 60 + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSecondsToString(value: number) {
|
||||||
|
const minutes = Math.floor(value / 60)
|
||||||
|
const seconds = value - minutes * 60
|
||||||
|
|
||||||
|
const paddedMinutes = padNumber(`${minutes}`, '0', 2)
|
||||||
|
|
||||||
|
return `${paddedMinutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function padNumber(string: string, pad: string, length: number) {
|
||||||
|
return (new Array(length + 1).join(pad) + string).slice(-length)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = event.currentTarget.value
|
||||||
|
const durationInSeconds = convertStringToSeconds(value)
|
||||||
|
onValueChange(durationInSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||||
|
// Allow the key to be processed normally
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current value
|
||||||
|
const input = event.currentTarget
|
||||||
|
let value = event.currentTarget.value
|
||||||
|
|
||||||
|
// Check if the key that was pressed is the backspace key
|
||||||
|
if (event.key === 'Backspace') {
|
||||||
|
// Remove the colon if the value is "12:"
|
||||||
|
if (value.length === 4) {
|
||||||
|
value = value.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the backspace key to be processed normally
|
||||||
|
input.value = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the key that was pressed is the tab key
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
// Allow the tab key to be processed normally
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the key that was pressed is an arrow key
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
// Step the value up by one
|
||||||
|
value = incrementTime(value)
|
||||||
|
} else if (event.key === 'ArrowDown') {
|
||||||
|
// Step the value down by one
|
||||||
|
value = decrementTime(value)
|
||||||
|
} else {
|
||||||
|
// Get the character that was entered
|
||||||
|
const char = parseInt(event.key)
|
||||||
|
|
||||||
|
// Check if the character is a number
|
||||||
|
const isNumber = !isNaN(char)
|
||||||
|
|
||||||
|
// Check if the character should be accepted or rejected
|
||||||
|
if (!isNumber || value.length >= 5) {
|
||||||
|
// Reject the character
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (value.length === 2) {
|
||||||
|
// Insert a colon after the second digit
|
||||||
|
input.value = value + ':'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementTime(time: string): string {
|
||||||
|
// Split the time into minutes and seconds
|
||||||
|
let [minutes, seconds] = time.split(':').map(Number)
|
||||||
|
|
||||||
|
// Increment the seconds
|
||||||
|
seconds += 1
|
||||||
|
|
||||||
|
// Check if the seconds have overflowed into the next minute
|
||||||
|
if (seconds >= 60) {
|
||||||
|
minutes += 1
|
||||||
|
seconds = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the time as a string and return it
|
||||||
|
return `${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementTime(time: string): string {
|
||||||
|
// Split the time into minutes and seconds
|
||||||
|
let [minutes, seconds] = time.split(':').map(Number)
|
||||||
|
|
||||||
|
// Decrement the seconds
|
||||||
|
seconds -= 1
|
||||||
|
|
||||||
|
// Check if the seconds have underflowed into the previous minute
|
||||||
|
if (seconds < 0) {
|
||||||
|
minutes -= 1
|
||||||
|
seconds = 59
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the minutes have underflowed into the previous hour
|
||||||
|
if (minutes < 0) {
|
||||||
|
minutes = 59
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the time as a string and return it
|
||||||
|
return `${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className={classNames(
|
||||||
|
{
|
||||||
|
Duration: true,
|
||||||
|
AlignRight: true,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
value={duration}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default DurationInput
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
.Label {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input {
|
.Input {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
border: none;
|
border: 2px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: block;
|
display: block;
|
||||||
padding: $unit-2x;
|
padding: $unit-2x;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&[type='number']::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid $blue;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.Bound {
|
&.Bound {
|
||||||
background-color: var(--input-bound-bg);
|
background-color: var(--input-bound-bg);
|
||||||
|
|
||||||
|
|
@ -22,6 +25,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.AlignRight {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
&.Hidden {
|
&.Hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(function Input(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<React.Fragment>
|
||||||
className={classNames({
|
|
||||||
Label: true,
|
|
||||||
Visible: props.visible,
|
|
||||||
})}
|
|
||||||
htmlFor={props.name}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
|
@ -55,11 +49,10 @@ const Input = React.forwardRef<HTMLInputElement, Props>(function Input(
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
formNoValidate
|
formNoValidate
|
||||||
/>
|
/>
|
||||||
{props.label}
|
|
||||||
{props.error && props.error.length > 0 && (
|
{props.error && props.error.length > 0 && (
|
||||||
<p className="InputError">{props.error}</p>
|
<p className="InputError">{props.error}</p>
|
||||||
)}
|
)}
|
||||||
</label>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
5
components/LabelledInput/index.scss
Normal file
5
components/LabelledInput/index.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.Label {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
68
components/LabelledInput/index.tsx
Normal file
68
components/LabelledInput/index.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> {
|
||||||
|
visible?: boolean
|
||||||
|
error?: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
visible: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabelledInput = React.forwardRef<HTMLInputElement, Props>(function Input(
|
||||||
|
props: Props,
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
// States
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
|
||||||
|
// Classes
|
||||||
|
const classes = classNames({ Input: true }, props.className)
|
||||||
|
const { defaultValue, ...inputProps } = props
|
||||||
|
|
||||||
|
// Change value when prop updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.value) setInputValue(`${props.value}`)
|
||||||
|
}, [props.value])
|
||||||
|
|
||||||
|
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
setInputValue(event.target.value)
|
||||||
|
if (props.onChange) props.onChange(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={classNames({
|
||||||
|
Label: true,
|
||||||
|
Visible: props.visible,
|
||||||
|
})}
|
||||||
|
htmlFor={props.name}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
{...inputProps}
|
||||||
|
autoComplete="off"
|
||||||
|
className={classes}
|
||||||
|
value={inputValue}
|
||||||
|
ref={forwardedRef}
|
||||||
|
onChange={handleChange}
|
||||||
|
formNoValidate
|
||||||
|
/>
|
||||||
|
{props.label}
|
||||||
|
{props.error && props.error.length > 0 && (
|
||||||
|
<p className="InputError">{props.error}</p>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
LabelledInput.defaultProps = defaultProps
|
||||||
|
|
||||||
|
export default LabelledInput
|
||||||
|
|
@ -9,7 +9,7 @@ import setUserToken from '~utils/setUserToken'
|
||||||
import { accountState } from '~utils/accountState'
|
import { accountState } from '~utils/accountState'
|
||||||
|
|
||||||
import Button from '~components/Button'
|
import Button from '~components/Button'
|
||||||
import Input from '~components/Input'
|
import Input from '~components/LabelledInput'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { getCookie } from 'cookies-next'
|
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
import PartySegmentedControl from '~components/PartySegmentedControl'
|
import PartySegmentedControl from '~components/PartySegmentedControl'
|
||||||
|
|
@ -13,6 +12,7 @@ import CharacterGrid from '~components/CharacterGrid'
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import { appState, initialAppState } from '~utils/appState'
|
import { appState, initialAppState } from '~utils/appState'
|
||||||
import { GridType, TeamElement } from '~utils/enums'
|
import { GridType, TeamElement } from '~utils/enums'
|
||||||
|
import type { DetailsObject } from '~types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
|
@ -59,25 +59,42 @@ const Party = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDetails(name?: string, description?: string, raid?: Raid) {
|
function updateDetails(details: DetailsObject) {
|
||||||
if (
|
if (
|
||||||
appState.party.name !== name ||
|
appState.party.name !== details.name ||
|
||||||
appState.party.description !== description ||
|
appState.party.description !== details.description ||
|
||||||
appState.party.raid?.id !== raid?.id
|
appState.party.raid?.id !== details.raid?.id
|
||||||
) {
|
) {
|
||||||
if (appState.party.id)
|
if (appState.party.id)
|
||||||
api.endpoints.parties
|
api.endpoints.parties
|
||||||
.update(appState.party.id, {
|
.update(appState.party.id, {
|
||||||
party: {
|
party: {
|
||||||
name: name,
|
name: details.name,
|
||||||
description: description,
|
description: details.description,
|
||||||
raid_id: raid?.id,
|
raid_id: details.raid?.id,
|
||||||
|
charge_attack: details.chargeAttack,
|
||||||
|
full_auto: details.fullAuto,
|
||||||
|
auto_guard: details.autoGuard,
|
||||||
|
clear_time: details.clearTime,
|
||||||
|
button_count: details.buttonCount,
|
||||||
|
chain_count: details.chainCount,
|
||||||
|
turn_count: details.turnCount,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
appState.party.name = name
|
appState.party.name = details.name
|
||||||
appState.party.description = description
|
appState.party.description = details.description
|
||||||
appState.party.raid = raid
|
appState.party.raid = details.raid
|
||||||
|
|
||||||
|
appState.party.chargeAttack = details.chargeAttack
|
||||||
|
appState.party.fullAuto = details.fullAuto
|
||||||
|
appState.party.autoGuard = details.autoGuard
|
||||||
|
|
||||||
|
appState.party.clearTime = details.clearTime
|
||||||
|
appState.party.buttonCount = details.buttonCount
|
||||||
|
appState.party.chainCount = details.chainCount
|
||||||
|
appState.party.turnCount = details.turnCount
|
||||||
|
|
||||||
appState.party.updated_at = party.updated_at
|
appState.party.updated_at = party.updated_at
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,94 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DetailToggleGroup {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
.ToggleSection,
|
||||||
|
.InputSection {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: $card-corner;
|
||||||
|
|
||||||
|
& > label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-size: $font-regular;
|
||||||
|
gap: $unit;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToggleSection {
|
||||||
|
padding: ($unit * 1.5) $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.InputSection {
|
||||||
|
padding: $unit-half $unit-2x;
|
||||||
|
padding-right: $unit-half;
|
||||||
|
|
||||||
|
.Input {
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.Input {
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
padding: $unit;
|
||||||
|
|
||||||
|
&:has(> input:focus) {
|
||||||
|
border: 2px solid $blue;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > input {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: $unit 0;
|
||||||
|
text-align: right;
|
||||||
|
width: 2rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input {
|
||||||
|
border-radius: 7px;
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit-half;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -75,6 +163,13 @@
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit-half;
|
||||||
|
margin-bottom: $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
.YoutubeWrapper {
|
.YoutubeWrapper {
|
||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border-radius: $card-corner;
|
border-radius: $card-corner;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
|
@ -8,32 +8,38 @@ import Linkify from 'react-linkify'
|
||||||
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import reactStringReplace from 'react-string-replace'
|
import reactStringReplace from 'react-string-replace'
|
||||||
import sanitizeHtml from 'sanitize-html'
|
|
||||||
|
|
||||||
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||||
|
|
||||||
import Button from '~components/Button'
|
import Button from '~components/Button'
|
||||||
import CharLimitedFieldset from '~components/CharLimitedFieldset'
|
import CharLimitedFieldset from '~components/CharLimitedFieldset'
|
||||||
|
import Input from '~components/Input'
|
||||||
|
import DurationInput from '~components/DurationInput'
|
||||||
|
import Token from '~components/Token'
|
||||||
|
|
||||||
import RaidDropdown from '~components/RaidDropdown'
|
import RaidDropdown from '~components/RaidDropdown'
|
||||||
import TextFieldset from '~components/TextFieldset'
|
import TextFieldset from '~components/TextFieldset'
|
||||||
|
import Switch from '~components/Switch'
|
||||||
|
|
||||||
import { accountState } from '~utils/accountState'
|
import { accountState } from '~utils/accountState'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
import { formatTimeAgo } from '~utils/timeAgo'
|
import { formatTimeAgo } from '~utils/timeAgo'
|
||||||
|
import { youtube } from '~utils/youtube'
|
||||||
|
|
||||||
import CheckIcon from '~public/icons/Check.svg'
|
import CheckIcon from '~public/icons/Check.svg'
|
||||||
import CrossIcon from '~public/icons/Cross.svg'
|
import CrossIcon from '~public/icons/Cross.svg'
|
||||||
import EditIcon from '~public/icons/Edit.svg'
|
import EditIcon from '~public/icons/Edit.svg'
|
||||||
|
|
||||||
|
import type { DetailsObject } from 'types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { youtube } from '~utils/youtube'
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
party?: Party
|
party?: Party
|
||||||
new: boolean
|
new: boolean
|
||||||
editable: boolean
|
editable: boolean
|
||||||
updateCallback: (name?: string, description?: string, raid?: Raid) => void
|
updateCallback: (details: DetailsObject) => void
|
||||||
deleteCallback: (
|
deleteCallback: (
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
) => void
|
) => void
|
||||||
|
|
@ -53,6 +59,17 @@ const PartyDetails = (props: Props) => {
|
||||||
const descriptionInput = React.createRef<HTMLTextAreaElement>()
|
const descriptionInput = React.createRef<HTMLTextAreaElement>()
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
|
||||||
|
const [chargeAttack, setChargeAttack] = useState(true)
|
||||||
|
const [fullAuto, setFullAuto] = useState(false)
|
||||||
|
const [autoGuard, setAutoGuard] = useState(false)
|
||||||
|
|
||||||
|
const [buttonCount, setButtonCount] = useState<number | undefined>(undefined)
|
||||||
|
const [chainCount, setChainCount] = useState<number | undefined>(undefined)
|
||||||
|
const [turnCount, setTurnCount] = useState<number | undefined>(undefined)
|
||||||
|
const [clearTime, setClearTime] = useState(0)
|
||||||
|
|
||||||
const [raidSlug, setRaidSlug] = useState('')
|
const [raidSlug, setRaidSlug] = useState('')
|
||||||
const [embeddedDescription, setEmbeddedDescription] =
|
const [embeddedDescription, setEmbeddedDescription] =
|
||||||
useState<React.ReactNode>()
|
useState<React.ReactNode>()
|
||||||
|
|
@ -88,23 +105,18 @@ const PartyDetails = (props: Props) => {
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
useEffect(() => {
|
||||||
event.preventDefault()
|
if (props.party) {
|
||||||
|
setName(props.party.name)
|
||||||
const { name, value } = event.target
|
setAutoGuard(props.party.auto_guard)
|
||||||
let newErrors = errors
|
setFullAuto(props.party.full_auto)
|
||||||
|
setChargeAttack(props.party.charge_attack)
|
||||||
setErrors(newErrors)
|
setClearTime(props.party.clear_time)
|
||||||
}
|
if (props.party.turn_count) setTurnCount(props.party.turn_count)
|
||||||
|
if (props.party.button_count) setButtonCount(props.party.button_count)
|
||||||
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
if (props.party.chain_count) setChainCount(props.party.chain_count)
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
const { name, value } = event.target
|
|
||||||
let newErrors = errors
|
|
||||||
|
|
||||||
setErrors(newErrors)
|
|
||||||
}
|
}
|
||||||
|
}, [props.party])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Extract the video IDs from the description
|
// Extract the video IDs from the description
|
||||||
|
|
@ -139,6 +151,100 @@ const PartyDetails = (props: Props) => {
|
||||||
}
|
}
|
||||||
}, [appState.party.description])
|
}, [appState.party.description])
|
||||||
|
|
||||||
|
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const { name, value } = event.target
|
||||||
|
setName(value)
|
||||||
|
|
||||||
|
let newErrors = errors
|
||||||
|
setErrors(newErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const { name, value } = event.target
|
||||||
|
let newErrors = errors
|
||||||
|
|
||||||
|
setErrors(newErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChargeAttackChanged(checked: boolean) {
|
||||||
|
setChargeAttack(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFullAutoChanged(checked: boolean) {
|
||||||
|
setFullAuto(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAutoGuardChanged(checked: boolean) {
|
||||||
|
setAutoGuard(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClearTimeInput(value: number) {
|
||||||
|
if (!isNaN(value)) setClearTime(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTurnCountInput(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = parseInt(event.currentTarget.value)
|
||||||
|
if (!isNaN(value)) setTurnCount(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleButtonCountInput(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = parseInt(event.currentTarget.value)
|
||||||
|
if (!isNaN(value)) setButtonCount(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChainCountInput(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = parseInt(event.currentTarget.value)
|
||||||
|
if (!isNaN(value)) setChainCount(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||||
|
// Allow the key to be processed normally
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current value
|
||||||
|
const input = event.currentTarget
|
||||||
|
let value = event.currentTarget.value
|
||||||
|
|
||||||
|
// Check if the key that was pressed is the backspace key
|
||||||
|
if (event.key === 'Backspace') {
|
||||||
|
// Remove the colon if the value is "12:"
|
||||||
|
if (value.length === 4) {
|
||||||
|
value = value.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the backspace key to be processed normally
|
||||||
|
input.value = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the key that was pressed is the tab key
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
// Allow the tab key to be processed normally
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the character that was entered and check if it is numeric
|
||||||
|
const char = parseInt(event.key)
|
||||||
|
const isNumber = !isNaN(char)
|
||||||
|
|
||||||
|
// Check if the character should be accepted or rejected
|
||||||
|
const numberValue = parseInt(`${value}${char}`)
|
||||||
|
const minValue = parseInt(event.currentTarget.min)
|
||||||
|
const maxValue = parseInt(event.currentTarget.max)
|
||||||
|
|
||||||
|
if (!isNumber || numberValue < minValue || numberValue > maxValue) {
|
||||||
|
// Reject the character if it isn't a number,
|
||||||
|
// or if it exceeds the min and max values
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchYoutubeData(videoId: string) {
|
async function fetchYoutubeData(videoId: string) {
|
||||||
return await youtube
|
return await youtube
|
||||||
.getVideoById(videoId, { maxResults: 1 })
|
.getVideoById(videoId, { maxResults: 1 })
|
||||||
|
|
@ -146,6 +252,11 @@ const PartyDetails = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDetails() {
|
function toggleDetails() {
|
||||||
|
if (name !== party.name) {
|
||||||
|
const resetName = party.name ? party.name : 'Untitled'
|
||||||
|
setName(resetName)
|
||||||
|
if (nameInput.current) nameInput.current.value = resetName
|
||||||
|
}
|
||||||
setOpen(!open)
|
setOpen(!open)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,12 +264,30 @@ const PartyDetails = (props: Props) => {
|
||||||
if (slug) setRaidSlug(slug)
|
if (slug) setRaidSlug(slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchValue(value: boolean) {
|
||||||
|
if (value) return 'on'
|
||||||
|
else return 'off'
|
||||||
|
}
|
||||||
|
|
||||||
function updateDetails(event: React.MouseEvent) {
|
function updateDetails(event: React.MouseEvent) {
|
||||||
const nameValue = nameInput.current?.value
|
const nameValue = nameInput.current?.value
|
||||||
const descriptionValue = descriptionInput.current?.value
|
const descriptionValue = descriptionInput.current?.value
|
||||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||||
|
|
||||||
props.updateCallback(nameValue, descriptionValue, raid)
|
const details: DetailsObject = {
|
||||||
|
fullAuto: fullAuto,
|
||||||
|
autoGuard: autoGuard,
|
||||||
|
chargeAttack: chargeAttack,
|
||||||
|
clearTime: clearTime,
|
||||||
|
buttonCount: buttonCount,
|
||||||
|
turnCount: turnCount,
|
||||||
|
chainCount: chainCount,
|
||||||
|
name: nameValue,
|
||||||
|
description: descriptionValue,
|
||||||
|
raid: raid,
|
||||||
|
}
|
||||||
|
|
||||||
|
props.updateCallback(details)
|
||||||
toggleDetails()
|
toggleDetails()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,10 +434,114 @@ const PartyDetails = (props: Props) => {
|
||||||
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
|
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
|
||||||
onChange={receiveRaid}
|
onChange={receiveRaid}
|
||||||
/>
|
/>
|
||||||
|
<ul className="SwitchToggleGroup DetailToggleGroup">
|
||||||
|
<li className="Ougi ToggleSection">
|
||||||
|
<label htmlFor="ougi">
|
||||||
|
<span>{t('party.details.labels.charge_attack')}</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
name="charge_attack"
|
||||||
|
onCheckedChange={handleChargeAttackChanged}
|
||||||
|
value={switchValue(chargeAttack)}
|
||||||
|
checked={chargeAttack}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li className="FullAuto ToggleSection">
|
||||||
|
<label htmlFor="full_auto">
|
||||||
|
<span>{t('party.details.labels.full_auto')}</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
onCheckedChange={handleFullAutoChanged}
|
||||||
|
name="full_auto"
|
||||||
|
value={switchValue(fullAuto)}
|
||||||
|
checked={fullAuto}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li className="AutoGuard ToggleSection">
|
||||||
|
<label htmlFor="auto_guard">
|
||||||
|
<span>{t('party.details.labels.auto_guard')}</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
onCheckedChange={handleAutoGuardChanged}
|
||||||
|
name="auto_guard"
|
||||||
|
value={switchValue(autoGuard)}
|
||||||
|
disabled={!fullAuto}
|
||||||
|
checked={autoGuard}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul className="InputToggleGroup DetailToggleGroup">
|
||||||
|
<li className="InputSection">
|
||||||
|
<label htmlFor="auto_guard">
|
||||||
|
<span>{t('party.details.labels.button_chain')}</span>
|
||||||
|
<div className="Input Bound">
|
||||||
|
<Input
|
||||||
|
name="buttons"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
value={`${buttonCount}`}
|
||||||
|
min="0"
|
||||||
|
max="99"
|
||||||
|
onChange={handleButtonCountInput}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
/>
|
||||||
|
<span>b</span>
|
||||||
|
<Input
|
||||||
|
name="chains"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
max="99"
|
||||||
|
value={`${chainCount}`}
|
||||||
|
onChange={handleChainCountInput}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
/>
|
||||||
|
<span>c</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li className="InputSection">
|
||||||
|
<label htmlFor="auto_guard">
|
||||||
|
<span>{t('party.details.labels.turn_count')}</span>
|
||||||
|
<Input
|
||||||
|
name="turn_count"
|
||||||
|
className="AlignRight Bound"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min="1"
|
||||||
|
max="999"
|
||||||
|
placeholder="0"
|
||||||
|
value={`${turnCount}`}
|
||||||
|
onChange={handleTurnCountInput}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li className="InputSection">
|
||||||
|
<label htmlFor="auto_guard">
|
||||||
|
<span>{t('party.details.labels.clear_time')}</span>
|
||||||
|
<div>
|
||||||
|
<DurationInput
|
||||||
|
name="clear_time"
|
||||||
|
className="Bound"
|
||||||
|
placeholder="00:00"
|
||||||
|
value={clearTime}
|
||||||
|
onValueChange={(value: number) => handleClearTimeInput(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<TextFieldset
|
<TextFieldset
|
||||||
fieldName="name"
|
fieldName="name"
|
||||||
placeholder={
|
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!'
|
'Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediel’s 3 first\nGood luck with RNG!'
|
||||||
}
|
}
|
||||||
value={props.party?.description}
|
value={props.party?.description}
|
||||||
onChange={handleTextAreaChange}
|
onChange={handleTextAreaChange}
|
||||||
|
|
@ -332,8 +565,63 @@ const PartyDetails = (props: Props) => {
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const clearTimeString = () => {
|
||||||
|
const minutes = Math.floor(clearTime / 60)
|
||||||
|
const seconds = clearTime - minutes * 60
|
||||||
|
|
||||||
|
if (minutes > 0)
|
||||||
|
return `${minutes}${t('party.details.suffix.minutes')} ${seconds}${t(
|
||||||
|
'party.details.suffix.seconds'
|
||||||
|
)}`
|
||||||
|
else return `${seconds}${t('party.details.suffix.seconds')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonChainToken = () => {
|
||||||
|
if (buttonCount || chainCount) {
|
||||||
|
let string = ''
|
||||||
|
|
||||||
|
if (buttonCount && buttonCount > 0) {
|
||||||
|
string += `${buttonCount}b`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buttonCount && chainCount && chainCount > 0) {
|
||||||
|
string += `0${t('party.details.suffix.buttons')}${chainCount}${t(
|
||||||
|
'party.details.suffix.chains'
|
||||||
|
)}`
|
||||||
|
} else if (buttonCount && chainCount && chainCount > 0) {
|
||||||
|
string += `${chainCount}${t('party.details.suffix.chains')}`
|
||||||
|
} else if (buttonCount && !chainCount) {
|
||||||
|
string += `0${t('party.details.suffix.chains')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Token>{string}</Token>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const readOnly = (
|
const readOnly = (
|
||||||
<section className={readOnlyClasses}>
|
<section className={readOnlyClasses}>
|
||||||
|
<section className="Details">
|
||||||
|
{
|
||||||
|
<Token>
|
||||||
|
{`${t('party.details.labels.charge_attack')} ${
|
||||||
|
chargeAttack ? 'On' : 'Off'
|
||||||
|
}`}
|
||||||
|
</Token>
|
||||||
|
}
|
||||||
|
{fullAuto ? <Token>{t('party.details.labels.full_auto')}</Token> : ''}
|
||||||
|
{autoGuard ? <Token>{t('party.details.labels.auto_guard')}</Token> : ''}
|
||||||
|
{turnCount ? (
|
||||||
|
<Token>
|
||||||
|
{t('party.details.turns.with_count', {
|
||||||
|
count: turnCount,
|
||||||
|
})}
|
||||||
|
</Token>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{clearTime > 0 ? <Token>{clearTimeString()}</Token> : ''}
|
||||||
|
{buttonChainToken()}
|
||||||
|
</section>
|
||||||
<Linkify>{embeddedDescription}</Linkify>
|
<Linkify>{embeddedDescription}</Linkify>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
@ -342,8 +630,8 @@ const PartyDetails = (props: Props) => {
|
||||||
<section className="DetailsWrapper">
|
<section className="DetailsWrapper">
|
||||||
<div className="PartyInfo">
|
<div className="PartyInfo">
|
||||||
<div className="Left">
|
<div className="Left">
|
||||||
<h1 className={!party.name ? 'empty' : ''}>
|
<h1 className={name === '' ? 'empty' : ''}>
|
||||||
{party.name ? party.name : 'Untitled'}
|
{name !== '' ? name : 'Untitled'}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="attribution">
|
<div className="attribution">
|
||||||
{renderUserBlock()}
|
{renderUserBlock()}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
DialogClose,
|
DialogClose,
|
||||||
} from '~components/Dialog'
|
} from '~components/Dialog'
|
||||||
|
|
||||||
import Input from '~components/Input'
|
import Input from '~components/LabelledInput'
|
||||||
import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar'
|
import CharacterSearchFilterBar from '~components/CharacterSearchFilterBar'
|
||||||
import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar'
|
import WeaponSearchFilterBar from '~components/WeaponSearchFilterBar'
|
||||||
import SummonSearchFilterBar from '~components/SummonSearchFilterBar'
|
import SummonSearchFilterBar from '~components/SummonSearchFilterBar'
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import setUserToken from '~utils/setUserToken'
|
||||||
import { accountState } from '~utils/accountState'
|
import { accountState } from '~utils/accountState'
|
||||||
|
|
||||||
import Button from '~components/Button'
|
import Button from '~components/Button'
|
||||||
import Input from '~components/Input'
|
import Input from '~components/LabelledInput'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
|
|
||||||
49
components/Switch/index.scss
Normal file
49
components/Switch/index.scss
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
.Switch {
|
||||||
|
$height: 34px;
|
||||||
|
background: $grey-70;
|
||||||
|
border-radius: calc($height / 2);
|
||||||
|
border: none;
|
||||||
|
position: relative;
|
||||||
|
width: 58px;
|
||||||
|
height: $height;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2px $grey-15;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='checked'] {
|
||||||
|
background: $grey-15;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
.SwitchThumb:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SwitchThumb {
|
||||||
|
background: $grey-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.SwitchThumb {
|
||||||
|
background: $grey-100;
|
||||||
|
border-radius: 13px;
|
||||||
|
display: block;
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
transition: transform 100ms;
|
||||||
|
transform: translateX(-1px);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='checked'] {
|
||||||
|
background: $grey-100;
|
||||||
|
transform: translateX(21px);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
components/Switch/index.tsx
Normal file
46
components/Switch/index.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react'
|
||||||
|
import * as RadixSwitch from '@radix-ui/react-switch'
|
||||||
|
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> {
|
||||||
|
thumbClass?: string
|
||||||
|
onCheckedChange: (checked: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Switch = (props: Props) => {
|
||||||
|
const {
|
||||||
|
checked,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
name,
|
||||||
|
onCheckedChange,
|
||||||
|
required,
|
||||||
|
thumbClass,
|
||||||
|
value,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const mainClasses = classNames({ Switch: true }, className)
|
||||||
|
const thumbClasses = classNames({ SwitchThumb: true }, thumbClass)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadixSwitch.Root
|
||||||
|
className={mainClasses}
|
||||||
|
checked={checked}
|
||||||
|
name={name}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
value={value}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
>
|
||||||
|
<RadixSwitch.Thumb className={thumbClasses} />
|
||||||
|
</RadixSwitch.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Switch
|
||||||
10
components/Token/index.scss
Normal file
10
components/Token/index.scss
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
.Token {
|
||||||
|
background: var(--input-bg);
|
||||||
|
border-radius: 99px;
|
||||||
|
display: inline;
|
||||||
|
font-size: $font-tiny;
|
||||||
|
font-weight: $bold;
|
||||||
|
min-width: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: $unit-half $unit;
|
||||||
|
}
|
||||||
20
components/Token/index.tsx
Normal file
20
components/Token/index.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
extends React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> {}
|
||||||
|
|
||||||
|
const Token = React.forwardRef<HTMLDivElement, Props>(function Token(
|
||||||
|
{ children, className, ...props },
|
||||||
|
forwardedRef
|
||||||
|
) {
|
||||||
|
const classes = classNames({ Token: true }, className)
|
||||||
|
return <div className={classes}>{children}</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Token
|
||||||
27
package-lock.json
generated
27
package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^10.5.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"next-usequerystate": "^1.7.0",
|
"next-usequerystate": "^1.7.0",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^11.15.5",
|
"react-i18next": "^11.15.5",
|
||||||
|
|
@ -46,6 +47,7 @@
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/node": "17.0.11",
|
"@types/node": "17.0.11",
|
||||||
|
"@types/pluralize": "^0.0.29",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.38",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-infinite-scroller": "^1.2.2",
|
"@types/react-infinite-scroller": "^1.2.2",
|
||||||
|
|
@ -2938,6 +2940,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pluralize": {
|
||||||
|
"version": "0.0.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz",
|
||||||
|
"integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
|
|
@ -5965,6 +5973,14 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pluralize": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.2.15",
|
"version": "8.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz",
|
||||||
|
|
@ -9201,6 +9217,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
||||||
},
|
},
|
||||||
|
"@types/pluralize": {
|
||||||
|
"version": "0.0.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz",
|
||||||
|
"integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
|
|
@ -11398,6 +11420,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
|
||||||
},
|
},
|
||||||
|
"pluralize": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="
|
||||||
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "8.2.15",
|
"version": "8.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"next-i18next": "^10.5.0",
|
"next-i18next": "^10.5.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"next-usequerystate": "^1.7.0",
|
"next-usequerystate": "^1.7.0",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^11.15.5",
|
"react-i18next": "^11.15.5",
|
||||||
|
|
@ -51,6 +52,7 @@
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/node": "17.0.11",
|
"@types/node": "17.0.11",
|
||||||
|
"@types/pluralize": "^0.0.29",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.38",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-infinite-scroller": "^1.2.2",
|
"@types/react-infinite-scroller": "^1.2.2",
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,26 @@
|
||||||
"characters": "Characters",
|
"characters": "Characters",
|
||||||
"weapons": "Weapons",
|
"weapons": "Weapons",
|
||||||
"summons": "Summons"
|
"summons": "Summons"
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"labels": {
|
||||||
|
"charge_attack": "Charge Attack",
|
||||||
|
"full_auto": "Full Auto",
|
||||||
|
"auto_guard": "Auto Guard",
|
||||||
|
"turn_count": "Turn count",
|
||||||
|
"button_chain": "Buttons/Chains",
|
||||||
|
"clear_time": "Clear time"
|
||||||
|
},
|
||||||
|
"suffix": {
|
||||||
|
"buttons": "b",
|
||||||
|
"chains": "c",
|
||||||
|
"minutes": "m",
|
||||||
|
"seconds": "s"
|
||||||
|
},
|
||||||
|
"turns": {
|
||||||
|
"with_count_one": "{{count}} turn",
|
||||||
|
"with_count_other": "{{count}} turns"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"saved": {
|
"saved": {
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,26 @@
|
||||||
"characters": "キャラ",
|
"characters": "キャラ",
|
||||||
"weapons": "武器",
|
"weapons": "武器",
|
||||||
"summons": "召喚石"
|
"summons": "召喚石"
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"labels": {
|
||||||
|
"charge_attack": "奥義",
|
||||||
|
"full_auto": "フルオート",
|
||||||
|
"auto_guard": "オートガード",
|
||||||
|
"turn_count": "経過ターン",
|
||||||
|
"button_chain": "ポチチェイン",
|
||||||
|
"clear_time": "討伐時間"
|
||||||
|
},
|
||||||
|
"suffix": {
|
||||||
|
"buttons": "ポチ",
|
||||||
|
"chains": "チェ",
|
||||||
|
"minutes": "分",
|
||||||
|
"seconds": "秒"
|
||||||
|
},
|
||||||
|
"turns": {
|
||||||
|
"with_count_one": "{{count}}ターン",
|
||||||
|
"with_count_other": "{{count}}ターン"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"saved": {
|
"saved": {
|
||||||
|
|
|
||||||
7
types/Party.d.ts
vendored
7
types/Party.d.ts
vendored
|
|
@ -11,6 +11,13 @@ interface Party {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
raid: Raid
|
raid: Raid
|
||||||
|
full_auto: boolean
|
||||||
|
auto_guard: boolean
|
||||||
|
charge_attack: boolean
|
||||||
|
clear_time: number
|
||||||
|
button_count?: number
|
||||||
|
turn_count?: number
|
||||||
|
chain_count?: number
|
||||||
job: Job
|
job: Job
|
||||||
job_skills: JobSkillObject
|
job_skills: JobSkillObject
|
||||||
shortcode: string
|
shortcode: string
|
||||||
|
|
|
||||||
14
types/index.d.ts
vendored
14
types/index.d.ts
vendored
|
|
@ -19,3 +19,17 @@ export type PaginationObject = {
|
||||||
totalPages: number
|
totalPages: number
|
||||||
perPage: number
|
perPage: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DetailsObject = {
|
||||||
|
[key: string]: boolean | number | string | Raid | undefined
|
||||||
|
fullAuto: boolean
|
||||||
|
autoGuard: boolean
|
||||||
|
chargeAttack: boolean
|
||||||
|
clearTime: number
|
||||||
|
buttonCount?: number
|
||||||
|
turnCount?: number
|
||||||
|
chainCount?: number
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
raid?: Raid
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,13 @@ interface AppState {
|
||||||
jobSkills: JobSkillObject
|
jobSkills: JobSkillObject
|
||||||
raid: Raid | undefined
|
raid: Raid | undefined
|
||||||
element: number
|
element: number
|
||||||
|
fullAuto: boolean
|
||||||
|
autoGuard: boolean
|
||||||
|
chargeAttack: boolean
|
||||||
|
clearTime: number
|
||||||
|
buttonCount?: number
|
||||||
|
turnCount?: number
|
||||||
|
chainCount?: number
|
||||||
extra: boolean
|
extra: boolean
|
||||||
user: User | undefined
|
user: User | undefined
|
||||||
favorited: boolean
|
favorited: boolean
|
||||||
|
|
@ -76,6 +83,13 @@ export const initialAppState: AppState = {
|
||||||
3: undefined,
|
3: undefined,
|
||||||
},
|
},
|
||||||
raid: undefined,
|
raid: undefined,
|
||||||
|
fullAuto: false,
|
||||||
|
autoGuard: false,
|
||||||
|
chargeAttack: true,
|
||||||
|
clearTime: 0,
|
||||||
|
buttonCount: undefined,
|
||||||
|
turnCount: undefined,
|
||||||
|
chainCount: undefined,
|
||||||
element: 0,
|
element: 0,
|
||||||
extra: false,
|
extra: false,
|
||||||
user: undefined,
|
user: undefined,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue