Merge branch 'main' into dev

This commit is contained in:
TrèsAbhi committed 2024-06-15 21:09:37 -05:00
commit 2d6b8105d2
33 files changed
+916 -390

No files matched your search

+17
View File
@@ -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 (
+28 -10
View File
@@ -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();
}
}
});
+47
View File
@@ -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>
);
}
+142
View File
@@ -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
View File
@@ -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>
);
}
+6 -5
View File
@@ -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>
+9 -5
View File
@@ -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>
);
}