mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
Merge branch 'main' into dev
This commit is contained in:
commit
2d6b8105d2
33 files changed
+916
-390
No files matched your search
@@ -1,3 +1,9 @@
|
||||
## Visit the website
|
||||
|
||||
### https://blitzkit.app/
|
||||
|
||||

|
||||
|
||||
# BlitzKit
|
||||
|
||||
[](https://codeium.com)
|
||||
@@ -10,12 +16,6 @@ Go to [application directory for BlitzKit](https://discord.com/application-direc
|
||||
|
||||

|
||||
|
||||
## Visit the website
|
||||
|
||||
### https://blitzkit.app/
|
||||
|
||||

|
||||
|
||||
## Visit the documentation website
|
||||
|
||||
### https://tresabhi.github.io/blitzkit/
|
||||
|
||||
@@ -58,9 +58,9 @@ while (chunkIndex < preDiscoveredManifest.chunks) {
|
||||
}
|
||||
|
||||
const regionalIdIndex: Record<Region, number> = {
|
||||
asia: ids.find((id) => idToRegion(id) === 'asia') ?? MIN_IDS.asia,
|
||||
com: ids.find((id) => idToRegion(id) === 'com') ?? MIN_IDS.com,
|
||||
eu: ids.find((id) => idToRegion(id) === 'eu') ?? MIN_IDS.eu,
|
||||
asia: ids.findLast((id) => idToRegion(id) === 'asia') ?? MIN_IDS.asia,
|
||||
com: ids.findLast((id) => idToRegion(id) === 'com') ?? MIN_IDS.com,
|
||||
eu: ids.findLast((id) => idToRegion(id) === 'eu') ?? MIN_IDS.eu,
|
||||
};
|
||||
const zeroStreak: Record<Region, number> = { asia: 0, com: 0, eu: 0 };
|
||||
let regionIndex = 0;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { code: string } },
|
||||
) {
|
||||
const response = await fetch(
|
||||
`https://www.patreon.com/api/oauth2/token?code=${params.code}&grant_type=authorization_code&client_id=${process.env.NEXT_PUBLIC_PATREON_CLIENT_ID}&client_secret=${process.env.PATREON_CLIENT_SECRET}&redirect_uri=${process.env.NEXT_PUBLIC_PATREON_REDIRECT_URI}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
},
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
return NextResponse.json(json);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
interface PatreonMembershipResponse {
|
||||
data: {
|
||||
attributes: unknown;
|
||||
id: `${number}`;
|
||||
relationships: {
|
||||
memberships: {
|
||||
data: {
|
||||
id: string; // uuid
|
||||
type: 'member';
|
||||
}[];
|
||||
};
|
||||
};
|
||||
type: 'user';
|
||||
};
|
||||
included: (
|
||||
| {
|
||||
attributes: {
|
||||
patron_status:
|
||||
| 'active_patron'
|
||||
| 'declined_patron'
|
||||
| 'former_patron'
|
||||
| null; // null = never a paid member
|
||||
};
|
||||
id: string; // uuid
|
||||
relationships: {
|
||||
campaign: {
|
||||
data: {
|
||||
id: `${number}`; // id of my campaign to check against
|
||||
type: 'campaign';
|
||||
};
|
||||
links: {
|
||||
related: string; // url
|
||||
};
|
||||
};
|
||||
};
|
||||
type: 'member';
|
||||
}
|
||||
| {
|
||||
attributes: {};
|
||||
id: `${number}`;
|
||||
type: 'campaign'; // ignore this discriminator
|
||||
}
|
||||
)[];
|
||||
links: {
|
||||
self: 'https://www.patreon.com/api/oauth2/v2/user/124201389';
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { token: string } },
|
||||
) {
|
||||
const response = await fetch(
|
||||
encodeURI(
|
||||
'https://www.patreon.com/api/oauth2/v2/identity?include=memberships.campaign&fields[member]=patron_status',
|
||||
),
|
||||
{ headers: { Authorization: `Bearer ${params.token}` } },
|
||||
);
|
||||
|
||||
if (!response.ok) return NextResponse.json(false);
|
||||
|
||||
const json = (await response.json()) as PatreonMembershipResponse;
|
||||
|
||||
return NextResponse.json(
|
||||
json.included.some(
|
||||
(item) =>
|
||||
item.type === 'member' &&
|
||||
item.relationships.campaign.data.id ===
|
||||
process.env.PATREON_CAMPAIGN_ID &&
|
||||
item.attributes.patron_status === 'active_patron',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { token: string } },
|
||||
) {
|
||||
const response = await fetch(
|
||||
`https://www.patreon.com/api/oauth2/token?grant_type=refresh_token&refresh_token=${params.token}&client_id=${process.env.NEXT_PUBLIC_PATREON_CLIENT_ID}&client_secret=${process.env.PATREON_CLIENT_SECRET}`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
return NextResponse.json(json);
|
||||
}
|
||||
@@ -6,6 +6,15 @@ import { useEffect } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { mutateApp } from '../../../stores/app';
|
||||
|
||||
export interface PatreonAuthResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
token_type: 'Bearer';
|
||||
scope: string;
|
||||
refresh_token: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
searchParams,
|
||||
@@ -27,22 +36,41 @@ export default function Page({
|
||||
}
|
||||
|
||||
mutateApp((draft) => {
|
||||
draft.login = {
|
||||
expiresAt: Number(searchParams.expires_at),
|
||||
draft.logins.wargaming = {
|
||||
id: Number(searchParams.account_id),
|
||||
token: searchParams.access_token,
|
||||
expires: Number(searchParams.expires_at) * 1000,
|
||||
};
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'patreon': {
|
||||
if (!searchParams.code) break;
|
||||
|
||||
// cannot await here because async breaks router.push :(
|
||||
fetch(`/api/patreon/auth/${searchParams.code}`)
|
||||
.then((response) => response.json() as Promise<PatreonAuthResponse>)
|
||||
.then((data) => {
|
||||
mutateApp((draft) => {
|
||||
draft.logins.patreon = {
|
||||
token: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expires: Date.now() + data.expires_in * 1000,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.error(`Unknown provider: ${params.provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
router.push(searchParams.return ? searchParams.return : '/');
|
||||
router.push(searchParams.return ?? '/');
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -10,17 +10,24 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { extendAuth } from '../../core/blitz/extendAuth';
|
||||
import { logout } from '../../core/blitz/logout';
|
||||
import { extendAuthPatreon } from '../../core/blitz/extendAuthPatreon';
|
||||
import { extendAuthWargaming } from '../../core/blitz/extendAuthWargaming';
|
||||
import { logoutPatreon } from '../../core/blitz/logoutPatreon';
|
||||
import { logoutWargaming } from '../../core/blitz/logoutWargaming';
|
||||
import isDev from '../../core/blitzkit/isDev';
|
||||
import { isLocalhost } from '../../core/blitzkit/isLocalhost';
|
||||
import { CURRENT_POLICIES_AGREEMENT_INDEX, useApp } from '../../stores/app';
|
||||
|
||||
const DEV_BUILD_AGREEMENT_COOLDOWN = 8 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Wargaming: 14 (refresh: 7)
|
||||
* Patreon: 31 (refresh: 15)
|
||||
*/
|
||||
|
||||
export function Checks() {
|
||||
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
|
||||
const login = useApp((state) => state.login);
|
||||
const logins = useApp((state) => state.logins);
|
||||
const policiesAgreementIndex = useApp(
|
||||
(state) => state.policiesAgreementIndex,
|
||||
);
|
||||
@@ -43,15 +50,26 @@ export function Checks() {
|
||||
}, [policiesAgreementIndex, isLegal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!login) return;
|
||||
if (logins.wargaming) {
|
||||
const expiresIn = logins.wargaming.expires - Date.now();
|
||||
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||
|
||||
const expiresIn = login.expiresAt - Date.now() / 1000;
|
||||
const expiresInDays = expiresIn / 60 / 60 / 24;
|
||||
if (expiresInDays < 0) {
|
||||
logoutWargaming();
|
||||
} else if (expiresInDays < 7) {
|
||||
extendAuthWargaming();
|
||||
}
|
||||
}
|
||||
|
||||
if (expiresInDays < 0) {
|
||||
logout();
|
||||
} else if (expiresInDays < 7) {
|
||||
extendAuth();
|
||||
if (logins.patreon) {
|
||||
const expiresIn = logins.patreon.expires - Date.now();
|
||||
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||
|
||||
if (expiresInDays < 0) {
|
||||
logoutPatreon();
|
||||
} else if (expiresInDays < 15) {
|
||||
extendAuthPatreon();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Link, Text } from '@radix-ui/themes';
|
||||
import { patreonLoginUrl } from '../../core/blitzkit/patreonLoginUrl';
|
||||
import { useAdExempt } from '../../hooks/useAdExempt';
|
||||
import { PatreonIcon } from '../../icons/Patreon';
|
||||
import { useApp } from '../../stores/app';
|
||||
|
||||
export function PatreonPlug() {
|
||||
const patreon = useApp((state) => state.logins.patreon);
|
||||
const exempt = useAdExempt();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap="2"
|
||||
align="center"
|
||||
direction="column"
|
||||
justify="center"
|
||||
p="4"
|
||||
py="8"
|
||||
>
|
||||
<Flex gap="1" align="center">
|
||||
<Text size="4" align="center">
|
||||
{exempt
|
||||
? 'You are supporting BlitzKit, thank you!'
|
||||
: 'Active Patreon supports go ad-free'}
|
||||
</Text>
|
||||
{!exempt && <CheckIcon width="1em" height="1em" />}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" wrap="wrap" justify="center">
|
||||
<Link href="https://www.patreon.com/tresabhi" target="_blank">
|
||||
<Button>
|
||||
<PatreonIcon width={15} height={15} />{' '}
|
||||
{exempt ? 'Manage membership' : 'Support on Patreon'}
|
||||
</Button>
|
||||
</Link>
|
||||
{!patreon && !exempt && (
|
||||
<Link href={patreonLoginUrl()}>
|
||||
<Button variant="outline">Log in with Patreon</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Heading,
|
||||
Link,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { Tool } from '../../constants/tools';
|
||||
import { ImgurSize, imgur } from '../../core/blitzkit/imgur';
|
||||
|
||||
type ToolCardProps = FlexProps & {
|
||||
tool: Tool;
|
||||
};
|
||||
|
||||
export function ToolCard({ tool, style, ...props }: ToolCardProps) {
|
||||
const size = tool.significant
|
||||
? ImgurSize.HugeThumbnail
|
||||
: ImgurSize.LargeThumbnail;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 'var(--radius-2)',
|
||||
overflow: 'hidden',
|
||||
backgroundImage: `url(${imgur(tool.image, { format: 'jpeg', size })})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
height: tool.significant ? 'min(320px, 50vh)' : '256px',
|
||||
...style,
|
||||
}}
|
||||
flexGrow={tool.significant ? undefined : '1'}
|
||||
flexBasis={
|
||||
tool.significant
|
||||
? undefined
|
||||
: {
|
||||
initial: undefined,
|
||||
sm: '0',
|
||||
}
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Link
|
||||
href={`/tools/${tool.id}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
flexGrow="1"
|
||||
style={{
|
||||
backgroundImage: `url(${imgur(tool.image, { format: 'jpeg', size })})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
px={{
|
||||
initial: '6',
|
||||
md: tool.significant ? '8' : '6',
|
||||
}}
|
||||
py={{
|
||||
initial: '4',
|
||||
sm: tool.significant ? '6' : '4',
|
||||
}}
|
||||
gap="4"
|
||||
align="center"
|
||||
justify="between"
|
||||
width="100%"
|
||||
direction={{
|
||||
initial: 'column',
|
||||
sm: 'row',
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'var(--color-panel-translucent)',
|
||||
backdropFilter: 'blur(64px)',
|
||||
WebkitBackdropFilter: 'blur(64px)',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
justify="center"
|
||||
align={{
|
||||
initial: 'center',
|
||||
sm: 'start',
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
align={{
|
||||
initial: 'center',
|
||||
sm: 'left',
|
||||
}}
|
||||
size={{
|
||||
initial: '6',
|
||||
sm: tool.significant ? '7' : '5',
|
||||
}}
|
||||
weight="medium"
|
||||
>
|
||||
{tool.title}
|
||||
</Heading>
|
||||
<Text
|
||||
align={{
|
||||
initial: 'center',
|
||||
sm: 'left',
|
||||
}}
|
||||
size={{
|
||||
initial: '3',
|
||||
sm: tool.significant ? '4' : '3',
|
||||
}}
|
||||
color="gray"
|
||||
>
|
||||
{tool.description}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
size={{
|
||||
initial: undefined,
|
||||
sm: tool.significant ? '3' : undefined,
|
||||
}}
|
||||
color={tool.button.color}
|
||||
style={{
|
||||
cursor: 'inherit',
|
||||
}}
|
||||
>
|
||||
{tool.button.text}
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Link>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+94
-133
@@ -1,146 +1,107 @@
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Button, Flex, Grid, Heading, Text } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import PageWrapper from '../components/PageWrapper';
|
||||
import { TOOLS } from '../constants/tools';
|
||||
import { ImgurSize, imgur } from '../core/blitzkit/imgur';
|
||||
'use client';
|
||||
|
||||
import { Box, Flex } from '@radix-ui/themes';
|
||||
import { Ad, AdType } from '../components/Ad';
|
||||
import { AdMidSectionResponsive } from '../components/AdMidSectionResponsive';
|
||||
import {
|
||||
compareTool,
|
||||
discordTool,
|
||||
moreTool,
|
||||
ratingTool,
|
||||
sessionTool,
|
||||
tankopediaTool,
|
||||
} from '../constants/tools';
|
||||
import { useAdExempt } from '../hooks/useAdExempt';
|
||||
import { Hero } from './components/Hero';
|
||||
import { PatreonPlug } from './components/PatreonPlug';
|
||||
import { ToolCard } from './components/ToolCard';
|
||||
|
||||
export default function Page() {
|
||||
const exempt = useAdExempt();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
|
||||
<PageWrapper size={1028}>
|
||||
<Grid
|
||||
gap="4"
|
||||
columns={{
|
||||
initial: undefined,
|
||||
sm: '2',
|
||||
}}
|
||||
flow="row-dense"
|
||||
>
|
||||
{TOOLS.map((tool) => {
|
||||
const size = tool.significant
|
||||
? ImgurSize.HugeThumbnail
|
||||
: ImgurSize.LargeThumbnail;
|
||||
<PatreonPlug />
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={tool.id}
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 'var(--radius-2)',
|
||||
overflow: 'hidden',
|
||||
backgroundImage: `url(${imgur(tool.image, { format: 'jpeg', size })})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
height: tool.significant ? 'min(320px, 50vh)' : '256px',
|
||||
}}
|
||||
gridColumn={{
|
||||
initial: undefined,
|
||||
sm: tool.significant ? '1 / 3' : undefined,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={tool.href ?? `/tools/${tool.id}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
flexGrow="1"
|
||||
style={{
|
||||
backgroundImage: `url(${imgur(tool.image, { format: 'jpeg', size })})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
p="4"
|
||||
justify={
|
||||
exempt
|
||||
? 'center'
|
||||
: {
|
||||
initial: 'center',
|
||||
lg: 'between',
|
||||
}
|
||||
}
|
||||
gap="4"
|
||||
>
|
||||
{!exempt && (
|
||||
<Ad
|
||||
type={AdType.WideSkyscraperVerticalPurple}
|
||||
display={{
|
||||
initial: 'none',
|
||||
lg: 'block',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
px={{
|
||||
initial: '6',
|
||||
md: tool.significant ? '8' : '6',
|
||||
}}
|
||||
py={{
|
||||
initial: '4',
|
||||
sm: tool.significant ? '6' : '4',
|
||||
}}
|
||||
gap="4"
|
||||
align="center"
|
||||
justify="between"
|
||||
width="100%"
|
||||
direction={{
|
||||
initial: 'column',
|
||||
sm: 'row',
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: 'var(--color-panel-translucent)',
|
||||
backdropFilter: 'blur(64px)',
|
||||
WebkitBackdropFilter: 'blur(64px)',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
justify="center"
|
||||
align={{
|
||||
initial: 'center',
|
||||
sm: 'start',
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
align={{
|
||||
initial: 'center',
|
||||
sm: 'left',
|
||||
}}
|
||||
size={{
|
||||
initial: '6',
|
||||
sm: tool.significant ? '7' : '5',
|
||||
}}
|
||||
weight="medium"
|
||||
>
|
||||
{tool.title}
|
||||
</Heading>
|
||||
<Text
|
||||
align={{
|
||||
initial: 'center',
|
||||
sm: 'left',
|
||||
}}
|
||||
size={{
|
||||
initial: '3',
|
||||
sm: tool.significant ? '4' : '3',
|
||||
}}
|
||||
color="gray"
|
||||
>
|
||||
{tool.description}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex maxWidth="1024px" flexGrow="1" gap="4" direction="column">
|
||||
<ToolCard tool={tankopediaTool} />
|
||||
|
||||
<Button
|
||||
size={{
|
||||
initial: undefined,
|
||||
sm: tool.significant ? '3' : undefined,
|
||||
}}
|
||||
color={tool.button.color}
|
||||
style={{
|
||||
cursor: 'inherit',
|
||||
}}
|
||||
>
|
||||
{tool.button.text}
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Link>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</PageWrapper>
|
||||
{!exempt && (
|
||||
<Flex
|
||||
justify="center"
|
||||
display={{
|
||||
initial: 'flex',
|
||||
lg: 'none',
|
||||
}}
|
||||
>
|
||||
<AdMidSectionResponsive />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={compareTool} />
|
||||
<ToolCard tool={sessionTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={ratingTool} />
|
||||
<ToolCard tool={discordTool} />
|
||||
</Flex>
|
||||
|
||||
{!exempt && (
|
||||
<Flex
|
||||
justify="center"
|
||||
display={{
|
||||
initial: 'flex',
|
||||
md: 'none',
|
||||
}}
|
||||
>
|
||||
<AdMidSectionResponsive />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={moreTool} />
|
||||
<Box flexGrow="1" flexBasis="0" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{!exempt && (
|
||||
<Ad
|
||||
type={AdType.WideSkyscraperVerticalPurple}
|
||||
display={{
|
||||
initial: 'none',
|
||||
lg: 'block',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { debounce } from 'lodash';
|
||||
import { Fragment, use, useEffect, useRef, useState } from 'react';
|
||||
import { lerp } from 'three/src/math/MathUtils';
|
||||
import { Ad, AdType } from '../../../../../../components/Ad';
|
||||
import { Link } from '../../../../../../components/Link';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||
@@ -25,15 +26,15 @@ import {
|
||||
CREW_MEMBER_NAMES,
|
||||
GUN_TYPE_NAMES,
|
||||
} from '../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||
import { useAdExempt } from '../../../../../../hooks/useAdExempt';
|
||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
|
||||
import { Info } from './components/Info';
|
||||
import { InfoWithDelta } from './components/InfoWithDelta';
|
||||
|
||||
export function Characteristics() {
|
||||
const wideFormat = useWideFormat(960);
|
||||
const exempt = useAdExempt();
|
||||
const awaitedEquipmentDefinitions = use(equipmentDefinitions);
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
const awaitedProvisionDefinitions = use(provisionDefinitions);
|
||||
@@ -128,7 +129,13 @@ export function Characteristics() {
|
||||
}, [hasImprovedVerticalStabilizer]);
|
||||
|
||||
return (
|
||||
<Flex direction={wideFormat ? 'row' : 'column'} gap="8" wrap="wrap">
|
||||
<Flex
|
||||
gap="8"
|
||||
direction={{
|
||||
initial: 'column',
|
||||
md: 'row',
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
@@ -480,6 +487,18 @@ export function Characteristics() {
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{!exempt && (
|
||||
<Flex
|
||||
justify="center"
|
||||
display={{
|
||||
initial: 'flex',
|
||||
xs: 'none',
|
||||
}}
|
||||
>
|
||||
<Ad type={AdType.MediumRectangleHorizontalPurple} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="5">Maneuverability</Heading>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||
import { Ad, AdType } from '../../../../../../components/Ad';
|
||||
import { useAdExempt } from '../../../../../../hooks/useAdExempt';
|
||||
import { Characteristics } from '../Characteristics';
|
||||
import { Consumables } from '../Characteristics/components/Consumables';
|
||||
import { Equipment } from '../Characteristics/components/Equipment';
|
||||
@@ -9,22 +10,44 @@ import { Provisions } from '../Characteristics/components/Provisions';
|
||||
import { Skills } from '../Characteristics/components/Skills';
|
||||
|
||||
export function CharacteristicsSection() {
|
||||
const wideFormat = useWideFormat(880);
|
||||
const exempt = useAdExempt();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
mt={exempt ? '4' : '0'}
|
||||
// align="center"
|
||||
px="4"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
direction={wideFormat ? 'row' : 'column'}
|
||||
gap="6"
|
||||
>
|
||||
{!exempt && (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
display={{
|
||||
initial: 'none',
|
||||
lg: 'flex',
|
||||
}}
|
||||
pt={{
|
||||
initial: '9',
|
||||
md: '0',
|
||||
}}
|
||||
>
|
||||
<Ad type={AdType.WideSkyscraperVerticalPurple} />
|
||||
<Ad type={AdType.WideSkyscraperVerticalPurple} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
flexGrow="1"
|
||||
gap="6"
|
||||
style={{ width: '100%' }}
|
||||
direction={wideFormat ? 'row' : 'column'}
|
||||
justify="center"
|
||||
direction={{
|
||||
initial: 'column',
|
||||
sm: 'row',
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap="4">
|
||||
<Heading>Configuration</Heading>
|
||||
@@ -37,10 +60,11 @@ export function CharacteristicsSection() {
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
style={{
|
||||
flex: 1,
|
||||
maxWidth: wideFormat ? 640 : undefined,
|
||||
minWidth: 320,
|
||||
flexGrow="1"
|
||||
maxWidth={{
|
||||
initial: 'unset',
|
||||
xs: exempt ? undefined : '320px',
|
||||
md: '640px',
|
||||
}}
|
||||
direction="column"
|
||||
gap="4"
|
||||
@@ -49,6 +73,25 @@ export function CharacteristicsSection() {
|
||||
<Characteristics />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{!exempt && (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
height="100%"
|
||||
display={{
|
||||
initial: 'none',
|
||||
xs: 'flex',
|
||||
}}
|
||||
pt={{
|
||||
initial: '9',
|
||||
md: '0',
|
||||
}}
|
||||
>
|
||||
<Ad type={AdType.WideSkyscraperVerticalPurple} />
|
||||
<Ad type={AdType.WideSkyscraperVerticalPurple} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { MidSectionAd } from '../../../../components/MidSectionAd';
|
||||
import { AdMidSectionResponsive } from '../../../../components/AdMidSectionResponsive';
|
||||
import PageWrapper from '../../../../components/PageWrapper';
|
||||
import { assignDuelMember } from '../../../../core/blitzkit/assignDuelMember';
|
||||
import { useAdExempt } from '../../../../hooks/useAdExempt';
|
||||
import { mutateDuel, useDuel } from '../../../../stores/duel';
|
||||
import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
|
||||
import { HistorySection } from './components/HistorySection';
|
||||
@@ -15,6 +16,7 @@ import { VideoSection } from './components/VideoSection';
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const initialId = parseInt(params.id);
|
||||
const assigned = useDuel((state) => state.assigned);
|
||||
const exempt = useAdExempt();
|
||||
|
||||
useEffect(() => {
|
||||
assignDuelMember('both', initialId);
|
||||
@@ -75,14 +77,13 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
{assigned && (
|
||||
<>
|
||||
<HeroSection />
|
||||
<MidSectionAd />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
<CharacteristicsSection />
|
||||
<MidSectionAd />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
<TechTreeSection />
|
||||
<VideoSection />
|
||||
<MidSectionAd />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
<HistorySection />
|
||||
{/* <TankopediaSeparator /> */}
|
||||
</>
|
||||
)}
|
||||
</PageWrapper>
|
||||
|
||||
@@ -5,10 +5,12 @@ import { times } from 'lodash';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Ad, AdType } from '../../../components/Ad';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { useAdExempt } from '../../../hooks/useAdExempt';
|
||||
import { TankSearch } from './components/TankSearch';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const exempt = useAdExempt();
|
||||
|
||||
return (
|
||||
<PageWrapper size={1200} color="purple">
|
||||
@@ -18,11 +20,13 @@ export default function Page() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex justify="center" wrap="wrap">
|
||||
{times(2, (index) => (
|
||||
<Ad key={index} type={AdType.MediumRectangleHorizontalPurple} />
|
||||
))}
|
||||
</Flex>
|
||||
{!exempt && (
|
||||
<Flex justify="center" wrap="wrap">
|
||||
{times(2, (index) => (
|
||||
<Ad key={index} type={AdType.MediumRectangleHorizontalPurple} />
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+11
-12
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Box, BoxProps, Flex, Text } from '@radix-ui/themes';
|
||||
import { Box, BoxProps, Text } from '@radix-ui/themes';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Vector2Tuple } from 'three';
|
||||
@@ -51,19 +51,18 @@ export function Ad({ type, style, ...props }: AdProps) {
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Flex
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
style={{ transform: 'translate(-50%, -50%)' }}
|
||||
direction="column"
|
||||
<Text
|
||||
color="gray"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
align="center"
|
||||
>
|
||||
<Text color="gray">Advertisement</Text>
|
||||
<Text size="1" color="gray">
|
||||
{dimensions[0]}x{dimensions[1]}
|
||||
</Text>
|
||||
</Flex>
|
||||
Advertisement
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
width={`${dimensions[0]}px`}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Flex, FlexProps } from '@radix-ui/themes';
|
||||
import { Ad, AdType } from './Ad';
|
||||
|
||||
export function AdMidSectionNarrowOnly(props: FlexProps) {
|
||||
return (
|
||||
<Flex
|
||||
justify="center"
|
||||
py="4"
|
||||
gap="4"
|
||||
display={{
|
||||
initial: 'flex',
|
||||
sm: 'none',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Ad type={AdType.MediumRectangleHorizontalPurple} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex, FlexProps } from '@radix-ui/themes';
|
||||
import { Ad, AdType } from './Ad';
|
||||
|
||||
export function MidSectionAd(props: FlexProps) {
|
||||
export function AdMidSectionResponsive(props: FlexProps) {
|
||||
return (
|
||||
<Flex justify="center" py="4" gap="4" {...props}>
|
||||
<Ad
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Flex, FlexProps } from '@radix-ui/themes';
|
||||
import { Ad, AdType } from './Ad';
|
||||
|
||||
export function AdMidSectionWideOnly(props: FlexProps) {
|
||||
return (
|
||||
<Flex
|
||||
justify="center"
|
||||
py="4"
|
||||
gap="4"
|
||||
display={{
|
||||
initial: 'none',
|
||||
sm: 'flex',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Ad type={AdType.LeaderboardHorizontalPurple} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,13 @@ export function Link({ href, onClick, ...props }: LinkProps) {
|
||||
href={href}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if (href) router.push(href);
|
||||
if (href) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(href);
|
||||
} else {
|
||||
router.push(href);
|
||||
}
|
||||
}
|
||||
onClick?.(event);
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ExitIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, IconButton, Text } from '@radix-ui/themes';
|
||||
import { use } from 'react';
|
||||
import { getAccountInfo } from '../../../core/blitz/getAccountInfo';
|
||||
import { idToRegion } from '../../../core/blitz/idToRegion';
|
||||
import { logout } from '../../../core/blitz/logout';
|
||||
import { useApp } from '../../../stores/app';
|
||||
|
||||
export function LoggedIn() {
|
||||
const login = useApp((state) => state.login!);
|
||||
const accountInfo = use(
|
||||
getAccountInfo(idToRegion(login.id), login.id, [], {
|
||||
access_token: login.token,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap="3" align="center">
|
||||
<Text>Hello, {accountInfo.nickname}</Text>
|
||||
|
||||
<IconButton variant="ghost" color="red" onClick={logout}>
|
||||
<ExitIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+118
-59
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Cross1Icon,
|
||||
DiscordLogoIcon,
|
||||
@@ -10,31 +12,36 @@ import {
|
||||
Button,
|
||||
Dialog,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Inset,
|
||||
Popover,
|
||||
Spinner,
|
||||
Separator,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Suspense, useState } from 'react';
|
||||
import { REGIONS, UNLOCALIZED_REGION_NAMES } from '../../constants/regions';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { REGIONS } from '../../constants/regions';
|
||||
import { TOOLS } from '../../constants/tools';
|
||||
import { authURL } from '../../core/blitz/authURL';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
import { logoutPatreon } from '../../core/blitz/logoutPatreon';
|
||||
import { logoutWargaming } from '../../core/blitz/logoutWargaming';
|
||||
import { patreonLoginUrl } from '../../core/blitzkit/patreonLoginUrl';
|
||||
import { BlitzkitWide } from '../../icons/BlitzkitWide';
|
||||
import { PatreonIcon } from '../../icons/Patreon';
|
||||
import { WargamingIcon } from '../../icons/Wargaming';
|
||||
import strings from '../../lang/en-US.json';
|
||||
import { theme } from '../../stitches.config';
|
||||
import { useApp } from '../../stores/app';
|
||||
import { Link } from '../Link';
|
||||
import { LoggedIn } from './components/LoggedIn';
|
||||
import * as styles from './index.css';
|
||||
|
||||
export const NAVBAR_HEIGHT = 64;
|
||||
|
||||
export default function Navbar() {
|
||||
const router = useRouter();
|
||||
const [showHamburgerMenu, setShowHamburgerMenu] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const login = useApp((state) => state.login!);
|
||||
const logins = useApp((state) => state.logins);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -120,66 +127,118 @@ export default function Navbar() {
|
||||
<PatreonIcon width={14} height={14} />
|
||||
</Link>
|
||||
|
||||
<Link href="/settings" onClick={() => setShowHamburgerMenu(false)}>
|
||||
<Link
|
||||
href="/settings"
|
||||
onClick={() => setShowHamburgerMenu(false)}
|
||||
color="gray"
|
||||
highContrast
|
||||
>
|
||||
<Flex style={{ width: '100%', height: '100%' }} justify="center">
|
||||
<IconButton size="4" variant="ghost" color="gray">
|
||||
<GearIcon />
|
||||
</IconButton>
|
||||
<GearIcon />
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
{login && (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton size="4" variant="ghost" color="gray">
|
||||
<PersonIcon />
|
||||
</IconButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<PersonIcon style={{ cursor: 'pointer' }} />
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content align="center">
|
||||
<Popover.Content align="end" width="320px">
|
||||
{(logins.patreon || logins.wargaming) && (
|
||||
<Flex direction="column" gap="2">
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LoggedIn />
|
||||
</Suspense>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
)}
|
||||
<Text align="center" color="gray">
|
||||
Logged in accounts
|
||||
</Text>
|
||||
|
||||
{!login && false && (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="ghost">Log in</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content
|
||||
style={{
|
||||
maxWidth: 360,
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" gap="2" align="center">
|
||||
<Heading size="3">Choose your region</Heading>
|
||||
|
||||
<Flex gap="4" align="center" justify="center" wrap="wrap">
|
||||
{REGIONS.map((region) => (
|
||||
<Link
|
||||
onClick={() => setShowHamburgerMenu(false)}
|
||||
key={region}
|
||||
href={authURL(
|
||||
region,
|
||||
typeof window === 'undefined'
|
||||
? undefined
|
||||
: location.href,
|
||||
)}
|
||||
{logins.patreon && (
|
||||
<Flex align="center" gap="2" justify="center">
|
||||
<PatreonIcon width={15} height={15} />
|
||||
<Text>Patreon</Text>
|
||||
<Button
|
||||
color="red"
|
||||
variant="ghost"
|
||||
onClick={logoutPatreon}
|
||||
>
|
||||
{UNLOCALIZED_REGION_NAMES[region]}
|
||||
</Link>
|
||||
))}
|
||||
</Flex>
|
||||
Logout
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{logins.wargaming && (
|
||||
<Flex align="center" gap="2" justify="center">
|
||||
<WargamingIcon width={15} height={15} />
|
||||
<Text>Wargaming</Text>
|
||||
<Button
|
||||
color="red"
|
||||
variant="ghost"
|
||||
onClick={logoutWargaming}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!logins.patreon !== !logins.wargaming && (
|
||||
<Inset side="x">
|
||||
<Separator my="4" size="4" />
|
||||
</Inset>
|
||||
)}
|
||||
|
||||
{(!logins.patreon || !logins.wargaming) && (
|
||||
<Flex direction="column" gap="2">
|
||||
<Text align="center" color="gray">
|
||||
Log in with...
|
||||
</Text>
|
||||
|
||||
{!logins.patreon && (
|
||||
<Link style={{ width: '100%' }} href={patreonLoginUrl()}>
|
||||
<Button style={{ width: '100%' }} color="tomato">
|
||||
<PatreonIcon width={15} height={15} /> Patreon
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!logins.wargaming && (
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button color="red">
|
||||
<WargamingIcon width={15} height={15} /> Wargaming
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content width="fit-content">
|
||||
<Flex direction="column" gap="4" align="center">
|
||||
<Text color="gray">Choose your region</Text>
|
||||
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{REGIONS.map((region) => (
|
||||
<Dialog.Close key={region}>
|
||||
<Link
|
||||
href={
|
||||
typeof window !== 'undefined'
|
||||
? `https://api.worldoftanks.${region}/wot/auth/login/?application_id=${WARGAMING_APPLICATION_ID}&redirect_uri=${encodeURIComponent(
|
||||
`${location.origin}/auth/wargaming?return=${location.origin}${location.pathname}`,
|
||||
)}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Button color="red">
|
||||
{strings.common.regions.normal[region]}
|
||||
</Button>
|
||||
</Link>
|
||||
</Dialog.Close>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
||||
+85
-85
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@radix-ui/themes';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
interface Tool {
|
||||
export interface Tool {
|
||||
id: string;
|
||||
image: string;
|
||||
title: string;
|
||||
@@ -16,89 +16,89 @@ interface Tool {
|
||||
significant?: boolean;
|
||||
}
|
||||
|
||||
export const tankopediaTool: Tool = {
|
||||
id: 'tankopedia',
|
||||
title: 'Tankopedia',
|
||||
description: 'Blitz tank encyclopedia',
|
||||
pageDescription:
|
||||
'Statistics, armor, and more for all tanks in World of Tanks Blitz',
|
||||
image: 'D8j6GZx',
|
||||
button: {
|
||||
text: 'Find your tank',
|
||||
color: 'purple',
|
||||
},
|
||||
significant: true,
|
||||
};
|
||||
|
||||
export const ratingTool: Tool = {
|
||||
id: 'rating',
|
||||
title: 'Rating',
|
||||
description: 'Full rating leaderboard',
|
||||
pageDescription:
|
||||
'Live and archived full rating leaderboard for World of Tanks Blitz',
|
||||
image: 'WEyMZH3',
|
||||
button: {
|
||||
text: 'View leaderboard',
|
||||
color: 'orange',
|
||||
},
|
||||
};
|
||||
|
||||
export const compareTool: Tool = {
|
||||
id: 'compare',
|
||||
title: 'Compare',
|
||||
description: 'Compare tank statistics',
|
||||
pageDescription:
|
||||
'Compare tanks statistics and loadouts in World of Tanks Blitz',
|
||||
image: 'O6VNl6e',
|
||||
button: {
|
||||
text: 'Compare tanks',
|
||||
color: 'crimson',
|
||||
},
|
||||
};
|
||||
|
||||
export const sessionTool: Tool = {
|
||||
// TODO: rename to tracker
|
||||
id: 'session',
|
||||
title: 'Session',
|
||||
description: 'Live session stats',
|
||||
pageDescription:
|
||||
'Track your stats in real time as you play World of Tanks Blitz',
|
||||
image: 'HdG9sTf',
|
||||
button: {
|
||||
text: 'Start tracking',
|
||||
color: 'blue',
|
||||
},
|
||||
};
|
||||
|
||||
export const discordTool: Tool = {
|
||||
id: 'discord',
|
||||
title: 'Discord bot',
|
||||
description: 'Stats right in Discord',
|
||||
href: 'https://discord.com/application-directory/1097673957865443370',
|
||||
image: '0bWE5hC',
|
||||
button: {
|
||||
text: 'Install now',
|
||||
color: 'indigo',
|
||||
},
|
||||
};
|
||||
|
||||
export const moreTool: Tool = {
|
||||
id: 'more',
|
||||
title: 'More coming soon',
|
||||
description: "What we're making",
|
||||
href: 'https://discord.gg/nDt7AjGJQH',
|
||||
image: '1nPm6VI',
|
||||
button: {
|
||||
text: 'Join Discord',
|
||||
color: 'indigo',
|
||||
},
|
||||
};
|
||||
|
||||
export const TOOLS: Tool[] = [
|
||||
{
|
||||
id: 'tankopedia',
|
||||
title: 'Tankopedia',
|
||||
description: 'Blitz tank encyclopedia',
|
||||
pageDescription:
|
||||
'Statistics, armor, and more for all tanks in World of Tanks Blitz',
|
||||
image: 'D8j6GZx',
|
||||
button: {
|
||||
text: 'Find your tank',
|
||||
color: 'purple',
|
||||
},
|
||||
significant: true,
|
||||
},
|
||||
{
|
||||
id: 'player-stats',
|
||||
title: 'Player stats',
|
||||
description: 'Periodical player statistics',
|
||||
pageDescription:
|
||||
'Periodical player statistics and tanks played in World of Tanks Blitz',
|
||||
image: '8gJGkKP',
|
||||
button: {
|
||||
text: 'Search players',
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rating',
|
||||
title: 'Rating',
|
||||
description: 'Full rating leaderboard',
|
||||
pageDescription:
|
||||
'Live and archived full rating leaderboard for World of Tanks Blitz',
|
||||
image: 'WEyMZH3',
|
||||
button: {
|
||||
text: 'View leaderboard',
|
||||
color: 'orange',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'compare',
|
||||
title: 'Compare',
|
||||
description: 'Compare tank statistics',
|
||||
pageDescription:
|
||||
'Compare tanks statistics and loadouts in World of Tanks Blitz',
|
||||
image: 'O6VNl6e',
|
||||
button: {
|
||||
text: 'Compare tanks',
|
||||
color: 'crimson',
|
||||
},
|
||||
},
|
||||
{
|
||||
// TODO: rename to tracker
|
||||
id: 'session',
|
||||
title: 'Session',
|
||||
description: 'Live session stats',
|
||||
pageDescription:
|
||||
'Track your stats in real time as you play World of Tanks Blitz',
|
||||
image: 'HdG9sTf',
|
||||
button: {
|
||||
text: 'Start tracking',
|
||||
color: 'blue',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'discord',
|
||||
title: 'Discord bot',
|
||||
description: 'Stats right in Discord',
|
||||
href: 'https://discord.com/application-directory/1097673957865443370',
|
||||
image: '0bWE5hC',
|
||||
button: {
|
||||
text: 'Install now',
|
||||
color: 'indigo',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'more',
|
||||
title: 'More coming soon',
|
||||
description: "What we're making",
|
||||
href: 'https://discord.gg/nDt7AjGJQH',
|
||||
image: '1nPm6VI',
|
||||
button: {
|
||||
text: 'Join Discord',
|
||||
color: 'indigo',
|
||||
},
|
||||
},
|
||||
tankopediaTool,
|
||||
ratingTool,
|
||||
compareTool,
|
||||
sessionTool,
|
||||
discordTool,
|
||||
moreTool,
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Region } from '../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
|
||||
|
||||
export function authURL(region: Region, callback?: string) {
|
||||
if (typeof window === 'undefined') throw new Error('window is undefined');
|
||||
|
||||
return `https://api.worldoftanks.${region}/wot/auth/login/?application_id=${WARGAMING_APPLICATION_ID}&redirect_uri=${encodeURIComponent(
|
||||
`${window.location.origin}/callback/wargaming/${callback ? `?return=${location.href}` : ''}`,
|
||||
)}`;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PatreonAuthResponse } from '../../app/auth/[provider]/page';
|
||||
import { mutateApp, useApp } from '../../stores/app';
|
||||
|
||||
export async function extendAuthPatreon() {
|
||||
const { patreon } = useApp.getState().logins;
|
||||
|
||||
if (!patreon) return;
|
||||
|
||||
const data = (await fetch(
|
||||
`/api/patreon/refresh/${patreon.refreshToken}`,
|
||||
).then((response) => response.json())) as PatreonAuthResponse;
|
||||
|
||||
mutateApp((draft) => {
|
||||
draft.logins.patreon = {
|
||||
token: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expires: Date.now() + data.expires_in * 1000,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -10,10 +10,10 @@ interface Extension {
|
||||
};
|
||||
}
|
||||
|
||||
export async function extendAuth() {
|
||||
const { login } = useApp.getState();
|
||||
export async function extendAuthWargaming() {
|
||||
const { wargaming } = useApp.getState().logins;
|
||||
|
||||
if (!login) return;
|
||||
if (!wargaming) return;
|
||||
|
||||
// error handling? what's that?
|
||||
/*
|
||||
@@ -42,18 +42,18 @@ export async function extendAuth() {
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.worldoftanks.${idToRegion(
|
||||
login.id,
|
||||
wargaming.id,
|
||||
)}/wot/auth/prolongate/?application_id=${WARGAMING_APPLICATION_ID}&access_token=${
|
||||
login.token
|
||||
wargaming.token
|
||||
}`,
|
||||
);
|
||||
const json = (await response.json()) as Extension;
|
||||
|
||||
mutateApp((draft) => {
|
||||
draft.login = {
|
||||
id: login.id,
|
||||
draft.logins.wargaming = {
|
||||
id: wargaming.id,
|
||||
token: json.data.access_token,
|
||||
expiresAt: json.data.expires_at,
|
||||
expires: json.data.expires_at * 1000,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mutateApp } from '../../stores/app';
|
||||
|
||||
export function logout() {
|
||||
export function logoutPatreon() {
|
||||
mutateApp((draft) => {
|
||||
draft.login = undefined;
|
||||
draft.logins.patreon = undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mutateApp } from '../../stores/app';
|
||||
|
||||
export function logoutWargaming() {
|
||||
mutateApp((draft) => {
|
||||
draft.logins.wargaming = undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function patreonLoginUrl() {
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: process.env.NEXT_PUBLIC_PATREON_CLIENT_ID as string,
|
||||
redirect_uri: process.env.NEXT_PUBLIC_PATREON_REDIRECT_URI as string,
|
||||
scope: 'identity',
|
||||
});
|
||||
|
||||
return `https://www.patreon.com/oauth2/authorize?${params}`;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { use } from 'react';
|
||||
import { useApp } from '../stores/app';
|
||||
|
||||
export function useAdExempt() {
|
||||
const patreon = useApp((state) => state.logins.patreon);
|
||||
|
||||
if (!patreon) return false;
|
||||
|
||||
const json = use(
|
||||
fetch(`/api/patreon/membership/${patreon.token}`, {
|
||||
cache: 'force-cache',
|
||||
}).then((response) => response.json()),
|
||||
);
|
||||
|
||||
return typeof json === 'boolean' && json;
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export function Blitzkit(props: ComponentProps<'svg'>) {
|
||||
fill="url(#paint4_linear_513_245)"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M213.905 307L91 219.714L137.393 186.767C150.906 177.17 164.124 171.539 177.048 169.875C189.971 168.211 201.919 171.275 212.893 179.069C219.702 183.905 223.718 189.006 224.941 194.372C226.165 199.658 225.302 204.506 222.354 208.918L222.692 209.158C229.978 206.301 237.935 205.286 246.563 206.113C255.247 206.9 263.613 210.151 271.66 215.866C282.803 223.779 287.398 232.825 285.446 243.004C283.551 253.142 275.96 262.929 262.672 272.366L213.905 307ZM242.999 260.08L221.456 275.38L184.905 249.422L206.448 234.123C213.007 229.465 219.473 227.23 225.846 227.42C232.22 227.529 238.193 229.562 243.764 233.519C249.392 237.516 252.31 241.797 252.521 246.364C252.732 250.85 249.558 255.422 242.999 260.08ZM186.937 223.64L167.77 237.252L135.693 214.472L154.861 200.859C161.363 196.241 167.435 193.887 173.076 193.798C178.717 193.708 184.07 195.462 189.135 199.059C194.256 202.696 196.809 206.557 196.796 210.643C196.726 214.69 193.44 219.022 186.937 223.64Z"
|
||||
fill="url(#paint5_linear_513_245)"
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function BlitzkitButtonWatermark(props: ComponentProps<'svg'>) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g opacity="0.1" clip-path="url(#clip0_513_262)">
|
||||
<g opacity="0.1" clipPath="url(#clip0_513_262)">
|
||||
<path
|
||||
d="M416.628 244.997L235.625 372.974C223.13 381.809 202.87 381.809 190.375 372.974L9.37173 244.997C3.1239 240.58 -3.95076e-06 234.79 0 229V196.6L213 54.5L426 196.6V229C426 234.79 422.876 240.58 416.628 244.997Z"
|
||||
fill="#8D8D8D"
|
||||
@@ -28,8 +28,8 @@ export function BlitzkitButtonWatermark(props: ComponentProps<'svg'>) {
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M416.628 180.603L235.625 52.6262C223.13 43.7913 202.87 43.7913 190.375 52.6262L9.37172 180.603C-3.12391 189.438 -3.12391 203.762 9.37173 212.597L190.375 340.574C202.87 349.409 223.13 349.409 235.625 340.574L416.628 212.597C429.124 203.762 429.124 189.438 416.628 180.603ZM218.47 76.4488L232.267 101.781L239 96.9786L224.094 71.0312L219.844 68L198.721 83.0665L203.957 86.8006L218.47 76.4488ZM184.681 106.963C181.602 104.766 180.042 102.326 180 99.6413C179.973 96.947 181.629 94.409 184.968 92.0273C188.143 89.7627 191.804 88.6061 195.95 88.5574C200.097 88.5088 204.484 90.1343 209.11 93.4339L209.91 94.005C214.92 97.578 217.431 101.507 217.445 105.793C217.459 110.059 214.606 114.3 208.886 118.517L208.208 119L203.24 115.456L203.856 115.017C207.537 112.391 209.494 109.697 209.727 106.934C209.886 104.918 208.879 102.845 206.707 100.715C206.753 100.859 206.794 101.004 206.832 101.151C207.434 103.493 206.38 105.631 203.671 107.564C200.852 109.575 197.731 110.512 194.31 110.375C190.888 110.238 187.679 109.101 184.681 106.963ZM311.596 231.945L218.056 168.108L238.292 153.762L286.867 185.722L284.968 151.871L307.355 136L309.049 176.111L380.102 183.378L356.236 200.298L309.86 195.31L310.153 202.23L331.832 217.599L311.596 231.945ZM91 219.714L213.905 307L262.672 272.366C275.96 262.929 283.551 253.142 285.446 243.003C287.398 232.825 282.803 223.779 271.66 215.866C263.613 210.151 255.247 206.9 246.563 206.113C237.935 205.286 229.978 206.301 222.692 209.158L222.354 208.918C225.302 204.506 226.165 199.658 224.941 194.372C223.718 189.006 219.702 183.905 212.893 179.069C201.919 171.275 189.971 168.211 177.048 169.875C164.124 171.539 150.906 177.17 137.393 186.767L91 219.714Z"
|
||||
fill="white"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export function WargamingIcon(props: ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_574_231)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.46409 0.156338C8.91652 0.149247 10.3384 0.57301 11.55 1.37404C12.7616 2.17507 13.7085 3.31739 14.2709 4.65654C14.8332 5.9957 14.9859 7.47154 14.7095 8.89745C14.4331 10.3234 13.7401 11.6353 12.7181 12.6673C11.6961 13.6994 10.391 14.4052 8.96789 14.6955C7.54475 14.9859 6.06748 14.8477 4.72288 14.2984C3.37829 13.7492 2.22676 12.8136 1.41391 11.6099C0.601062 10.4062 0.163403 8.98846 0.156278 7.53603V7.50009C0.150882 5.5586 0.916644 3.69442 2.28523 2.31733C3.65382 0.940243 5.51323 0.162958 7.45472 0.156338H7.46409ZM7.46409 1.56259C8.76962 1.56799 10.0372 2.00218 11.0719 2.79837C12.1065 3.59456 12.8509 4.70863 13.1906 5.96922C13.5302 7.2298 13.4463 8.56706 12.9517 9.77529C12.4571 10.9835 11.5792 11.9958 10.4532 12.6563C10.8305 11.3103 10.9857 9.91172 10.9125 8.51571C11.2938 8.62353 11.5094 8.68446 11.8922 8.8079C11.47 7.70829 10.9518 6.64795 10.3438 5.63915C9.79373 6.67527 9.10279 7.63018 8.29065 8.47665L9.51565 8.40009C8.46097 10.2938 6.97347 11.2126 5.08909 11.4798C4.6594 9.33759 4.98128 5.85478 7.38597 4.21728L7.62972 5.38603C8.09847 4.63134 9.02503 3.6329 9.71409 2.93759C8.57965 2.49502 7.34402 2.37866 6.1469 2.60165C6.34534 2.77196 6.59065 2.98759 6.78909 3.15634C4.07815 4.3079 2.58909 6.20009 1.6094 8.40009C1.48639 7.55586 1.54531 6.69509 1.78221 5.87549C2.0191 5.0559 2.4285 4.29643 2.98295 3.64802C3.5374 2.99961 4.22409 2.47724 4.99695 2.11595C5.76982 1.75465 6.611 1.56279 7.46409 1.55321V1.56259Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_574_231">
|
||||
<rect width="15" height="15" fill="currentColor" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+13
-3
@@ -3,16 +3,25 @@ import { merge } from 'lodash';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface Login {
|
||||
interface WargamingLogin {
|
||||
id: number;
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
interface PatreonLogin {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
devBuildAgreementTime: number;
|
||||
developerMode: boolean;
|
||||
login?: Login;
|
||||
logins: {
|
||||
wargaming?: WargamingLogin;
|
||||
patreon?: PatreonLogin;
|
||||
};
|
||||
policiesAgreementIndex: number;
|
||||
}
|
||||
|
||||
@@ -24,6 +33,7 @@ export const useApp = create<App>()(
|
||||
devBuildAgreementTime: 0,
|
||||
developerMode: false,
|
||||
policiesAgreementIndex: -1,
|
||||
logins: {},
|
||||
}),
|
||||
{ name: 'app', merge: (a, b) => merge(b, a) },
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user