Began refactoring PartyDetails into several files
* PartyHeader will live at the top, above the new segmented control * PartyDetails stays below, only showing remixed teams and the description * PartyDropdown handles the new ... menu
This commit is contained in:
parent
ef5fd20497
commit
f8df3a2a49
8 changed files with 1505 additions and 550 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
|
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
width: 30vw;
|
width: 30vw;
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
margin: 0 $unit-2x;
|
margin: 0 $unit-2x;
|
||||||
|
|
@ -130,6 +131,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .destructive {
|
||||||
|
color: $error;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $error;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
color: $grey-50;
|
color: $grey-50;
|
||||||
|
|
||||||
|
|
@ -177,12 +186,12 @@
|
||||||
.MenuGroup {
|
.MenuGroup {
|
||||||
border-bottom: 1px solid var(--menu-separator);
|
border-bottom: 1px solid var(--menu-separator);
|
||||||
|
|
||||||
&:first-child .MenuItem:first-child:hover {
|
&:first-child .MenuItem:first-child {
|
||||||
border-top-left-radius: 6px;
|
border-top-left-radius: 6px;
|
||||||
border-top-right-radius: 6px;
|
border-top-right-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child .MenuItem:last-child:hover {
|
&:last-child .MenuItem:last-child {
|
||||||
border-bottom-left-radius: 6px;
|
border-bottom-left-radius: 6px;
|
||||||
border-bottom-right-radius: 6px;
|
border-bottom-right-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,11 @@
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav.RepNavigation {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: $unit-4x;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ import type { DetailsObject } from '~types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
import WeaponRep from '~components/reps/WeaponRep'
|
||||||
|
import CharacterRep from '~components/reps/CharacterRep'
|
||||||
|
import SummonRep from '~components/reps/SummonRep'
|
||||||
|
import PartyHeader from '../PartyHeader'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
new?: boolean
|
new?: boolean
|
||||||
|
|
@ -160,6 +165,29 @@ const Party = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remixing the party
|
||||||
|
function remixTeam() {
|
||||||
|
// setOriginalName(partySnapshot.name ? partySnapshot.name : t('no_title'))
|
||||||
|
|
||||||
|
if (props.team && props.team.shortcode) {
|
||||||
|
const body = getLocalId()
|
||||||
|
api
|
||||||
|
.remix({ shortcode: props.team.shortcode, body: body })
|
||||||
|
.then((response) => {
|
||||||
|
const remix = response.data.party
|
||||||
|
|
||||||
|
// Store the edit key in local storage
|
||||||
|
if (remix.edit_key) {
|
||||||
|
storeEditKey(remix.id, remix.edit_key)
|
||||||
|
setEditKey(remix.id, remix.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(`/p/${remix.shortcode}`)
|
||||||
|
// setRemixToastOpen(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deleting the party
|
// Deleting the party
|
||||||
function deleteTeam() {
|
function deleteTeam() {
|
||||||
if (props.team && editable) {
|
if (props.team && editable) {
|
||||||
|
|
@ -348,14 +376,23 @@ const Party = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
<PartyHeader
|
||||||
|
party={props.team}
|
||||||
|
new={props.new || false}
|
||||||
|
editable={party.editable}
|
||||||
|
deleteCallback={deleteTeam}
|
||||||
|
remixCallback={remixTeam}
|
||||||
|
updateCallback={updateDetails}
|
||||||
|
/>
|
||||||
|
|
||||||
{navigation}
|
{navigation}
|
||||||
|
|
||||||
<section id="Party">{currentGrid()}</section>
|
<section id="Party">{currentGrid()}</section>
|
||||||
<PartyDetails
|
<PartyDetails
|
||||||
party={props.team}
|
party={props.team}
|
||||||
new={props.new || false}
|
new={props.new || false}
|
||||||
editable={party.editable}
|
editable={party.editable}
|
||||||
updateCallback={updateDetails}
|
updateCallback={updateDetails}
|
||||||
deleteCallback={deleteTeam}
|
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { subscribe, useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
|
@ -10,27 +9,20 @@ 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 Alert from '~components/common/Alert'
|
|
||||||
import Button from '~components/common/Button'
|
import Button from '~components/common/Button'
|
||||||
import CharLimitedFieldset from '~components/common/CharLimitedFieldset'
|
|
||||||
import DurationInput from '~components/common/DurationInput'
|
|
||||||
import GridRepCollection from '~components/GridRepCollection'
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
import GridRep from '~components/GridRep'
|
import GridRep from '~components/GridRep'
|
||||||
import Input from '~components/common/Input'
|
|
||||||
import RaidDropdown from '~components/RaidDropdown'
|
|
||||||
import Switch from '~components/common/Switch'
|
|
||||||
import Tooltip from '~components/common/Tooltip'
|
import Tooltip from '~components/common/Tooltip'
|
||||||
import TextFieldset from '~components/common/TextFieldset'
|
import TextFieldset from '~components/common/TextFieldset'
|
||||||
import Token from '~components/common/Token'
|
|
||||||
|
|
||||||
import api from '~utils/api'
|
import api from '~utils/api'
|
||||||
import { accountState } from '~utils/accountState'
|
|
||||||
import { appState, initialAppState } from '~utils/appState'
|
import { appState, initialAppState } from '~utils/appState'
|
||||||
import { formatTimeAgo } from '~utils/timeAgo'
|
import { formatTimeAgo } from '~utils/timeAgo'
|
||||||
import { youtube } from '~utils/youtube'
|
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 EllipsisIcon from '~public/icons/Ellipsis.svg'
|
||||||
import EditIcon from '~public/icons/Edit.svg'
|
import EditIcon from '~public/icons/Edit.svg'
|
||||||
import RemixIcon from '~public/icons/Remix.svg'
|
import RemixIcon from '~public/icons/Remix.svg'
|
||||||
|
|
||||||
|
|
@ -44,38 +36,21 @@ interface Props {
|
||||||
new: boolean
|
new: boolean
|
||||||
editable: boolean
|
editable: boolean
|
||||||
updateCallback: (details: DetailsObject) => void
|
updateCallback: (details: DetailsObject) => void
|
||||||
deleteCallback: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PartyDetails = (props: Props) => {
|
const PartyDetails = (props: Props) => {
|
||||||
const { party, raids } = useSnapshot(appState)
|
|
||||||
|
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const locale = router.locale || 'en'
|
|
||||||
|
|
||||||
const youtubeUrlRegex =
|
const youtubeUrlRegex =
|
||||||
/(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g
|
/(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g
|
||||||
|
|
||||||
const nameInput = React.createRef<HTMLInputElement>()
|
|
||||||
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 [alertOpen, setAlertOpen] = useState(false)
|
const [alertOpen, setAlertOpen] = useState(false)
|
||||||
|
|
||||||
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 [remixes, setRemixes] = useState<Party[]>([])
|
const [remixes, setRemixes] = useState<Party[]>([])
|
||||||
|
|
||||||
const [raidSlug, setRaidSlug] = useState('')
|
|
||||||
const [embeddedDescription, setEmbeddedDescription] =
|
const [embeddedDescription, setEmbeddedDescription] =
|
||||||
useState<React.ReactNode>()
|
useState<React.ReactNode>()
|
||||||
|
|
||||||
|
|
@ -91,59 +66,11 @@ const PartyDetails = (props: Props) => {
|
||||||
Visible: open,
|
Visible: open,
|
||||||
})
|
})
|
||||||
|
|
||||||
const userClass = classNames({
|
|
||||||
user: true,
|
|
||||||
empty: !party.user,
|
|
||||||
})
|
|
||||||
|
|
||||||
const linkClass = classNames({
|
|
||||||
wind: party && party.element == 1,
|
|
||||||
fire: party && party.element == 2,
|
|
||||||
water: party && party.element == 3,
|
|
||||||
earth: party && party.element == 4,
|
|
||||||
dark: party && party.element == 5,
|
|
||||||
light: party && party.element == 6,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.party) {
|
|
||||||
setName(props.party.name)
|
|
||||||
setAutoGuard(props.party.auto_guard)
|
|
||||||
setFullAuto(props.party.full_auto)
|
|
||||||
setChargeAttack(props.party.charge_attack)
|
|
||||||
setClearTime(props.party.clear_time)
|
|
||||||
setRemixes(props.party.remixes)
|
|
||||||
if (props.party.turn_count) setTurnCount(props.party.turn_count)
|
|
||||||
if (props.party.button_count) setButtonCount(props.party.button_count)
|
|
||||||
if (props.party.chain_count) setChainCount(props.party.chain_count)
|
|
||||||
}
|
|
||||||
}, [props.party])
|
|
||||||
|
|
||||||
// Subscribe to router changes and reset state
|
|
||||||
// if the new route is a new team
|
|
||||||
useEffect(() => {
|
|
||||||
router.events.on('routeChangeStart', (url, { shallow }) => {
|
|
||||||
if (url === '/new' || url === '/') {
|
|
||||||
const party = initialAppState.party
|
|
||||||
|
|
||||||
setName(party.name ? party.name : '')
|
|
||||||
setAutoGuard(party.autoGuard)
|
|
||||||
setFullAuto(party.fullAuto)
|
|
||||||
setChargeAttack(party.chargeAttack)
|
|
||||||
setClearTime(party.clearTime)
|
|
||||||
setRemixes(party.remixes)
|
|
||||||
setTurnCount(party.turnCount)
|
|
||||||
setButtonCount(party.buttonCount)
|
|
||||||
setChainCount(party.chainCount)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Extract the video IDs from the description
|
// Extract the video IDs from the description
|
||||||
if (appState.party.description) {
|
if (appState.party.description) {
|
||||||
|
|
@ -177,16 +104,6 @@ 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>) {
|
function handleTextAreaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
|
@ -196,81 +113,6 @@ const PartyDetails = (props: Props) => {
|
||||||
setErrors(newErrors)
|
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 })
|
||||||
|
|
@ -289,30 +131,9 @@ const PartyDetails = (props: Props) => {
|
||||||
setOpen(!open)
|
setOpen(!open)
|
||||||
}
|
}
|
||||||
|
|
||||||
function receiveRaid(slug?: string) {
|
|
||||||
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 descriptionValue = descriptionInput.current?.value
|
|
||||||
const raid = raids.find((raid) => raid.slug === raidSlug)
|
|
||||||
|
|
||||||
const details: DetailsObject = {
|
const details: DetailsObject = {
|
||||||
fullAuto: fullAuto,
|
description: descriptionInput.current?.value,
|
||||||
autoGuard: autoGuard,
|
|
||||||
chargeAttack: chargeAttack,
|
|
||||||
clearTime: clearTime,
|
|
||||||
buttonCount: buttonCount,
|
|
||||||
turnCount: turnCount,
|
|
||||||
chainCount: chainCount,
|
|
||||||
name: name,
|
|
||||||
description: descriptionValue,
|
|
||||||
raid: raid,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
props.updateCallback(details)
|
props.updateCallback(details)
|
||||||
|
|
@ -323,15 +144,33 @@ const PartyDetails = (props: Props) => {
|
||||||
setAlertOpen(!alertOpen)
|
setAlertOpen(!alertOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteParty() {
|
|
||||||
props.deleteCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods: Navigation
|
// Methods: Navigation
|
||||||
function goTo(shortcode?: string) {
|
function goTo(shortcode?: string) {
|
||||||
if (shortcode) router.push(`/p/${shortcode}`)
|
if (shortcode) router.push(`/p/${shortcode}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractYoutubeVideoIds(text: string) {
|
||||||
|
// Initialize an array to store the video IDs
|
||||||
|
const videoIds = []
|
||||||
|
|
||||||
|
// Use the regular expression to find all the Youtube URLs in the text
|
||||||
|
let match
|
||||||
|
while ((match = youtubeUrlRegex.exec(text)) !== null) {
|
||||||
|
// Extract the video ID from the URL
|
||||||
|
const videoId = match[1]
|
||||||
|
|
||||||
|
// Add the video ID to the array, along with the character position of the URL
|
||||||
|
videoIds.push({
|
||||||
|
id: videoId,
|
||||||
|
url: match[0],
|
||||||
|
position: match.index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the array of video IDs
|
||||||
|
return videoIds
|
||||||
|
}
|
||||||
|
|
||||||
// Methods: Favorites
|
// Methods: Favorites
|
||||||
function toggleFavorite(teamId: string, favorited: boolean) {
|
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||||
if (favorited) unsaveFavorite(teamId)
|
if (favorited) unsaveFavorite(teamId)
|
||||||
|
|
@ -370,103 +209,6 @@ const PartyDetails = (props: Props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractYoutubeVideoIds(text: string) {
|
|
||||||
// Initialize an array to store the video IDs
|
|
||||||
const videoIds = []
|
|
||||||
|
|
||||||
// Use the regular expression to find all the Youtube URLs in the text
|
|
||||||
let match
|
|
||||||
while ((match = youtubeUrlRegex.exec(text)) !== null) {
|
|
||||||
// Extract the video ID from the URL
|
|
||||||
const videoId = match[1]
|
|
||||||
|
|
||||||
// Add the video ID to the array, along with the character position of the URL
|
|
||||||
videoIds.push({
|
|
||||||
id: videoId,
|
|
||||||
url: match[0],
|
|
||||||
position: match.index,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the array of video IDs
|
|
||||||
return videoIds
|
|
||||||
}
|
|
||||||
|
|
||||||
const userImage = (picture?: string, element?: string) => {
|
|
||||||
if (picture && element)
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
alt={picture}
|
|
||||||
className={`profile ${element}`}
|
|
||||||
srcSet={`/profile/${picture}.png,
|
|
||||||
/profile/${picture}@2x.png 2x`}
|
|
||||||
src={`/profile/${picture}.png`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
else
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
alt={t('no_user')}
|
|
||||||
className={`profile anonymous`}
|
|
||||||
srcSet={`/profile/npc.png,
|
|
||||||
/profile/npc@2x.png 2x`}
|
|
||||||
src={`/profile/npc.png`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const userBlock = (username?: string, picture?: string, element?: string) => {
|
|
||||||
return (
|
|
||||||
<div className={userClass}>
|
|
||||||
{userImage(picture, element)}
|
|
||||||
{username ? username : t('no_user')}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderUserBlock = () => {
|
|
||||||
let username, picture, element
|
|
||||||
if (accountState.account.authorized && props.new) {
|
|
||||||
username = accountState.account.user?.username
|
|
||||||
picture = accountState.account.user?.avatar.picture
|
|
||||||
element = accountState.account.user?.avatar.element
|
|
||||||
} else if (party.user && !props.new) {
|
|
||||||
username = party.user.username
|
|
||||||
picture = party.user.avatar.picture
|
|
||||||
element = party.user.avatar.element
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username && picture && element) {
|
|
||||||
return linkedUserBlock(username, picture, element)
|
|
||||||
} else if (!props.new) {
|
|
||||||
return userBlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkedUserBlock = (
|
|
||||||
username?: string,
|
|
||||||
picture?: string,
|
|
||||||
element?: string
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Link href={`/${username}`} passHref>
|
|
||||||
<a className={linkClass}>{userBlock(username, picture, element)}</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkedRaidBlock = (raid: Raid) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Link href={`/teams?raid=${raid.slug}`} passHref>
|
|
||||||
<a className={`Raid ${linkClass}`}>{raid.name[locale]}</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRemixes() {
|
function renderRemixes() {
|
||||||
return remixes.map((party, i) => {
|
return remixes.map((party, i) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -490,142 +232,9 @@ const PartyDetails = (props: Props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAlert = () => {
|
|
||||||
if (party.editable) {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
open={alertOpen}
|
|
||||||
primaryAction={deleteParty}
|
|
||||||
primaryActionText={t('modals.delete_team.buttons.confirm')}
|
|
||||||
cancelAction={() => setAlertOpen(false)}
|
|
||||||
cancelActionText={t('modals.delete_team.buttons.cancel')}
|
|
||||||
message={t('modals.delete_team.description')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editable = () => {
|
const editable = () => {
|
||||||
return (
|
return (
|
||||||
<section className={editableClasses}>
|
<section className={editableClasses}>
|
||||||
<CharLimitedFieldset
|
|
||||||
fieldName="name"
|
|
||||||
placeholder="Name your team"
|
|
||||||
value={props.party?.name}
|
|
||||||
limit={50}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
error={errors.name}
|
|
||||||
ref={nameInput}
|
|
||||||
/>
|
|
||||||
<RaidDropdown
|
|
||||||
showAllRaidsOption={false}
|
|
||||||
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
|
|
||||||
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={
|
||||||
|
|
@ -663,91 +272,9 @@ const PartyDetails = (props: Props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = () => {
|
||||||
return (
|
return (
|
||||||
<section className={readOnlyClasses}>
|
<section className={readOnlyClasses}>
|
||||||
<section className="Details">
|
|
||||||
<Token
|
|
||||||
className={classNames({
|
|
||||||
ChargeAttack: true,
|
|
||||||
On: chargeAttack,
|
|
||||||
Off: !chargeAttack,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{`${t('party.details.labels.charge_attack')} ${
|
|
||||||
chargeAttack ? 'On' : 'Off'
|
|
||||||
}`}
|
|
||||||
</Token>
|
|
||||||
|
|
||||||
<Token
|
|
||||||
className={classNames({
|
|
||||||
FullAuto: true,
|
|
||||||
On: fullAuto,
|
|
||||||
Off: !fullAuto,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{`${t('party.details.labels.full_auto')} ${
|
|
||||||
fullAuto ? 'On' : 'Off'
|
|
||||||
}`}
|
|
||||||
</Token>
|
|
||||||
|
|
||||||
<Token
|
|
||||||
className={classNames({
|
|
||||||
AutoGuard: true,
|
|
||||||
On: autoGuard,
|
|
||||||
Off: !autoGuard,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{`${t('party.details.labels.auto_guard')} ${
|
|
||||||
autoGuard ? 'On' : 'Off'
|
|
||||||
}`}
|
|
||||||
</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>
|
||||||
)
|
)
|
||||||
|
|
@ -765,56 +292,8 @@ const PartyDetails = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="DetailsWrapper">
|
<section className="DetailsWrapper">
|
||||||
<div className="PartyInfo">
|
|
||||||
<div className="Left">
|
|
||||||
<div className="Header">
|
|
||||||
<h1 className={name ? '' : 'empty'}>
|
|
||||||
{name ? name : t('no_title')}
|
|
||||||
</h1>
|
|
||||||
{party.remix && party.sourceParty ? (
|
|
||||||
<Tooltip content={t('tooltips.source')}>
|
|
||||||
<Button
|
|
||||||
className="IconButton Blended"
|
|
||||||
leftAccessoryIcon={<RemixIcon />}
|
|
||||||
text={t('tokens.remix')}
|
|
||||||
onClick={() => goTo(party.sourceParty?.shortcode)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="attribution">
|
|
||||||
{renderUserBlock()}
|
|
||||||
{party.raid ? linkedRaidBlock(party.raid) : ''}
|
|
||||||
{party.created_at != '' ? (
|
|
||||||
<time
|
|
||||||
className="last-updated"
|
|
||||||
dateTime={new Date(party.created_at).toString()}
|
|
||||||
>
|
|
||||||
{formatTimeAgo(new Date(party.created_at), locale)}
|
|
||||||
</time>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{party.editable ? (
|
|
||||||
<div className="Right">
|
|
||||||
<Button
|
|
||||||
leftAccessoryIcon={<EditIcon />}
|
|
||||||
text={t('buttons.show_info')}
|
|
||||||
onClick={toggleDetails}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{readOnly()}
|
{readOnly()}
|
||||||
{editable()}
|
{editable()}
|
||||||
|
|
||||||
{deleteAlert()}
|
|
||||||
</section>
|
</section>
|
||||||
{remixes && remixes.length > 0 ? remixSection() : ''}
|
{remixes && remixes.length > 0 ? remixSection() : ''}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
0
components/party/PartyDropdown/index.scss
Normal file
0
components/party/PartyDropdown/index.scss
Normal file
197
components/party/PartyDropdown/index.tsx
Normal file
197
components/party/PartyDropdown/index.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
// Libraries
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { subscribe, useSnapshot } from 'valtio'
|
||||||
|
import { Trans, useTranslation } from 'next-i18next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
// Dependencies: Common
|
||||||
|
import Button from '~components/common/Button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
} from '~components/common/DropdownMenuContent'
|
||||||
|
|
||||||
|
// Dependencies: Toasts
|
||||||
|
import RemixedToast from '~components/toasts/RemixedToast'
|
||||||
|
import UrlCopiedToast from '~components/toasts/UrlCopiedToast'
|
||||||
|
|
||||||
|
// Dependencies: Alerts
|
||||||
|
import DeleteTeamAlert from '~components/dialogs/DeleteTeamAlert'
|
||||||
|
import RemixTeamAlert from '~components/dialogs/RemixTeamAlert'
|
||||||
|
|
||||||
|
// Dependencies: Utils
|
||||||
|
import api from '~utils/api'
|
||||||
|
import { accountState } from '~utils/accountState'
|
||||||
|
import { appState } from '~utils/appState'
|
||||||
|
import { getLocalId } from '~utils/localId'
|
||||||
|
import { retrieveLocaleCookies } from '~utils/retrieveCookies'
|
||||||
|
import { setEditKey, storeEditKey } from '~utils/userToken'
|
||||||
|
|
||||||
|
// Dependencies: Icons
|
||||||
|
import EllipsisIcon from '~public/icons/Ellipsis.svg'
|
||||||
|
|
||||||
|
// Dependencies: Props
|
||||||
|
interface Props {
|
||||||
|
editable: boolean
|
||||||
|
deleteTeamCallback: () => void
|
||||||
|
remixTeamCallback: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PartyDropdown = ({
|
||||||
|
editable,
|
||||||
|
deleteTeamCallback,
|
||||||
|
remixTeamCallback,
|
||||||
|
}: Props) => {
|
||||||
|
// Localization
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// Router
|
||||||
|
const router = useRouter()
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
const localeData = retrieveLocaleCookies()
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const [deleteAlertOpen, setDeleteAlertOpen] = useState(false)
|
||||||
|
const [remixAlertOpen, setRemixAlertOpen] = useState(false)
|
||||||
|
|
||||||
|
const [copyToastOpen, setCopyToastOpen] = useState(false)
|
||||||
|
const [remixToastOpen, setRemixToastOpen] = useState(false)
|
||||||
|
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [originalName, setOriginalName] = useState('')
|
||||||
|
|
||||||
|
// Snapshots
|
||||||
|
const { account } = useSnapshot(accountState)
|
||||||
|
const { party: partySnapshot } = useSnapshot(appState)
|
||||||
|
|
||||||
|
// Subscribe to app state to listen for party name and
|
||||||
|
// unsubscribe when component is unmounted
|
||||||
|
const unsubscribe = subscribe(appState, () => {
|
||||||
|
const newName =
|
||||||
|
appState.party && appState.party.name ? appState.party.name : ''
|
||||||
|
setName(newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => () => unsubscribe(), [])
|
||||||
|
|
||||||
|
// Methods: Event handlers (Buttons)
|
||||||
|
function handleButtonClicked() {
|
||||||
|
setOpen(!open)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods: Event handlers (Menus)
|
||||||
|
function handleOpenChange(open: boolean) {
|
||||||
|
setOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method: Actions
|
||||||
|
function copyToClipboard() {
|
||||||
|
if (router.asPath.split('/')[1] === 'p') {
|
||||||
|
navigator.clipboard.writeText(window.location.href)
|
||||||
|
setCopyToastOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods: Event handlers
|
||||||
|
|
||||||
|
// Alerts / Delete team
|
||||||
|
function openDeleteTeamAlert() {
|
||||||
|
setDeleteAlertOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteTeamAlertChange(open: boolean) {
|
||||||
|
setDeleteAlertOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alerts / Remix team
|
||||||
|
function openRemixTeamAlert() {
|
||||||
|
setRemixAlertOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemixTeamAlertChange(open: boolean) {
|
||||||
|
setRemixAlertOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toasts / Copy URL
|
||||||
|
function handleCopyToastOpenChanged(open: boolean) {
|
||||||
|
setCopyToastOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopyToastCloseClicked() {
|
||||||
|
setCopyToastOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toasts / Remix team
|
||||||
|
function handleRemixToastOpenChanged(open: boolean) {
|
||||||
|
setRemixToastOpen(open)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemixToastCloseClicked() {
|
||||||
|
setRemixToastOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableItems = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenuGroup className="MenuGroup">
|
||||||
|
<DropdownMenuItem className="MenuItem" onClick={copyToClipboard}>
|
||||||
|
<span>Copy link to team</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="MenuItem" onClick={openRemixTeamAlert}>
|
||||||
|
<span>Remix team</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuGroup className="MenuGroup">
|
||||||
|
<DropdownMenuItem className="MenuItem" onClick={openDeleteTeamAlert}>
|
||||||
|
<span className="destructive">Delete team</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="DropdownWrapper">
|
||||||
|
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
leftAccessoryIcon={<EllipsisIcon />}
|
||||||
|
className={classNames({ Active: open })}
|
||||||
|
blended={true}
|
||||||
|
onClick={handleButtonClicked}
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>{editableItems()}</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteTeamAlert
|
||||||
|
open={deleteAlertOpen}
|
||||||
|
onOpenChange={handleDeleteTeamAlertChange}
|
||||||
|
deleteCallback={deleteTeamCallback}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemixTeamAlert
|
||||||
|
creator={editable}
|
||||||
|
name={partySnapshot.name ? partySnapshot.name : t('no_title')}
|
||||||
|
open={remixAlertOpen}
|
||||||
|
onOpenChange={handleRemixTeamAlertChange}
|
||||||
|
remixCallback={remixTeamCallback}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PartyDropdown
|
||||||
394
components/party/PartyHeader/index.scss
Normal file
394
components/party/PartyHeader/index.scss
Normal file
|
|
@ -0,0 +1,394 @@
|
||||||
|
.DetailsWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-2x;
|
||||||
|
margin: $unit-4x auto 0 auto;
|
||||||
|
max-width: $grid-width;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
.Button:not(.IconButton) {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.Text {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.PartyDetails {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
margin: 0 auto $unit-2x;
|
||||||
|
max-width: $unit * 94;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
padding: 0 $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Visible {
|
||||||
|
// margin-bottom: $unit-12x;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Editable {
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
&.Visible {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: $unit * 22;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectTrigger {
|
||||||
|
padding: $unit-2x;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DetailToggleGroup {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToggleSection,
|
||||||
|
.InputSection {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: $input-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 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
.Button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ReadOnly {
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
&.Visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: $font-regular;
|
||||||
|
line-height: $font-regular * 1.2;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $unit;
|
||||||
|
margin-bottom: $unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.YoutubeWrapper {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: $card-corner;
|
||||||
|
margin: $unit 0;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
contain: content;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 60%;
|
||||||
|
height: 60%;
|
||||||
|
|
||||||
|
@include breakpoint(tablet) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* gradient */
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
|
||||||
|
background-position: top;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
height: 60px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* responsive iframe with a 16:9 aspect ratio
|
||||||
|
thanks https://css-tricks.com/responsive-iframes/
|
||||||
|
*/
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
padding-bottom: calc(100% / (16 / 9));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover > .PlayerButton {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Play button */
|
||||||
|
& > .PlayerButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
background-image: url('/icons/youtube.svg');
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .PlayerButton,
|
||||||
|
& > .PlayerButton:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post-click styles */
|
||||||
|
&.lyt-activated {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
&.lyt-activated::before,
|
||||||
|
&.lyt-activated > .PlayerButton {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.PartyInfo {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: $unit * 94;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@include breakpoint(phone) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
padding: 0 $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .Right {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-half;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .Left {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: $unit;
|
||||||
|
margin-bottom: $unit;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: $font-xlarge;
|
||||||
|
font-weight: $normal;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribution {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: $font-small;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
font-size: $font-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited:not(.fire):not(.water):not(.wind):not(.earth):not(.dark):not(
|
||||||
|
.light
|
||||||
|
) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover:not(.fire):not(.water):not(.wind):not(.earth):not(.dark):not(
|
||||||
|
.light
|
||||||
|
) {
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > *:not(:last-child):after {
|
||||||
|
content: ' · ';
|
||||||
|
margin: 0 calc($unit / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: calc($unit / 2);
|
||||||
|
margin-top: 1px;
|
||||||
|
|
||||||
|
img,
|
||||||
|
.no-user {
|
||||||
|
$diameter: 24px;
|
||||||
|
|
||||||
|
border-radius: calc($diameter / 2);
|
||||||
|
height: $diameter;
|
||||||
|
width: $diameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.gran {
|
||||||
|
background-color: #cee7fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.djeeta {
|
||||||
|
background-color: #ffe1fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-user {
|
||||||
|
background: $grey-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
831
components/party/PartyHeader/index.tsx
Normal file
831
components/party/PartyHeader/index.tsx
Normal file
|
|
@ -0,0 +1,831 @@
|
||||||
|
import React, { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { subscribe, useSnapshot } from 'valtio'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
import Linkify from 'react-linkify'
|
||||||
|
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import reactStringReplace from 'react-string-replace'
|
||||||
|
|
||||||
|
import Alert from '~components/common/Alert'
|
||||||
|
import Button from '~components/common/Button'
|
||||||
|
import CharLimitedFieldset from '~components/common/CharLimitedFieldset'
|
||||||
|
import DurationInput from '~components/common/DurationInput'
|
||||||
|
import GridRepCollection from '~components/GridRepCollection'
|
||||||
|
import GridRep from '~components/GridRep'
|
||||||
|
import Input from '~components/common/Input'
|
||||||
|
import RaidDropdown from '~components/RaidDropdown'
|
||||||
|
import Switch from '~components/common/Switch'
|
||||||
|
import Tooltip from '~components/common/Tooltip'
|
||||||
|
import TextFieldset from '~components/common/TextFieldset'
|
||||||
|
import Token from '~components/common/Token'
|
||||||
|
|
||||||
|
import PartyDropdown from '~components/party/PartyDropdown'
|
||||||
|
|
||||||
|
import api from '~utils/api'
|
||||||
|
import { accountState } from '~utils/accountState'
|
||||||
|
import { appState, initialAppState } from '~utils/appState'
|
||||||
|
import { formatTimeAgo } from '~utils/timeAgo'
|
||||||
|
import { youtube } from '~utils/youtube'
|
||||||
|
|
||||||
|
import CheckIcon from '~public/icons/Check.svg'
|
||||||
|
import CrossIcon from '~public/icons/Cross.svg'
|
||||||
|
import EditIcon from '~public/icons/Edit.svg'
|
||||||
|
import EllipsisIcon from '~public/icons/Ellipsis.svg'
|
||||||
|
import RemixIcon from '~public/icons/Remix.svg'
|
||||||
|
|
||||||
|
import type { DetailsObject } from 'types'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
interface Props {
|
||||||
|
party?: Party
|
||||||
|
new: boolean
|
||||||
|
editable: boolean
|
||||||
|
deleteCallback: () => void
|
||||||
|
remixCallback: () => void
|
||||||
|
updateCallback: (details: DetailsObject) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PartyHeader = (props: Props) => {
|
||||||
|
const { party, raids } = useSnapshot(appState)
|
||||||
|
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
const router = useRouter()
|
||||||
|
const locale = router.locale || 'en'
|
||||||
|
|
||||||
|
const youtubeUrlRegex =
|
||||||
|
/(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g
|
||||||
|
|
||||||
|
const nameInput = React.createRef<HTMLInputElement>()
|
||||||
|
const descriptionInput = React.createRef<HTMLTextAreaElement>()
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [alertOpen, setAlertOpen] = useState(false)
|
||||||
|
|
||||||
|
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 [remixes, setRemixes] = useState<Party[]>([])
|
||||||
|
|
||||||
|
const [raidSlug, setRaidSlug] = useState('')
|
||||||
|
const [embeddedDescription, setEmbeddedDescription] =
|
||||||
|
useState<React.ReactNode>()
|
||||||
|
|
||||||
|
const readOnlyClasses = classNames({
|
||||||
|
PartyDetails: true,
|
||||||
|
ReadOnly: true,
|
||||||
|
Visible: !open,
|
||||||
|
})
|
||||||
|
|
||||||
|
const editableClasses = classNames({
|
||||||
|
PartyDetails: true,
|
||||||
|
Editable: true,
|
||||||
|
Visible: open,
|
||||||
|
})
|
||||||
|
|
||||||
|
const userClass = classNames({
|
||||||
|
user: true,
|
||||||
|
empty: !party.user,
|
||||||
|
})
|
||||||
|
|
||||||
|
const linkClass = classNames({
|
||||||
|
wind: party && party.element == 1,
|
||||||
|
fire: party && party.element == 2,
|
||||||
|
water: party && party.element == 3,
|
||||||
|
earth: party && party.element == 4,
|
||||||
|
dark: party && party.element == 5,
|
||||||
|
light: party && party.element == 6,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.party) {
|
||||||
|
setName(props.party.name)
|
||||||
|
setAutoGuard(props.party.auto_guard)
|
||||||
|
setFullAuto(props.party.full_auto)
|
||||||
|
setChargeAttack(props.party.charge_attack)
|
||||||
|
setClearTime(props.party.clear_time)
|
||||||
|
setRemixes(props.party.remixes)
|
||||||
|
if (props.party.turn_count) setTurnCount(props.party.turn_count)
|
||||||
|
if (props.party.button_count) setButtonCount(props.party.button_count)
|
||||||
|
if (props.party.chain_count) setChainCount(props.party.chain_count)
|
||||||
|
}
|
||||||
|
}, [props.party])
|
||||||
|
|
||||||
|
// Subscribe to router changes and reset state
|
||||||
|
// if the new route is a new team
|
||||||
|
useEffect(() => {
|
||||||
|
router.events.on('routeChangeStart', (url, { shallow }) => {
|
||||||
|
if (url === '/new' || url === '/') {
|
||||||
|
const party = initialAppState.party
|
||||||
|
|
||||||
|
setName(party.name ? party.name : '')
|
||||||
|
setAutoGuard(party.autoGuard)
|
||||||
|
setFullAuto(party.fullAuto)
|
||||||
|
setChargeAttack(party.chargeAttack)
|
||||||
|
setClearTime(party.clearTime)
|
||||||
|
setRemixes(party.remixes)
|
||||||
|
setTurnCount(party.turnCount)
|
||||||
|
setButtonCount(party.buttonCount)
|
||||||
|
setChainCount(party.chainCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Extract the video IDs from the description
|
||||||
|
if (appState.party.description) {
|
||||||
|
const videoIds = extractYoutubeVideoIds(appState.party.description)
|
||||||
|
|
||||||
|
// Fetch the video titles for each ID
|
||||||
|
const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id))
|
||||||
|
|
||||||
|
// Wait for all the video titles to be fetched
|
||||||
|
Promise.all(fetchPromises).then((videoTitles) => {
|
||||||
|
// Replace the video URLs in the description with LiteYoutubeEmbed elements
|
||||||
|
const newDescription = reactStringReplace(
|
||||||
|
appState.party.description,
|
||||||
|
youtubeUrlRegex,
|
||||||
|
(match, i) => (
|
||||||
|
<LiteYouTubeEmbed
|
||||||
|
key={`${match}-${i}`}
|
||||||
|
id={match}
|
||||||
|
title={videoTitles[i]}
|
||||||
|
wrapperClass="YoutubeWrapper"
|
||||||
|
playerClass="PlayerButton"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the state with the new description
|
||||||
|
setEmbeddedDescription(newDescription)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setEmbeddedDescription('')
|
||||||
|
}
|
||||||
|
}, [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) {
|
||||||
|
return await youtube
|
||||||
|
.getVideoById(videoId, { maxResults: 1 })
|
||||||
|
.then((data) => data.items[0].snippet.localized.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDetails() {
|
||||||
|
// Enabling this code will make live updates not work,
|
||||||
|
// but I'm not sure why it's here, so we're not going to remove it.
|
||||||
|
|
||||||
|
// if (name !== party.name) {
|
||||||
|
// const resetName = party.name ? party.name : ''
|
||||||
|
// setName(resetName)
|
||||||
|
// if (nameInput.current) nameInput.current.value = resetName
|
||||||
|
// }
|
||||||
|
setOpen(!open)
|
||||||
|
}
|
||||||
|
|
||||||
|
function receiveRaid(slug?: string) {
|
||||||
|
if (slug) setRaidSlug(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchValue(value: boolean) {
|
||||||
|
if (value) return 'on'
|
||||||
|
else return 'off'
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDetails(event: React.MouseEvent) {
|
||||||
|
const descriptionValue = descriptionInput.current?.value
|
||||||
|
const raid = raids.find((raid) => raid.slug === raidSlug)
|
||||||
|
|
||||||
|
const details: DetailsObject = {
|
||||||
|
fullAuto: fullAuto,
|
||||||
|
autoGuard: autoGuard,
|
||||||
|
chargeAttack: chargeAttack,
|
||||||
|
clearTime: clearTime,
|
||||||
|
buttonCount: buttonCount,
|
||||||
|
turnCount: turnCount,
|
||||||
|
chainCount: chainCount,
|
||||||
|
name: name,
|
||||||
|
description: descriptionValue,
|
||||||
|
raid: raid,
|
||||||
|
}
|
||||||
|
|
||||||
|
props.updateCallback(details)
|
||||||
|
toggleDetails()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
setAlertOpen(!alertOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteParty() {
|
||||||
|
props.deleteCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods: Navigation
|
||||||
|
function goTo(shortcode?: string) {
|
||||||
|
if (shortcode) router.push(`/p/${shortcode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods: Favorites
|
||||||
|
function toggleFavorite(teamId: string, favorited: boolean) {
|
||||||
|
if (favorited) unsaveFavorite(teamId)
|
||||||
|
else saveFavorite(teamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFavorite(teamId: string) {
|
||||||
|
api.saveTeam({ id: teamId }).then((response) => {
|
||||||
|
if (response.status == 201) {
|
||||||
|
const index = remixes.findIndex((p) => p.id === teamId)
|
||||||
|
const party = remixes[index]
|
||||||
|
|
||||||
|
party.favorited = true
|
||||||
|
|
||||||
|
let clonedParties = clonedeep(remixes)
|
||||||
|
clonedParties[index] = party
|
||||||
|
|
||||||
|
setRemixes(clonedParties)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsaveFavorite(teamId: string) {
|
||||||
|
api.unsaveTeam({ id: teamId }).then((response) => {
|
||||||
|
if (response.status == 200) {
|
||||||
|
const index = remixes.findIndex((p) => p.id === teamId)
|
||||||
|
const party = remixes[index]
|
||||||
|
|
||||||
|
party.favorited = false
|
||||||
|
|
||||||
|
let clonedParties = clonedeep(remixes)
|
||||||
|
clonedParties[index] = party
|
||||||
|
|
||||||
|
setRemixes(clonedParties)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractYoutubeVideoIds(text: string) {
|
||||||
|
// Initialize an array to store the video IDs
|
||||||
|
const videoIds = []
|
||||||
|
|
||||||
|
// Use the regular expression to find all the Youtube URLs in the text
|
||||||
|
let match
|
||||||
|
while ((match = youtubeUrlRegex.exec(text)) !== null) {
|
||||||
|
// Extract the video ID from the URL
|
||||||
|
const videoId = match[1]
|
||||||
|
|
||||||
|
// Add the video ID to the array, along with the character position of the URL
|
||||||
|
videoIds.push({
|
||||||
|
id: videoId,
|
||||||
|
url: match[0],
|
||||||
|
position: match.index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the array of video IDs
|
||||||
|
return videoIds
|
||||||
|
}
|
||||||
|
|
||||||
|
const userImage = (picture?: string, element?: string) => {
|
||||||
|
if (picture && element)
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={picture}
|
||||||
|
className={`profile ${element}`}
|
||||||
|
srcSet={`/profile/${picture}.png,
|
||||||
|
/profile/${picture}@2x.png 2x`}
|
||||||
|
src={`/profile/${picture}.png`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
alt={t('no_user')}
|
||||||
|
className={`profile anonymous`}
|
||||||
|
srcSet={`/profile/npc.png,
|
||||||
|
/profile/npc@2x.png 2x`}
|
||||||
|
src={`/profile/npc.png`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBlock = (username?: string, picture?: string, element?: string) => {
|
||||||
|
return (
|
||||||
|
<div className={userClass}>
|
||||||
|
{userImage(picture, element)}
|
||||||
|
{username ? username : t('no_user')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderUserBlock = () => {
|
||||||
|
let username, picture, element
|
||||||
|
if (accountState.account.authorized && props.new) {
|
||||||
|
username = accountState.account.user?.username
|
||||||
|
picture = accountState.account.user?.avatar.picture
|
||||||
|
element = accountState.account.user?.avatar.element
|
||||||
|
} else if (party.user && !props.new) {
|
||||||
|
username = party.user.username
|
||||||
|
picture = party.user.avatar.picture
|
||||||
|
element = party.user.avatar.element
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username && picture && element) {
|
||||||
|
return linkedUserBlock(username, picture, element)
|
||||||
|
} else if (!props.new) {
|
||||||
|
return userBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedUserBlock = (
|
||||||
|
username?: string,
|
||||||
|
picture?: string,
|
||||||
|
element?: string
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link href={`/${username}`} passHref>
|
||||||
|
<a className={linkClass}>{userBlock(username, picture, element)}</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedRaidBlock = (raid: Raid) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link href={`/teams?raid=${raid.slug}`} passHref>
|
||||||
|
<a className={`Raid ${linkClass}`}>{raid.name[locale]}</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRemixes() {
|
||||||
|
return remixes.map((party, i) => {
|
||||||
|
return (
|
||||||
|
<GridRep
|
||||||
|
id={party.id}
|
||||||
|
shortcode={party.shortcode}
|
||||||
|
name={party.name}
|
||||||
|
createdAt={new Date(party.created_at)}
|
||||||
|
raid={party.raid}
|
||||||
|
grid={party.weapons}
|
||||||
|
user={party.user}
|
||||||
|
favorited={party.favorited}
|
||||||
|
fullAuto={party.full_auto}
|
||||||
|
autoGuard={party.auto_guard}
|
||||||
|
key={`party-${i}`}
|
||||||
|
displayUser={true}
|
||||||
|
onClick={goTo}
|
||||||
|
onSave={toggleFavorite}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAlert = () => {
|
||||||
|
if (party.editable) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
open={alertOpen}
|
||||||
|
primaryAction={deleteParty}
|
||||||
|
primaryActionText={t('modals.delete_team.buttons.confirm')}
|
||||||
|
cancelAction={() => setAlertOpen(false)}
|
||||||
|
cancelActionText={t('modals.delete_team.buttons.cancel')}
|
||||||
|
message={t('modals.delete_team.description')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editable = () => {
|
||||||
|
return (
|
||||||
|
<section className={editableClasses}>
|
||||||
|
<CharLimitedFieldset
|
||||||
|
fieldName="name"
|
||||||
|
placeholder="Name your team"
|
||||||
|
value={props.party?.name}
|
||||||
|
limit={50}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
error={errors.name}
|
||||||
|
ref={nameInput}
|
||||||
|
/>
|
||||||
|
<RaidDropdown
|
||||||
|
showAllRaidsOption={false}
|
||||||
|
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
|
||||||
|
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
|
||||||
|
fieldName="name"
|
||||||
|
placeholder={
|
||||||
|
'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}
|
||||||
|
onChange={handleTextAreaChange}
|
||||||
|
error={errors.description}
|
||||||
|
ref={descriptionInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="bottom">
|
||||||
|
<div className="left">
|
||||||
|
{router.pathname !== '/new' ? (
|
||||||
|
<Button
|
||||||
|
leftAccessoryIcon={<CrossIcon />}
|
||||||
|
className="Blended medium destructive"
|
||||||
|
onClick={handleClick}
|
||||||
|
text={t('buttons.delete')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="right">
|
||||||
|
<Button text={t('buttons.cancel')} onClick={toggleDetails} />
|
||||||
|
<Button
|
||||||
|
leftAccessoryIcon={<CheckIcon className="Check" />}
|
||||||
|
text={t('buttons.save_info')}
|
||||||
|
onClick={updateDetails}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 = () => {
|
||||||
|
return (
|
||||||
|
<section className={readOnlyClasses}>
|
||||||
|
<section className="Details">
|
||||||
|
<Token
|
||||||
|
className={classNames({
|
||||||
|
ChargeAttack: true,
|
||||||
|
On: chargeAttack,
|
||||||
|
Off: !chargeAttack,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{`${t('party.details.labels.charge_attack')} ${
|
||||||
|
chargeAttack ? 'On' : 'Off'
|
||||||
|
}`}
|
||||||
|
</Token>
|
||||||
|
|
||||||
|
<Token
|
||||||
|
className={classNames({
|
||||||
|
FullAuto: true,
|
||||||
|
On: fullAuto,
|
||||||
|
Off: !fullAuto,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{`${t('party.details.labels.full_auto')} ${
|
||||||
|
fullAuto ? 'On' : 'Off'
|
||||||
|
}`}
|
||||||
|
</Token>
|
||||||
|
|
||||||
|
<Token
|
||||||
|
className={classNames({
|
||||||
|
AutoGuard: true,
|
||||||
|
On: autoGuard,
|
||||||
|
Off: !autoGuard,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{`${t('party.details.labels.auto_guard')} ${
|
||||||
|
autoGuard ? 'On' : 'Off'
|
||||||
|
}`}
|
||||||
|
</Token>
|
||||||
|
|
||||||
|
{turnCount ? (
|
||||||
|
<Token>
|
||||||
|
{t('party.details.turns.with_count', {
|
||||||
|
count: turnCount,
|
||||||
|
})}
|
||||||
|
</Token>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{clearTime > 0 ? <Token>{clearTimeString()}</Token> : ''}
|
||||||
|
{buttonChainToken()}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const remixSection = () => {
|
||||||
|
return (
|
||||||
|
<section className="Remixes">
|
||||||
|
<h3>{t('remixes')}</h3>
|
||||||
|
{<GridRepCollection>{renderRemixes()}</GridRepCollection>}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="DetailsWrapper">
|
||||||
|
<div className="PartyInfo">
|
||||||
|
<div className="Left">
|
||||||
|
<div className="Header">
|
||||||
|
<h1 className={name ? '' : 'empty'}>
|
||||||
|
{name ? name : t('no_title')}
|
||||||
|
</h1>
|
||||||
|
{party.remix && party.sourceParty ? (
|
||||||
|
<Tooltip content={t('tooltips.source')}>
|
||||||
|
<Button
|
||||||
|
className="IconButton Blended"
|
||||||
|
leftAccessoryIcon={<RemixIcon />}
|
||||||
|
text={t('tokens.remix')}
|
||||||
|
onClick={() => goTo(party.sourceParty?.shortcode)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="attribution">
|
||||||
|
{renderUserBlock()}
|
||||||
|
{party.raid ? linkedRaidBlock(party.raid) : ''}
|
||||||
|
{party.created_at != '' ? (
|
||||||
|
<time
|
||||||
|
className="last-updated"
|
||||||
|
dateTime={new Date(party.created_at).toString()}
|
||||||
|
>
|
||||||
|
{formatTimeAgo(new Date(party.created_at), locale)}
|
||||||
|
</time>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{party.editable ? (
|
||||||
|
<div className="Right">
|
||||||
|
<Button
|
||||||
|
leftAccessoryIcon={<EditIcon />}
|
||||||
|
text={t('buttons.show_info')}
|
||||||
|
onClick={toggleDetails}
|
||||||
|
/>
|
||||||
|
<PartyDropdown
|
||||||
|
editable={props.editable}
|
||||||
|
deleteTeamCallback={props.deleteCallback}
|
||||||
|
remixTeamCallback={props.remixCallback}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{readOnly()}
|
||||||
|
{editable()}
|
||||||
|
|
||||||
|
{deleteAlert()}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PartyHeader
|
||||||
Loading…
Reference in a new issue