From d80cf0ec473fd74a4fe051c7eb19f828f23175f9 Mon Sep 17 00:00:00 2001 From: Ewan Lyon Date: Wed, 23 Dec 2020 13:06:19 +1100 Subject: [PATCH] Update veto dashboard --- src/dashboard/atoms/fit-text.tsx | 48 +++++ src/dashboard/atoms/grabhandle.tsx | 31 +++ src/dashboard/atoms/styled-ui.tsx | 58 ++++++ src/dashboard/maps.html | 9 +- src/dashboard/setup/maps/maps.tsx | 155 --------------- src/dashboard/setup/maps/selected-map.tsx | 28 --- src/dashboard/setup/maps/veto.tsx | 220 ++++++++++++++++++++++ src/dashboard/setup/maps/vetosingle.tsx | 103 ++++++++++ src/extension/extraData.ts | 4 + src/types/map-data.d.ts | 3 +- 10 files changed, 473 insertions(+), 186 deletions(-) create mode 100644 src/dashboard/atoms/fit-text.tsx create mode 100644 src/dashboard/atoms/grabhandle.tsx create mode 100644 src/dashboard/atoms/styled-ui.tsx delete mode 100644 src/dashboard/setup/maps/maps.tsx delete mode 100644 src/dashboard/setup/maps/selected-map.tsx create mode 100644 src/dashboard/setup/maps/veto.tsx create mode 100644 src/dashboard/setup/maps/vetosingle.tsx diff --git a/src/dashboard/atoms/fit-text.tsx b/src/dashboard/atoms/fit-text.tsx new file mode 100644 index 0000000..42936ed --- /dev/null +++ b/src/dashboard/atoms/fit-text.tsx @@ -0,0 +1,48 @@ +// From jr-layouts by Hoishin https://github.com/JapaneseRestream/jr-layouts +// Slightly modified by Ewan Lyon + +import React, { useRef, useEffect } from 'react'; + +import styled from 'styled-components'; + +export const Text = styled.div` + white-space: nowrap; +`; + +interface Props { + text: string; + style?: React.CSSProperties; + className?: string; +} + +export const FitText: React.FunctionComponent = React.memo((props: Props) => { + const containerRef = useRef(null); + const textRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + const text = textRef.current; + + if (!container || !text) { + return; + } + + const MAX_WIDTH = container.clientWidth; + const currentWidth = text.clientWidth; + const scaleX = currentWidth > MAX_WIDTH ? MAX_WIDTH / currentWidth : 1; + const newTransform = `scaleX(${scaleX})`; + + text.style.transform = newTransform; + }); + + return ( +
+ {props.text} +
+ ); +}); + +FitText.displayName = 'FitText'; diff --git a/src/dashboard/atoms/grabhandle.tsx b/src/dashboard/atoms/grabhandle.tsx new file mode 100644 index 0000000..84b173c --- /dev/null +++ b/src/dashboard/atoms/grabhandle.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styled from 'styled-components'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; + +const Handle = styled.div` + height: 9px; + width: 18px; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const HandleBar = styled.div` + height: 2px; + width: 100%; + background: #525f78; + border-radius: 1px; +`; + +interface Props { + handleProps: DraggableProvidedDragHandleProps | undefined; +} + +export const GrabHandles: React.FC = (props: Props) => { + return ( + + + + + ); +}; diff --git a/src/dashboard/atoms/styled-ui.tsx b/src/dashboard/atoms/styled-ui.tsx new file mode 100644 index 0000000..f05a9c8 --- /dev/null +++ b/src/dashboard/atoms/styled-ui.tsx @@ -0,0 +1,58 @@ +import styled from 'styled-components'; + +import { Button, Checkbox, TextField } from '@material-ui/core'; +import { green, red } from '@material-ui/core/colors'; + +export const GreenButton = styled(Button)` + &.MuiButton-contained { + background: ${green[600]}; + color: white; + } + + &.MuiButton-contained:hover { + background: ${green[300]}; + } +`; + +export const RedButton = styled(Button)` + &.MuiButton-contained { + background: ${red[600]}; + color: white; + } + + &.MuiButton-contained:hover { + background: ${red[600]}; + } +`; + +export const GreenCheckbox = styled(Checkbox)` + &.MuiCheckbox-colorSecondary.Mui-checked:hover { + background-color: ${green[600]}0a; + } + + &.MuiIconButton-colorSecondary:hover { + background-color: ${green[600]}0a; + } + + &.MuiCheckbox-colorSecondary.Mui-checked { + color: ${green[600]}; + } + + & svg { + background-color: #ffffff; + } +`; + +export const LightTextfield = styled(TextField)` + &.Mui-focused { + color: #a8bde3; + } + + &.MuiInput-underline:after { + border-bottom: 2px solid #a8bde3; + } + + &.MuiInputBase-input { + color: #ffffff; + } +`; diff --git a/src/dashboard/maps.html b/src/dashboard/maps.html index c8e4a17..9d28fcf 100644 --- a/src/dashboard/maps.html +++ b/src/dashboard/maps.html @@ -5,9 +5,14 @@ Maps + -
- +
+ \ No newline at end of file diff --git a/src/dashboard/setup/maps/maps.tsx b/src/dashboard/setup/maps/maps.tsx deleted file mode 100644 index 8ed5b59..0000000 --- a/src/dashboard/setup/maps/maps.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useState } from 'react'; -import { render } from 'react-dom'; -import styled from 'styled-components'; -import { - Grid, - Select, - MenuItem, - FormControl, - InputLabel, - ThemeProvider, - Switch, - Button, - IconButton -} from '@material-ui/core'; -import DeleteIcon from '@material-ui/icons/Delete'; -import { useReplicant } from 'use-nodecg'; -import { TeamData } from '../../../types/extra-data'; -import { ExtraMapData } from '../../../types/map-data'; -import { TeamData as DummyTD } from '../../../extension/dummyData'; -import { MapData } from '../../map-data'; -import { theme } from '../../theme'; -import { SelectedMap } from './selected-map'; -import { shadow2 } from '../../atoms/shadows'; - -const SelectedMapContainer = styled(Grid)` - background-color: #435370; - box-shadow: ${shadow2}; - margin: 3px 0; -`; - -const Maps: React.FC = () => { - const [teamOneRep] = useReplicant('teamOne', DummyTD); - const [teamTwoRep] = useReplicant('teamTwo', DummyTD); - const [mapDataRep] = useReplicant('mapInfo', []); - - const [localBan, setLocalBan] = useState(false); - const [localTeam, setLocalTeam] = useState(''); - const [localMap, setLocalMap] = useState(''); - - const mapList = Object.keys(MapData).map(map => { - return ( - - {map} - - ); - }); - mapList.push( - - Unselected - - ); - - function removeMap(index: number): void { - // eslint-disable-next-line no-undef - nodecg.sendMessage('removeMap', mapDataRep[index].map); - } - - const selectedMaps = mapDataRep.map((selMap, index) => { - let teamName; - if (selMap.team === '1') { - teamName = teamOneRep.name; - } else if (selMap.team === '2') { - teamName = teamTwoRep.name; - } else { - teamName = 'Server'; - } - - return ( - - - - - removeMap(index)}> - - - - ); - }); - - function AddMap(): void { - let teamSelected; - if (localTeam === teamOneRep.name) { - teamSelected = '1'; - } else if (localTeam === teamTwoRep.name) { - teamSelected = '2'; - } else { - teamSelected = 'Server'; - } - - // eslint-disable-next-line no-undef - nodecg.sendMessage('addMap', { - map: localMap, - ban: localBan, - team: teamSelected - } as ExtraMapData); - } - - return ( - - - {selectedMaps} -
- - {/* Team selection */} - - Team - - - {/* Pick or Ban */} - - Pick - setLocalBan(!localBan)} /> - Ban - - - {/* Map selection */} - - Map - - - -
- -
-
- ); -}; - -render(, document.getElementById('maps')); diff --git a/src/dashboard/setup/maps/selected-map.tsx b/src/dashboard/setup/maps/selected-map.tsx deleted file mode 100644 index 552b470..0000000 --- a/src/dashboard/setup/maps/selected-map.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Grid } from '@material-ui/core'; - -const TeamName = styled.span` - width: 130px; - white-space: nowrap; -`; -const MapName = styled.span` - width: 90px; - text-align: right; -`; - -interface Props { - team: string; - map: string; - ban?: boolean; -} - -export const SelectedMap: React.FC = (props: Props) => { - return ( - - {props.team} - {props.ban ? 'BAN' : 'PICK'} - {props.map} - - ); -}; diff --git a/src/dashboard/setup/maps/veto.tsx b/src/dashboard/setup/maps/veto.tsx new file mode 100644 index 0000000..3c1588a --- /dev/null +++ b/src/dashboard/setup/maps/veto.tsx @@ -0,0 +1,220 @@ +import React, { useState } from 'react'; +import { render } from 'react-dom'; +import styled from 'styled-components'; +import { useReplicant } from 'use-nodecg'; +import { theme } from '../../theme'; + +import { Grid, Select, Chip, FormControl, InputLabel, MenuItem } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/styles'; +// import { Schedule } from '../types/schedule'; +import { + DragDropContext, + Droppable, + Draggable, + DraggingStyle, + NotDraggingStyle, + DropResult, +} from 'react-beautiful-dnd'; +import { ExtraMapData } from '../../../types/map-data'; +import { DashVETOSingle } from './vetosingle'; +import { TeamData as DummyTD } from '../../../extension/dummyData'; +import { TeamData } from '../../../types/extra-data'; +import { GreenButton } from '../../atoms/styled-ui'; + +const GreenButtonExtra = styled(GreenButton)` + min-width: 44px; +`; + +const SpacedChip = styled(Chip)` + margin: 2px 0; +`; + +const Divider = styled.div` + height: 1px; + width: 100%; + background: #525f78; + margin: 8px 0; +`; + +const mapNames = ['Dust2', 'Inferno', 'Mirage', 'Nuke', 'Overpass', 'Train', 'Vertigo']; + +const reorder = (list: any[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; + +const getItemStyle = (draggableStyle: DraggingStyle | NotDraggingStyle | undefined): React.CSSProperties => ({ + // Some basic styles to make the items look a bit nicer + userSelect: 'none', + margin: '0 10px 6px 10px', + + // Styles we need to apply on draggables + ...draggableStyle, +}); + +const DashVeto: React.FC = () => { + // const [currentMatchRep] = useReplicant('currentMatch', ''); + // const [scheduleRep] = useReplicant('schedule', []); + const [teamOneRep] = useReplicant('teamOne', DummyTD); + const [teamTwoRep] = useReplicant('teamTwo', DummyTD); + const [vetoRep] = useReplicant('mapInfo', []); + const [teamSelected, setTeamSelected] = useState(''); + const [mapSelected, setMapSelected] = useState(''); + const [vetoType, setVetoType] = useState('Ban'); + + // const scheduleIndex = scheduleRep.findIndex(game => game.id === currentMatchRep.toString()); + + // if (scheduleIndex === -1) { + // console.log('VETO: Could not find scheduled match'); + // return <>Could not find scheduled match to VETO (could probably do with a better ui tho); + // } + + // const scheduleGame = scheduleRep[scheduleIndex]; + + const mapItems = mapNames.map(map => { + const mapAlreadySelected = vetoRep.find(veto => (veto.map === map)); + return ( + + {map} + + ); + }); + + function onDragEnd(result: DropResult) { + // Dropped outside the list + if (!result.destination) { + return; + } + + const items = reorder(vetoRep, result.source.index, result.destination.index); + + nodecg.sendMessage('reorderMaps', items); + } + + function AddMap(): void { + // eslint-disable-next-line no-undef + nodecg.sendMessage('addMap', { + map: mapSelected, + ban: vetoType === 'Ban', + team: teamSelected + } as ExtraMapData); + } + + return ( + + + + + + Team + + + + + setVetoType('Pick')} + variant={vetoType === 'Pick' ? 'default' : 'outlined'} + /> + setVetoType('Ban')} + variant={vetoType === 'Ban' ? 'default' : 'outlined'} + /> + setVetoType('Default')} + variant={vetoType === 'Default' ? 'default' : 'outlined'} + /> + + + + Map + + + + + + + + + + {provided => ( +
+ {vetoRep.map((veto, index) => { + return ( + + {provided => ( +
+ +
+ )} +
+ ); + })} + {provided.placeholder} +
+ )} +
+
+
+
+ ); +}; + +render(, document.getElementById('dash-veto')); diff --git a/src/dashboard/setup/maps/vetosingle.tsx b/src/dashboard/setup/maps/vetosingle.tsx new file mode 100644 index 0000000..9d3705a --- /dev/null +++ b/src/dashboard/setup/maps/vetosingle.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import styled from 'styled-components'; +import { theme } from '../../theme'; + +import { ExtraMapData } from '../../../types/map-data'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; + +import { Chip, Grid } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/styles'; +import { GrabHandles } from '../../atoms/grabhandle'; +import { FitText, Text as FitTextText } from '../../atoms/fit-text'; +import { RedButton } from '../../atoms/styled-ui'; + +const VETOSingleContainer = styled.div` + background: #40495f; + border-radius: 7px; + padding: 10px; + box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.2), 0px 3px 3px rgba(0, 0, 0, 0.12), 0px 3px 4px rgba(0, 0, 0, 0.14); +`; + +const TeamName = styled(FitText)` + min-width: 100px; + max-width: 100px; + + justify-content: flex-start !important; + & > ${FitTextText} { + transform-origin: left !important; + } +`; + +const ChosenText = styled.span` + font-style: italic; +`; + +const MapText = styled.span` + width: 68px; + text-align: right; +`; + +const RedButtonExtra = styled(RedButton)` + min-width: 44px; +`; + +const Divider = styled.div` + height: 1px; + width: 100%; + background: #525f78; + margin: 8px 0; +`; + +interface Props { + veto: ExtraMapData; + handleProps: DraggableProvidedDragHandleProps | undefined; + otherTeamName: string; +} + +export const DashVETOSingle: React.FC = (props: Props) => { + let sidePicker = <>; + + if (!props.veto.ban) { + sidePicker = ( + + + + nodecg.sendMessage('setVetoSide', { mapName: props.veto.map, side: 'CT' })} + variant={props.veto.side === 'CT' ? 'default' : 'outlined'} + /> + nodecg.sendMessage('setVetoSide', { mapName: props.veto.map, side: 'T' })} + variant={props.veto.side === 'T' ? 'default' : 'outlined'} + /> + nodecg.sendMessage('setVetoSide', { mapName: props.veto.map, side: 'Knife' })} + variant={props.veto.side === 'Knife' ? 'default' : 'outlined'} + /> + + + ); + } + + function removeVeto() { + nodecg.sendMessage('removeVetoMap', props.veto.map); + } + + return ( + + + + + + {props.veto.ban ? 'Ban' : 'Pick'} + {props.veto.map} + + + {sidePicker} + + + ); +}; diff --git a/src/extension/extraData.ts b/src/extension/extraData.ts index ba949f1..3dcf8ca 100644 --- a/src/extension/extraData.ts +++ b/src/extension/extraData.ts @@ -283,6 +283,10 @@ nodecg.listenFor('removeMap', mapName => { } }); +nodecg.listenFor('reorderMaps', newOrder => { + mapDataRep.value = newOrder; +}); + function sumGrenades(players: CSGOOutputAllplayer[]): TeamData['grenades'] { const nades = { he: 0, diff --git a/src/types/map-data.d.ts b/src/types/map-data.d.ts index 3a0bd41..ed5937f 100644 --- a/src/types/map-data.d.ts +++ b/src/types/map-data.d.ts @@ -1,9 +1,10 @@ export interface ExtraMapData { map: string; - team: '1' | '2' | 'Server'; + team: string; teamOneScore?: number; teamTwoScore?: number; ban: boolean; + side: string; } export interface MapPlayerData {