Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add and edit matches manually #1534

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/match-info/create-event-match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export const createEventMatch = (
// UTC Date - scheduled match time
time: string
},
) => request<null>('POST', `events/${eventKey}/matches`, {}, match)
) => request<null>('PUT', `events/${eventKey}/matches/${match.key}`, {}, match)
11 changes: 9 additions & 2 deletions src/components/event-matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { formatMatchKey } from '@/utils/format-match-key'
interface Props {
matches: ProcessedMatchInfo[]
eventKey: string
isAdmin?: boolean
}

const searchTextStyles = css`
Expand All @@ -28,7 +29,7 @@ const enum QueryRank {
MatchExact,
}

export const EventMatches = ({ matches, eventKey }: Props) => {
export const EventMatches = ({ matches, eventKey, isAdmin }: Props) => {
const [searchQuery, setSearchQuery] = useState<string>('')
const s = searchQuery.trim().toLowerCase()

Expand Down Expand Up @@ -80,7 +81,13 @@ export const EventMatches = ({ matches, eventKey }: Props) => {
/>
<div class={matchListStyle}>
{filteredMatches.map((m) => (
<MatchDetailsCard eventKey={eventKey} match={m} key={m.key} link />
<MatchDetailsCard
eventKey={eventKey}
match={m}
key={m.key}
link
isAdmin={isAdmin}
/>
))}
</div>
</>
Expand Down
33 changes: 29 additions & 4 deletions src/components/match-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Card from '@/components/card'
import { css } from '@linaria/core'
import { memo } from '@/utils/memo'
import clsx from 'clsx'
import IconButton from './icon-button'
import { mdiPencil } from '@mdi/js'

interface MatchCardProps {
match: {
Expand All @@ -17,13 +19,14 @@ interface MatchCardProps {
eventKey: string
link?: boolean
class?: string
isAdmin?: boolean
}

const matchCardStyle = css`
font-size: 0.93rem;
align-items: center;
display: grid;
grid-template-columns: auto auto 10rem;
grid-template-columns: auto auto auto 10rem;
overflow: hidden;
text-decoration: none;

Expand All @@ -40,13 +43,21 @@ const matchCardStyle = css`
const matchTitleStyle = css`
font-weight: bold;
grid-row: span 2;
grid-column: 1;
white-space: nowrap;
margin: 0.3rem 0.6rem;
margin: 0.3rem 0rem 0.3rem 0.6rem;
align-items: center;

& > * {
margin: 0.3rem 0;
}
`

const pencilStyle = css`
grid-column: 2;
grid-row: span 2;
`

const matchNumStyle = css`
grid-row: 2;
text-transform: uppercase;
Expand All @@ -58,7 +69,7 @@ const matchNumStyle = css`

const allianceStyle = css`
white-space: nowrap;
grid-column: 3;
grid-column: 4;
align-self: stretch;
margin-left: 0.3rem;
padding: 0.35rem 0.8rem;
Expand All @@ -82,8 +93,13 @@ const blueStyle = css`
background-color: var(--alliance-blue);
`

const pencilIconStyle = css`
width: 1rem;
aspect-ratio: 1;
`

