mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-14 05:06:11 +01:00
feat: voting averages
This commit is contained in:
1 parent
c3c16bf607
commit
2d2d31adf7
24 files changed
+643
-254
No files matched your search
+25
@@ -0,0 +1,25 @@
|
||||
import { StarFilledIcon, StarIcon } from '@radix-ui/react-icons';
|
||||
import { Box } from '@radix-ui/themes';
|
||||
|
||||
interface PartialStarProps {
|
||||
fill: number;
|
||||
}
|
||||
|
||||
export function PartialStar({ fill }: PartialStarProps) {
|
||||
return (
|
||||
<Box height="1em" width="1em" position="relative">
|
||||
<StarIcon width="1em" height="1em" />
|
||||
{fill > 0 && (
|
||||
<Box
|
||||
top="0"
|
||||
left="0"
|
||||
width={`${fill * 100}%`}
|
||||
position="absolute"
|
||||
overflow="hidden"
|
||||
>
|
||||
<StarFilledIcon width="1em" height="1em" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Flex, Text, TextProps } from '@radix-ui/themes';
|
||||
import { times } from 'lodash';
|
||||
import { PartialStar } from './components/PartialStar';
|
||||
|
||||
export type StarsInt = 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
interface StarsProps {
|
||||
stars: number | null;
|
||||
}
|
||||
|
||||
export function Stars({ stars }: StarsProps) {
|
||||
let color: TextProps['color'] = 'green';
|
||||
|
||||
if (stars === null) color = 'gray';
|
||||
else if (stars < 1.5) color = 'tomato';
|
||||
else if (stars < 2.5) color = 'orange';
|
||||
else if (stars < 3.5) color = 'amber';
|
||||
else if (stars < 4.5) color = 'blue';
|
||||
|
||||
return (
|
||||
<Text color={color}>
|
||||
<Flex>
|
||||
{stars !== null &&
|
||||
times(Math.floor(stars), (index) => (
|
||||
<PartialStar fill={1} key={index} />
|
||||
))}
|
||||
{stars !== null && stars % 1 !== 0 && <PartialStar fill={stars % 1} />}
|
||||
{times(5 - Math.ceil(stars ?? 0), (index) => (
|
||||
<PartialStar fill={0} key={index} />
|
||||
))}
|
||||
</Flex>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import { Stars } from '../../Stars';
|
||||
|
||||
interface StarRowProps {
|
||||
stars: number | null;
|
||||
children: string;
|
||||
}
|
||||
|
||||
export function StarRow({ stars, children }: StarRowProps) {
|
||||
return (
|
||||
<Text color={stars === null ? 'gray' : undefined}>
|
||||
<Flex align="center" justify="between" width="15rem">
|
||||
{children}
|
||||
<Stars stars={stars} />
|
||||
</Flex>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { memo } from 'react';
|
||||
import { WargamingLoginButton } from '../../../../../../../../../components/Navbar/components/WargamingLoginButton';
|
||||
import { formatCompact } from '../../../../../../../../../core/math/formatCompact';
|
||||
import { useTankVotes } from '../../../../../../../../../hooks/useTankVotes';
|
||||
import * as App from '../../../../../../../../../stores/app';
|
||||
import * as Duel from '../../../../../../../../../stores/duel';
|
||||
import { StarRow } from './components/StarRow';
|
||||
|
||||
export const Votes = memo(
|
||||
() => {
|
||||
const id = Duel.use((state) => state.protagonist.tank.id);
|
||||
const votes = useTankVotes(id);
|
||||
const wargaming = App.useDeferred(
|
||||
undefined,
|
||||
(state) => state.logins.wargaming,
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="3">
|
||||
<Flex justify="between" align="end">
|
||||
<Heading size="4">
|
||||
{formatCompact(votes.votes)} {votes.votes === 1 ? 'vote' : 'votes'}
|
||||
</Heading>
|
||||
|
||||
{!wargaming && (
|
||||
<WargamingLoginButton variant="outline">
|
||||
Login to vote
|
||||
</WargamingLoginButton>
|
||||
)}
|
||||
{wargaming && <Button>Vote</Button>}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="0" direction="column">
|
||||
<Flex
|
||||
gap={{ initial: '0', sm: '6' }}
|
||||
direction={{ initial: 'column', sm: 'row' }}
|
||||
>
|
||||
<StarRow stars={votes.categories.easiness} children="Easiness" />
|
||||
<StarRow stars={votes.categories.firepower} children="Firepower" />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
gap={{ initial: '0', sm: '6' }}
|
||||
direction={{ initial: 'column', sm: 'row' }}
|
||||
>
|
||||
<StarRow
|
||||
stars={votes.categories.maneuverability}
|
||||
children="Maneuverability"
|
||||
/>
|
||||
<StarRow
|
||||
stars={votes.categories.survivability}
|
||||
children="Survivability"
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{votes.last_updated && (
|
||||
<Text align="center" size="2" color="gray" mt="2">
|
||||
You voted on {new Date(votes.last_updated).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { Button, Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { WargamingLoginButton } from '../../../../../../../../components/Navbar/components/WargamingLoginButton';
|
||||
import * as App from '../../../../../../../../stores/app';
|
||||
|
||||
export function VotesSkeleton() {
|
||||
const wargaming = App.useDeferred(
|
||||
undefined,
|
||||
(state) => state.logins.wargaming,
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="3">
|
||||
<Flex justify="between" align="end">
|
||||
<Heading size="4">
|
||||
<Skeleton height="1em" width="6rem" />
|
||||
</Heading>
|
||||
|
||||
{!wargaming && (
|
||||
<WargamingLoginButton variant="outline">
|
||||
Login to vote
|
||||
</WargamingLoginButton>
|
||||
)}
|
||||
{wargaming && <Button>Vote</Button>}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="0" direction="column">
|
||||
<Flex gap="6">
|
||||
<Flex align="center" justify="between" width="15rem">
|
||||
<Text>Easiness</Text>
|
||||
<Skeleton height="1em" width="5rem" />
|
||||
</Flex>
|
||||
<Flex align="center" justify="between" width="15rem">
|
||||
<Text>Firepower</Text>
|
||||
<Skeleton height="1em" width="5rem" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="6">
|
||||
<Flex align="center" justify="between" width="15rem">
|
||||
<Text>Maneuverability</Text>
|
||||
<Skeleton height="1em" width="5rem" />
|
||||
</Flex>
|
||||
<Flex align="center" justify="between" width="15rem">
|
||||
<Text>Survivability</Text>
|
||||
<Skeleton height="1em" width="5rem" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Code, Flex } from '@radix-ui/themes';
|
||||
import { Suspense } from 'react';
|
||||
import { classIcons } from '../../../../../../../components/ClassIcon';
|
||||
import { asset } from '../../../../../../../core/blitzkit/asset';
|
||||
import { tankIcon } from '../../../../../../../core/blitzkit/tankIcon';
|
||||
import strings from '../../../../../../../lang/en-US.json';
|
||||
import * as App from '../../../../../../../stores/app';
|
||||
import * as Duel from '../../../../../../../stores/duel';
|
||||
import { Listing } from './components/Listing';
|
||||
import { Votes } from './components/Votes';
|
||||
import { VotesSkeleton } from './components/VotesSkeleton';
|
||||
|
||||
export function MetaSection() {
|
||||
const developerMode = App.useDeferred(false, (state) => state.developerMode);
|
||||
@@ -13,119 +15,110 @@ export function MetaSection() {
|
||||
const ClassIcon = classIcons[tank.class];
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
gap="6"
|
||||
direction={{ initial: 'column', sm: 'row' }}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
src={tankIcon(tank.id)}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
height: '8rem',
|
||||
width: '8rem',
|
||||
}}
|
||||
/>
|
||||
<Flex justify="center" align="center">
|
||||
<Flex direction="column" align="center" gap="6">
|
||||
<Flex gap="6">
|
||||
<Flex direction="column" width="100%">
|
||||
{tank.nameFull && (
|
||||
<Listing label="Full-name">{tank.nameFull}</Listing>
|
||||
)}
|
||||
<Listing label="Nation">
|
||||
<Flex align="center" gap="1">
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={tank.nation}
|
||||
src={asset(`flags/circle/${tank.nation}.webp`)}
|
||||
/>
|
||||
{
|
||||
strings.common.nations[
|
||||
tank.nation as keyof typeof strings.common.nations
|
||||
]
|
||||
}
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Class">
|
||||
<Flex align="center" gap="1">
|
||||
<ClassIcon width="1em" height="1em" />
|
||||
{strings.common.tank_class_short[tank.class]}
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Tier">{tank.tier}</Listing>
|
||||
<Listing
|
||||
label="Type"
|
||||
color={
|
||||
tank.treeType === 'collector'
|
||||
? 'blue'
|
||||
: tank.treeType === 'premium'
|
||||
? 'amber'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{strings.common.tree_type[tank.treeType]}
|
||||
</Listing>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
wrap={{ initial: 'nowrap', sm: 'wrap' }}
|
||||
direction={{ initial: 'column', sm: 'row' }}
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
gapX="6"
|
||||
width="100%"
|
||||
maxWidth="calc(2 * 15rem + var(--space-6))"
|
||||
>
|
||||
{tank.nameFull && <Listing label="Full-name">{tank.nameFull}</Listing>}
|
||||
<Listing label="Nation">
|
||||
<Flex align="center" gap="1">
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={tank.nation}
|
||||
src={asset(`flags/circle/${tank.nation}.webp`)}
|
||||
/>
|
||||
{
|
||||
strings.common.nations[
|
||||
tank.nation as keyof typeof strings.common.nations
|
||||
]
|
||||
}
|
||||
<Flex direction="column" width="100%">
|
||||
{developerMode && (
|
||||
<Listing label="DEV: ID">
|
||||
<Code>{tank.id}</Code>
|
||||
</Listing>
|
||||
)}
|
||||
{tank.treeType === 'premium' && (
|
||||
<Listing label="Purchase price">
|
||||
<Flex align="center" gap="1">
|
||||
{tank.price.value / 400}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt="gold"
|
||||
src={asset('icons/currencies/gold.webp')}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
)}
|
||||
<Listing
|
||||
label={`${tank.treeType === 'researchable' ? 'Purchase' : 'Restoration'} price`}
|
||||
>
|
||||
<Flex align="center" gap="1">
|
||||
{tank.price.value.toLocaleString()}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={tank.price.type}
|
||||
src={asset(
|
||||
`icons/currencies/${tank.price.type === 'gold' ? 'gold' : 'silver'}.webp`,
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Sale price">
|
||||
<Flex align="center" gap="1">
|
||||
{(tank.price.value / 2).toLocaleString()}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={tank.price.type}
|
||||
src={asset(
|
||||
`icons/currencies/${tank.price.type === 'gold' ? 'gold' : 'silver'}.webp`,
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
{tank.xp && (
|
||||
<Listing label="Research XP">
|
||||
<Flex align="center" gap="1">
|
||||
{tank.xp.toLocaleString()}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt="xp"
|
||||
src={asset('icons/currencies/xp.webp')}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
)}
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Tier">{tank.tier}</Listing>
|
||||
<Listing label="Class">
|
||||
<Flex align="center" gap="1">
|
||||
<ClassIcon width="1em" height="1em" />
|
||||
{strings.common.tank_class_short[tank.class]}
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing
|
||||
label="Type"
|
||||
color={
|
||||
tank.treeType === 'collector'
|
||||
? 'blue'
|
||||
: tank.treeType === 'premium'
|
||||
? 'amber'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{strings.common.tree_type[tank.treeType]}
|
||||
</Listing>
|
||||
{developerMode && (
|
||||
<Listing label="DEV: ID">
|
||||
<Code>{tank.id}</Code>
|
||||
</Listing>
|
||||
)}
|
||||
{tank.treeType === 'premium' && (
|
||||
<Listing label="Purchase price">
|
||||
<Flex align="center" gap="1">
|
||||
{tank.price.value / 400}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt="gold"
|
||||
src={asset('icons/currencies/gold.webp')}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
)}
|
||||
<Listing
|
||||
label={`${tank.treeType === 'researchable' ? 'Purchase' : 'Restoration'} price`}
|
||||
>
|
||||
<Flex align="center" gap="1">
|
||||
{tank.price.value.toLocaleString()}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={tank.price.type}
|
||||
src={asset(
|
||||
`icons/currencies/${tank.price.type === 'gold' ? 'gold' : 'silver'}.webp`,
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
<Listing label="Sale price">
|
||||
<Flex align="center" gap="1">
|
||||
{(tank.price.value / 2).toLocaleString()}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt={tank.price.type}
|
||||
src={asset(
|
||||
`icons/currencies/${tank.price.type === 'gold' ? 'gold' : 'silver'}.webp`,
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
{tank.xp && (
|
||||
<Listing label="Research XP">
|
||||
<Flex align="center" gap="1">
|
||||
{tank.xp.toLocaleString()}
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
alt="xp"
|
||||
src={asset('icons/currencies/xp.webp')}
|
||||
/>
|
||||
</Flex>
|
||||
</Listing>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Suspense fallback={<VotesSkeleton />}>
|
||||
<Votes />
|
||||
</Suspense>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -46,8 +46,6 @@ export const TankModel = memo(() => {
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
console.log(node.name);
|
||||
|
||||
function translateTexture(offset: number) {
|
||||
const mesh = node.children[0] as Mesh;
|
||||
const material = mesh?.material as MeshStandardMaterial;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { StarFilledIcon, StarIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Text, TextProps } from '@radix-ui/themes';
|
||||
import { times } from 'lodash';
|
||||
|
||||
interface StarsProps {
|
||||
stars: 1 | 2 | 3 | 4 | 5;
|
||||
lowerIsBetter?: boolean;
|
||||
}
|
||||
|
||||
export function Stars({ stars, lowerIsBetter = false }: StarsProps) {
|
||||
let color: TextProps['color'];
|
||||
const trueStars = lowerIsBetter ? 6 - stars : stars;
|
||||
|
||||
switch (trueStars) {
|
||||
case 1:
|
||||
color = 'red';
|
||||
break;
|
||||
case 2:
|
||||
color = 'orange';
|
||||
break;
|
||||
case 3:
|
||||
color = 'amber';
|
||||
break;
|
||||
case 4:
|
||||
color = 'blue';
|
||||
break;
|
||||
case 5:
|
||||
color = 'green';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<Text color={color}>
|
||||
{times(stars, () => (
|
||||
<StarFilledIcon />
|
||||
))}
|
||||
</Text>
|
||||
<Text>
|
||||
{times(5 - stars, () => (
|
||||
<StarIcon />
|
||||
))}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { WargamingLoginButton } from '../../../../../../components/Navbar/components/WargamingLoginButton';
|
||||
import * as App from '../../../../../../stores/app';
|
||||
import { Stars } from './components/Stars';
|
||||
|
||||
export function VotingSection() {
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<Flex direction="column" align="center">
|
||||
<Heading>User poll</Heading>
|
||||
<Flex align="center" gap="2">
|
||||
1.4K votes
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="0" direction="column">
|
||||
<Flex gap="6">
|
||||
<Flex align="center" justify="between" width="13rem">
|
||||
<Text>Difficulty</Text>
|
||||
<Stars lowerIsBetter stars={4} />
|
||||
</Flex>
|
||||
<Flex align="center" justify="between" width="13rem">
|
||||
<Text>Firepower</Text>
|
||||
<Stars stars={4} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="6">
|
||||
<Flex align="center" justify="between" width="13rem">
|
||||
<Text>Maneuverability</Text>
|
||||
<Stars stars={5} />
|
||||
</Flex>
|
||||
<Flex align="center" justify="between" width="13rem">
|
||||
<Text>Survivability</Text>
|
||||
<Stars stars={1} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{!wargaming && (
|
||||
<Flex align="center" gap="2">
|
||||
<Text color="gray">Sign in to vote</Text>
|
||||
<WargamingLoginButton />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import { HeroSection } from './components/Model/HeroSection';
|
||||
import { MetaSection } from './components/Model/MetaSection';
|
||||
import { TechTreeSection } from './components/Model/TechTreeSection';
|
||||
import { VideoSection } from './components/VideoSection';
|
||||
import { VotingSection } from './components/VotingSection';
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const id = Number(params.id);
|
||||
@@ -83,11 +82,11 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
<PageWrapper p="0" noMaxWidth color="purple" size={1600} gap="9">
|
||||
<HeroSection id={id} />
|
||||
<CalloutsSection />
|
||||
<VotingSection />
|
||||
<MetaSection />
|
||||
{/* <VotingSection /> */}
|
||||
{/* <ShotDisplaySection /> */}
|
||||
<TechTreeSection />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
<MetaSection />
|
||||
<CharacteristicsSection />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
<GameModeSection />
|
||||
|
||||
Reference in new issue
Block a user