mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: cast ui
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
|
||||
+18
-7
@@ -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>
|
||||
|
||||
+179
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+10
-5
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user