feat: cast ui

This commit is contained in:
TrèsAbhi committed 2024-09-01 16:25:43 -05:00
1 parent 2d2d31adf7
commit c5e2c1fd0f
7 files changed
+253 -37

No files matched your search

@@ -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<TankVotes>);
} 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<TankVotes>);
} 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<TankVotes>);
} satisfies BlitzkitResponseError);
}
}
+26 -10
View File
@@ -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<Record<TankVoteCategory, StarsInt>> = {};
@@ -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<undefined>);
} satisfies BlitzkitResponse);
} catch (message) {
return NextResponse.json({
status: 'error',
error: 'DATABASE_ERROR',
message,
} satisfies BlitzkitResponse);
} satisfies BlitzkitResponseError);
}
}
@@ -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 (
<Box height="1em" width="1em" position="relative">
<Box height="1em" width="1em" position="relative" {...props}>
<StarIcon width="1em" height="1em" />
{fill > 0 && (
<Box
@@ -4,11 +4,12 @@ import { PartialStar } from './components/PartialStar';
export type StarsInt = 1 | 2 | 3 | 4 | 5;
interface StarsProps {
type StarsProps = TextProps & {
stars: number | null;
}
onCast?: (value: StarsInt) => 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 (
<Text color={color}>
<Flex>
<Text color={color} {...props}>
<Flex style={{ cursor: onCast ? 'pointer' : 'unset' }}>
{stars !== null &&
times(Math.floor(stars), (index) => (
<PartialStar fill={1} key={index} />
<PartialStar
fill={1}
key={index}
onClick={() => onCast?.((index + 1) as StarsInt)}
/>
))}
{stars !== null && stars % 1 !== 0 && <PartialStar fill={stars % 1} />}
{times(5 - Math.ceil(stars ?? 0), (index) => (
<PartialStar fill={0} key={index} />
<PartialStar
fill={0}
key={index}
onClick={() =>
onCast?.((Math.floor(stars ?? 0) + index + 1) as StarsInt)
}
/>
))}
</Flex>
</Text>
@@ -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<StarsInt | null>(null);
const [firepower, setFirepower] = useState<StarsInt | null>(null);
const [maneuverability, setManeuverability] = useState<StarsInt | null>(null);
const [survivability, setSurvivability] = useState<StarsInt | null>(null);
const appStore = App.useStore();
// true = vote successful, false = vote failed, null = hide alert
const [postVote, setPostVote] = useState<boolean | null>(null);
// true = open, false = closed, null = casting
const [showDialogue, setShowDialogue] = useState<boolean | null>(false);
return (
<>
<Dialog.Root open={showDialogue !== false} onOpenChange={setShowDialogue}>
<Dialog.Trigger>
<Button variant="surface">
<PaperPlaneIcon /> Vote
</Button>
</Dialog.Trigger>
<Dialog.Content maxWidth="23rem">
<Heading>Voting for {tank.name}</Heading>
<Flex direction="column" mt="4">
<Flex align="center" justify="between">
<Text>
Easiness{' '}
<Text color="gray" size="2">
{easiness === null ? '(empty)' : `(${easiness})`}
</Text>
</Text>
<Stars stars={easiness} size="5" onCast={setEasiness} />
</Flex>
<Flex align="center" justify="between">
<Text>
Firepower{' '}
<Text color="gray" size="2">
{firepower === null ? '(empty)' : `(${firepower})`}
</Text>
</Text>
<Stars stars={firepower} size="5" onCast={setFirepower} />
</Flex>
<Flex align="center" justify="between">
<Text>
Maneuverability{' '}
<Text color="gray" size="2">
{maneuverability === null
? '(empty)'
: `(${maneuverability})`}
</Text>
</Text>
<Stars
stars={maneuverability}
size="5"
onCast={setManeuverability}
/>
</Flex>
<Flex align="center" justify="between">
<Text>
Survivability{' '}
<Text color="gray" size="2">
{survivability === null ? '(empty)' : `(${survivability})`}
</Text>
</Text>
<Stars stars={survivability} size="5" onCast={setSurvivability} />
</Flex>
</Flex>
<Flex justify="end" mt="4" gap="2">
<Dialog.Close>
<Button variant="outline" color="red">
Cancel
</Button>
</Dialog.Close>
<Button
disabled={
showDialogue === null ||
(easiness === null &&
firepower === null &&
maneuverability === null &&
survivability === null)
}
onClick={() => {
const wargaming = appStore.getState().logins.wargaming;
if (!wargaming) {
return setShowDialogue(false);
}
setShowDialogue(null);
fetch(
`/api/tank-voting/${tank.id}/cast?${
easiness === null ? '' : `easiness=${easiness}&`
}${firepower === null ? '' : `firepower=${firepower}&`}${
maneuverability === null
? ''
: `maneuverability=${maneuverability}&`
}${
survivability === null
? ''
: `survivability=${survivability}&`
}player=${wargaming.id}&token=${wargaming.token}`,
)
.then(
(response) => response.json() as Promise<BlitzkitResponse>,
)
.then((json) => {
setShowDialogue(false);
setPostVote(json.status === 'ok');
if (json.status === 'error') {
console.error('vote failed', json);
}
});
}}
>
{showDialogue === null ? <Spinner /> : <PaperPlaneIcon />} Vote
</Button>
</Flex>
</Dialog.Content>
</Dialog.Root>
<AlertDialog.Root
open={postVote !== null}
onOpenChange={() => setPostVote(null)}
>
{postVote === true && (
<AlertDialog.Content maxWidth="20rem">
<AlertDialog.Title>Vote cased</AlertDialog.Title>
<AlertDialog.Description>
It may take a few minutes for it to show up.
</AlertDialog.Description>
<Flex justify="end" mt="4">
<AlertDialog.Cancel>
<Button>Okay</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
)}
{postVote === false && (
<AlertDialog.Content maxWidth="20rem">
<AlertDialog.Title>Voting failed</AlertDialog.Title>
<AlertDialog.Description>
So sorry about this! If you're a nerd, feel free to check out the
console.
</AlertDialog.Description>
<Flex justify="end" mt="4">
<AlertDialog.Cancel>
<Button>Gotcha</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
)}
</AlertDialog.Root>
</>
);
}
@@ -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(
</Heading>
{!wargaming && (
<WargamingLoginButton variant="outline">
<WargamingLoginButton variant="surface">
Login to vote
</WargamingLoginButton>
)}
{wargaming && <Button>Vote</Button>}
{wargaming && <VoteCaster />}
</Flex>
<Flex gap="0" direction="column">
@@ -54,9 +55,13 @@ export const Votes = memo(
/>
</Flex>
{votes.last_updated && (
{wargaming && (
<Text align="center" size="2" color="gray" mt="2">
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`}
</>
</Text>
)}
</Flex>
+7 -5
View File
@@ -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<Data = undefined> =
| {
status: 'error';
error: string;
message?: unknown;
}
| BlitzkitResponseError
| {
status: 'ok';
data: Data;