diff --git a/src/app/api/tank-voting/[id]/average/route.ts b/src/app/api/tank-voting/[id]/average/route.ts index bb984e8bf..9e93b294c 100644 --- a/src/app/api/tank-voting/[id]/average/route.ts +++ b/src/app/api/tank-voting/[id]/average/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { isValidBlitzId } from '../../../../../core/blitz/isValidBlitzId'; import { tankDefinitions } from '../../../../../core/blitzkit/tankDefinitions'; import { usersDatabase } from '../../../../../databases/users'; -import { BlitzkitResponse } from '../../../../../hooks/useTankVotes'; +import { + BlitzkitResponse, + BlitzkitResponseError, +} from '../../../../../hooks/useTankVotes'; import { TankVoteCategory } from '../cast/route'; export interface TankVotes { @@ -22,7 +25,7 @@ export async function GET( return NextResponse.json({ status: 'error', error: 'INVALID_TANK_ID', - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } const blitzIdString = request.nextUrl.searchParams.get('player'); @@ -33,7 +36,7 @@ export async function GET( return NextResponse.json({ status: 'error', error: 'UNDEFINED_TOKEN_OR_PLAYER_ID', - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } if ( @@ -44,7 +47,7 @@ export async function GET( return NextResponse.json({ status: 'error', error: 'INVALID_TOKEN_OR_PLAYER_ID', - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } try { @@ -76,6 +79,6 @@ export async function GET( status: 'error', error: 'DATABASE_ERROR', message, - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } } diff --git a/src/app/api/tank-voting/[id]/cast/route.ts b/src/app/api/tank-voting/[id]/cast/route.ts index fe51e340b..3290e0fdc 100644 --- a/src/app/api/tank-voting/[id]/cast/route.ts +++ b/src/app/api/tank-voting/[id]/cast/route.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { isValidBlitzId } from '../../../../../core/blitz/isValidBlitzId'; import { tankDefinitions } from '../../../../../core/blitzkit/tankDefinitions'; import { usersDatabase } from '../../../../../databases/users'; -import { BlitzkitResponse } from '../../../../../hooks/useTankVotes'; +import { + BlitzkitResponse, + BlitzkitResponseError, +} from '../../../../../hooks/useTankVotes'; import { StarsInt } from '../../../../tools/tankopedia/[id]/components/Model/MetaSection/components/Stars'; export const TANK_VOTE_CATEGORIES = [ @@ -25,16 +28,29 @@ export async function GET( return NextResponse.json({ status: 'error', error: 'INVALID_TANK_ID', - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } - const blitzId = Number(request.nextUrl.searchParams.get('player')); + const blitzIdString = request.nextUrl.searchParams.get('player'); + const blitzId = Number(blitzIdString); + const blitzToken = request.nextUrl.searchParams.get('token'); - if (isNaN(blitzId) || !(await isValidBlitzId(blitzId))) { + if (blitzIdString === null || blitzToken === null) { return NextResponse.json({ status: 'error', - error: 'INVALID_PLAYER_ID', - } satisfies BlitzkitResponse); + error: 'UNDEFINED_TOKEN_OR_PLAYER_ID', + } satisfies BlitzkitResponseError); + } + + if ( + blitzIdString !== null && + (!(await isValidBlitzId(blitzId, blitzToken ?? undefined)) || + isNaN(blitzId)) + ) { + return NextResponse.json({ + status: 'error', + error: 'INVALID_TOKEN_OR_PLAYER_ID', + } satisfies BlitzkitResponseError); } const votes: Partial> = {}; @@ -51,7 +67,7 @@ export async function GET( return NextResponse.json({ status: 'error', error: 'INVALID_VOTE', - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } votes[category] = vote as StarsInt; @@ -62,7 +78,7 @@ export async function GET( return NextResponse.json({ status: 'error', error: 'NO_VOTES', - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } try { @@ -90,12 +106,12 @@ export async function GET( return NextResponse.json({ status: 'ok', data: undefined, - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponse); } catch (message) { return NextResponse.json({ status: 'error', error: 'DATABASE_ERROR', message, - } satisfies BlitzkitResponse); + } satisfies BlitzkitResponseError); } } diff --git a/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Stars/components/PartialStar.tsx b/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Stars/components/PartialStar.tsx index f47a8cfa5..73fbf8f1d 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Stars/components/PartialStar.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Stars/components/PartialStar.tsx @@ -1,13 +1,13 @@ import { StarFilledIcon, StarIcon } from '@radix-ui/react-icons'; -import { Box } from '@radix-ui/themes'; +import { Box, BoxProps } from '@radix-ui/themes'; -interface PartialStarProps { +type PartialStarProps = BoxProps & { fill: number; -} +}; -export function PartialStar({ fill }: PartialStarProps) { +export function PartialStar({ fill, ...props }: PartialStarProps) { return ( - + {fill > 0 && ( void; +}; -export function Stars({ stars }: StarsProps) { +export function Stars({ stars, onCast, ...props }: StarsProps) { let color: TextProps['color'] = 'green'; if (stars === null) color = 'gray'; @@ -18,15 +19,25 @@ export function Stars({ stars }: StarsProps) { else if (stars < 4.5) color = 'blue'; return ( - - + + {stars !== null && times(Math.floor(stars), (index) => ( - + onCast?.((index + 1) as StarsInt)} + /> ))} {stars !== null && stars % 1 !== 0 && } {times(5 - Math.ceil(stars ?? 0), (index) => ( - + + onCast?.((Math.floor(stars ?? 0) + index + 1) as StarsInt) + } + /> ))} diff --git a/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/components/VoteCaster.tsx b/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/components/VoteCaster.tsx new file mode 100644 index 000000000..defb79869 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/components/VoteCaster.tsx @@ -0,0 +1,179 @@ +import { PaperPlaneIcon } from '@radix-ui/react-icons'; +import { + AlertDialog, + Button, + Dialog, + Flex, + Heading, + Spinner, + Text, +} from '@radix-ui/themes'; +import { useState } from 'react'; +import { BlitzkitResponse } from '../../../../../../../../../../hooks/useTankVotes'; +import * as App from '../../../../../../../../../../stores/app'; +import * as Duel from '../../../../../../../../../../stores/duel'; +import { Stars, StarsInt } from '../../Stars'; + +export function VoteCaster() { + const tank = Duel.use((state) => state.protagonist.tank); + const [easiness, setEasiness] = useState(null); + const [firepower, setFirepower] = useState(null); + const [maneuverability, setManeuverability] = useState(null); + const [survivability, setSurvivability] = useState(null); + const appStore = App.useStore(); + // true = vote successful, false = vote failed, null = hide alert + const [postVote, setPostVote] = useState(null); + // true = open, false = closed, null = casting + const [showDialogue, setShowDialogue] = useState(false); + + return ( + <> + + + + + + + Voting for {tank.name} + + + + + Easiness{' '} + + {easiness === null ? '(empty)' : `(${easiness})`} + + + + + + + Firepower{' '} + + {firepower === null ? '(empty)' : `(${firepower})`} + + + + + + + Maneuverability{' '} + + {maneuverability === null + ? '(empty)' + : `(${maneuverability})`} + + + + + + + Survivability{' '} + + {survivability === null ? '(empty)' : `(${survivability})`} + + + + + + + + + + + + + + + + + setPostVote(null)} + > + {postVote === true && ( + + Vote cased + + It may take a few minutes for it to show up. + + + + + + + + + )} + + {postVote === false && ( + + Voting failed + + So sorry about this! If you're a nerd, feel free to check out the + console. + + + + + + + + + )} + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/index.tsx b/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/index.tsx index 58c58a4ed..f0f25b76b 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/index.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/MetaSection/components/Votes/index.tsx @@ -1,4 +1,4 @@ -import { Button, Flex, Heading, Text } from '@radix-ui/themes'; +import { Flex, Heading, Text } from '@radix-ui/themes'; import { memo } from 'react'; import { WargamingLoginButton } from '../../../../../../../../../components/Navbar/components/WargamingLoginButton'; import { formatCompact } from '../../../../../../../../../core/math/formatCompact'; @@ -6,6 +6,7 @@ import { useTankVotes } from '../../../../../../../../../hooks/useTankVotes'; import * as App from '../../../../../../../../../stores/app'; import * as Duel from '../../../../../../../../../stores/duel'; import { StarRow } from './components/StarRow'; +import { VoteCaster } from './components/VoteCaster'; export const Votes = memo( () => { @@ -24,11 +25,11 @@ export const Votes = memo( {!wargaming && ( - + Login to vote )} - {wargaming && } + {wargaming && } @@ -54,9 +55,13 @@ export const Votes = memo( /> - {votes.last_updated && ( + {wargaming && ( - You voted on {new Date(votes.last_updated).toLocaleDateString()} + <> + {votes.last_updated + ? `You voted on ${new Date(votes.last_updated).toLocaleDateString()}` + : `You haven't voted yet`} + )} diff --git a/src/hooks/useTankVotes.ts b/src/hooks/useTankVotes.ts index 4c03b238a..b7d004749 100644 --- a/src/hooks/useTankVotes.ts +++ b/src/hooks/useTankVotes.ts @@ -3,12 +3,14 @@ import { TankVotes } from '../app/api/tank-voting/[id]/average/route'; import { unwrapBlitzkitResponse } from '../core/blitzkit/unwrapBlitzkitResponse'; import * as App from '../stores/app'; +export interface BlitzkitResponseError { + status: 'error'; + error: string; + message?: unknown; +} + export type BlitzkitResponse = - | { - status: 'error'; - error: string; - message?: unknown; - } + | BlitzkitResponseError | { status: 'ok'; data: Data;