export const MatchDetailsCard = memo(
({ match, eventKey, link, class: className }: MatchCardProps) => {
({ match, eventKey, link, class: className, isAdmin }: MatchCardProps) => {
const matchName = formatMatchKey(match.key)

const createTeamLinks = (teams: string[]) =>
Expand Down Expand Up @@ -111,6 +127,15 @@ export const MatchDetailsCard = memo(
<div class={matchNumStyle}>{`Match ${matchName.num}`}</div>
)}
</div>
<div class={pencilStyle}>
{isAdmin && (
<IconButton
icon={mdiPencil}
href={`/events/${eventKey}/matches/${match.key}/editor`}
class={pencilIconStyle}
/>
)}
</div>
{match.time && (
<time dateTime={match.time.toISOString()}>
{formatTime(match.time)}
Expand Down
8 changes: 8 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ const routes = [
path: '/events/:eventKey/matches/:matchKey/scout',
component: () => import('./routes/scout'),
},
{
path: '/events/:eventKey/match-creator',
component: () => import('./routes/match-creator'),
},
{
path: '/events/:eventKey/matches/:matchKey/editor',
component: () => import('./routes/match-editor'),
},
{
path: '/events/:eventKey/teams/:teamNum',
component: () => import('./routes/event-team'),
Expand Down
11 changes: 10 additions & 1 deletion src/routes/event-match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ConnectionType,
useNetworkConnection,
} from '@/utils/use-network-connection'
import { useJWT } from '@/jwt'

interface Props {
eventKey: string
Expand Down Expand Up @@ -160,6 +161,10 @@ const EventMatch = ({ eventKey, matchKey }: Props) => {
}
}, [match, isOnline])

const { jwt } = useJWT()
const isAdmin =
jwt && (jwt.peregrineRoles.isAdmin || jwt.peregrineRoles.isSuperAdmin)

return (
// page setup
<Page
Expand Down Expand Up @@ -211,7 +216,11 @@ const EventMatch = ({ eventKey, matchKey }: Props) => {
</div>
</Card>
)}
<MatchDetailsCard match={match} eventKey={eventKey} />
<MatchDetailsCard
match={match}
eventKey={eventKey}
isAdmin={isAdmin || false}
/>
{reports && reports.length > 0 ? (
<MatchReports
match={match}
Expand Down
16 changes: 15 additions & 1 deletion src/routes/event.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Heading } from '@/components/heading'
import { EventMatches } from '@/components/event-matches'
import Loader from '@/components/loader'
import { useEventMatches } from '@/cache/event-matches/use'
import { useJWT } from '@/jwt'

interface Props {
eventKey: string
Expand Down Expand Up @@ -52,6 +53,9 @@ const Event = ({ eventKey }: Props) => {
const matches = useEventMatches(eventKey)
const eventInfo = useEventInfo(eventKey)
const newestIncompleteMatch = matches && nextIncompleteMatch(matches)
const { jwt } = useJWT()
const isAdmin =
jwt && (jwt.peregrineRoles.isAdmin || jwt.peregrineRoles.isSuperAdmin)

return (
<Page
Expand All @@ -65,6 +69,11 @@ const Event = ({ eventKey }: Props) => {
</Heading>
{eventInfo && <EventInfoCard event={eventInfo} />}
<Button href={`/events/${eventKey}/analysis`}>Analysis</Button>
{isAdmin && (
<Button href={`/events/${eventKey}/match-creator`}>
Create New Match
</Button>
)}
</div>

<div class={sectionStyle}>
Expand All @@ -77,11 +86,16 @@ const Event = ({ eventKey }: Props) => {
match={newestIncompleteMatch}
eventKey={eventKey}
link
isAdmin={isAdmin || false}
/>
)}
{matches ? (
matches.length > 0 ? (
<EventMatches matches={matches} eventKey={eventKey} />
<EventMatches
matches={matches}
eventKey={eventKey}
isAdmin={isAdmin || false}
/>
) : (
<p class={noMatchesStyle}>No matches yet</p>
)
Expand Down
111 changes: 111 additions & 0 deletions src/routes/match-creator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { createEventMatch } from '@/api/match-info/create-event-match'
import Button from '@/components/button'
import Card from '@/components/card'
import { useErrorEmitter, ErrorBoundary } from '@/components/error-boundary'
import { Form } from '@/components/form'
import Page from '@/components/page'
import TextInput from '@/components/text-input'
import { route } from '@/router'
import {
formatDate,
formatTime,
getTimeFromParts,
} from '@/utils/get-time-from-parts'
import { css } from '@linaria/core'
import { useState } from 'preact/hooks'

const cardStyle = css`
margin: 1.5rem;
padding: 1.5rem 2rem;
width: 20rem;
margin-left: auto;
margin-right: auto;
& > * {
margin-left: 0;
margin-right: 0;
}
`

const CreatorForm = ({ eventKey }: { eventKey: string }) => {
const [isLoading, setIsLoading] = useState(false)
const [matchNumber, setMatchNumber] = useState('')
const [day, setDay] = useState('')
const [time, setTime] = useState('')
const [redScore, setRedScore] = useState(0)
const [blueScore, setBlueScore] = useState(0)
const [teamList, setTeamList] = useState([''])
const emitError = useErrorEmitter()

const onSubmit = (e: Event) => {
e.preventDefault()
setIsLoading(true)
createEventMatch(eventKey, {
redAlliance: teamList.slice(0, 3),
blueAlliance: teamList.slice(3, 6),
time: getTimeFromParts(day, time),
redScore,
blueScore,
key: `qm${matchNumber}`,
})
.then(() => route(`/events/${eventKey}/matches/qm${matchNumber}`))
.catch(emitError)
.finally(() => setIsLoading(false))
}

return (
<Form onSubmit={onSubmit}>
{(isValid) => (
<>
<TextInput label="Match Number" onInput={setMatchNumber} required />
<TextInput
label="Date (in mm/dd format) or leave blank for today"
onInput={setDay}
placeholder={formatDate(new Date(Date.now()))}
/>
<TextInput
label="Time (in hh:mm format) or leave blank for current time"
onInput={setTime}
placeholder={formatTime(new Date(Date.now()))}
/>
<TextInput
label="Teams (separate numbers with commas, red alliance first)"
required
onInput={(input) => {
const teams = input.split(', ')
for (let i = 0; i < teams.length; i++) {
teams[i] = 'frc' + teams[i]
}
setTeamList(teams)
}}
placeholder="Red1, Red2, Red3, Blue1, Blue2, Blue3"
/>
<TextInput
label="Red Alliance Score"
onInput={(input) => setRedScore(Number.parseInt(input))}
placeholder="0"
/>
<TextInput
label="Blue Alliance Score"
onInput={(input) => setBlueScore(Number.parseInt(input))}
placeholder="0"
/>
<Button disabled={isLoading || !isValid}>
{isLoading ? 'Saving Match Information' : 'Save Match'}
</Button>
</>
)}
</Form>
)
}

const MatchCreator = ({ eventKey }: { eventKey: string }) => (
<Page name={'Create Match'} back={`/events/${eventKey}`}>
<Card class={cardStyle}>
<ErrorBoundary>
<CreatorForm eventKey={eventKey} />
</ErrorBoundary>
</Card>
</Page>
)

export default MatchCreator
Loading
Loading