mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: more login progress
This commit is contained in:
1 parent
1e64cb9e7d
commit
513105a624
18 files changed
+290
-295
No files matched your search
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import { idToRegion } from '@blitzkit/core';
|
||||
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { PatreonAuthResponse } from '../pages/auth/[provider]/_index';
|
||||
import { App } from '../stores/app';
|
||||
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../stores/app/constants';
|
||||
|
||||
type Extension =
|
||||
| {
|
||||
status: 'ok';
|
||||
data: {
|
||||
access_token: string;
|
||||
account_id: number;
|
||||
expires_at: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
status: 'error';
|
||||
};
|
||||
|
||||
export function Checks() {
|
||||
return (
|
||||
<App.Provider>
|
||||
<Content />
|
||||
</App.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wargaming: 14 (refresh: 7)
|
||||
* Patreon: 31 (refresh: 15)
|
||||
*/
|
||||
function Content() {
|
||||
const logins = App.use((state) => state.logins);
|
||||
const mutateApp = App.useMutation();
|
||||
const appStore = App.useStore();
|
||||
const policiesAgreementIndex = App.use(
|
||||
(state) => state.policiesAgreementIndex,
|
||||
);
|
||||
const isLegal = window.location.pathname.startsWith('/legal');
|
||||
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function unsubscribe() {
|
||||
window.removeEventListener('scroll', show);
|
||||
window.removeEventListener('pointermove', show);
|
||||
window.removeEventListener('pointerdown', show);
|
||||
}
|
||||
|
||||
function show() {
|
||||
setShowPoliciesAgreement(true);
|
||||
unsubscribe();
|
||||
}
|
||||
|
||||
if (
|
||||
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
||||
!isLegal
|
||||
) {
|
||||
window.addEventListener('scroll', show);
|
||||
window.addEventListener('pointermove', show);
|
||||
window.addEventListener('pointerdown', show);
|
||||
}
|
||||
|
||||
return unsubscribe;
|
||||
}, [policiesAgreementIndex, isLegal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (logins.wargaming) {
|
||||
const expiresIn = logins.wargaming.expires - Date.now();
|
||||
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||
|
||||
if (expiresInDays < 0) {
|
||||
mutateApp((draft) => {
|
||||
draft.logins.wargaming = undefined;
|
||||
});
|
||||
} else if (expiresInDays < 7) {
|
||||
const { wargaming } = appStore.getState().logins;
|
||||
|
||||
if (!wargaming) return;
|
||||
|
||||
fetch(
|
||||
`https://api.worldoftanks.${idToRegion(
|
||||
wargaming.id,
|
||||
)}/wot/auth/prolongate/?application_id=${
|
||||
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
|
||||
}&access_token=${wargaming.token}`,
|
||||
)
|
||||
.then((response) => response.json() as Promise<Extension>)
|
||||
.then((json) => {
|
||||
mutateApp((draft) => {
|
||||
if (json.status === 'error') {
|
||||
draft.logins.wargaming = undefined;
|
||||
} else {
|
||||
draft.logins.wargaming = {
|
||||
id: wargaming.id,
|
||||
token: json.data.access_token,
|
||||
expires: json.data.expires_at * 1000,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (logins.patreon) {
|
||||
const expiresIn = logins.patreon.expires - Date.now();
|
||||
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||
|
||||
if (expiresInDays < 0) {
|
||||
mutateApp((draft) => {
|
||||
draft.logins.patreon = undefined;
|
||||
});
|
||||
} else if (expiresInDays < 15) {
|
||||
const { patreon } = appStore.getState().logins;
|
||||
|
||||
if (!patreon) return;
|
||||
|
||||
fetch(`/api/patreon/refresh/${patreon.refreshToken}`)
|
||||
.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,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!showPoliciesAgreement) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="end"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'var(--color-overlay)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
p="8"
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'var(--color-panel-solid)',
|
||||
}}
|
||||
justify="center"
|
||||
>
|
||||
<Flex
|
||||
align="start"
|
||||
gap="4"
|
||||
style={{
|
||||
maxWidth: 640 * 2,
|
||||
}}
|
||||
direction="column"
|
||||
>
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading>
|
||||
{policiesAgreementIndex === -1
|
||||
? "BlitzKit's policies"
|
||||
: "BlitzKit's policies have changed"}
|
||||
</Heading>
|
||||
|
||||
<Text>
|
||||
This website utilizes cookies to perform analytics and personalize
|
||||
your experience. You can learn more through{' '}
|
||||
<Link href="/docs/legal/privacy-policy">our privacy policy</Link>.
|
||||
You may opt out of personalized advertizement by visiting{' '}
|
||||
<Link href="https://optout.aboutads.info/">WebChoices</Link>. By
|
||||
using BlitzKit, you also agree to our{' '}
|
||||
<Link href="/docs/legal/terms-of-service">terms of service</Link>.
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
mt="2"
|
||||
onClick={() => {
|
||||
appStore.setState({
|
||||
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
|
||||
});
|
||||
setShowPoliciesAgreement(false);
|
||||
}}
|
||||
>
|
||||
I agree
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { patreonLoginUrl } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { PersonIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Button,
|
||||
@@ -14,8 +13,6 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { Suspense } from 'react';
|
||||
import { App } from '../../stores/app';
|
||||
import { $patreonLogin } from '../../stores/patreonLogin';
|
||||
import { $wargamingLogin } from '../../stores/wargamingLogin';
|
||||
import { BlitzKitTheme } from '../BlitzKitTheme';
|
||||
import { PatreonIcon } from '../PatreonIcon';
|
||||
import { WargamingAccountName } from '../WargamingAccountName';
|
||||
@@ -23,8 +20,16 @@ import { WargamingIcon } from '../WargamingIcon';
|
||||
import { WargamingLoginButton } from '../WargamingLoginButton';
|
||||
|
||||
export function NavbarAccount() {
|
||||
const patreon = useStore($patreonLogin);
|
||||
const wargaming = useStore($wargamingLogin);
|
||||
return (
|
||||
<App.Provider>
|
||||
<Content />
|
||||
</App.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const logins = App.use((state) => state.logins);
|
||||
const mutateApp = App.useMutation();
|
||||
|
||||
return (
|
||||
<App.Provider>
|
||||
@@ -41,13 +46,13 @@ export function NavbarAccount() {
|
||||
width="320px"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
{(patreon.token || wargaming.token) && (
|
||||
{(logins.patreon || logins.wargaming) && (
|
||||
<Flex direction="column" gap="2">
|
||||
<Text align="center" color="gray">
|
||||
Logged in accounts
|
||||
</Text>
|
||||
|
||||
{patreon.token && (
|
||||
{logins.patreon && (
|
||||
<Flex align="center" gap="2" justify="center">
|
||||
<PatreonIcon width={15} height={15} />
|
||||
<Text>Patreon</Text>
|
||||
@@ -55,7 +60,9 @@ export function NavbarAccount() {
|
||||
color="red"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
$patreonLogin.setKey('token', undefined);
|
||||
mutateApp((draft) => {
|
||||
draft.logins.patreon = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
@@ -63,7 +70,7 @@ export function NavbarAccount() {
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{wargaming.token && (
|
||||
{logins.wargaming && (
|
||||
<Flex align="center" gap="2" justify="center">
|
||||
<WargamingIcon width={15} height={15} />
|
||||
<Text>
|
||||
@@ -77,7 +84,9 @@ export function NavbarAccount() {
|
||||
color="red"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
$wargamingLogin.setKey('token', undefined);
|
||||
mutateApp((draft) => {
|
||||
draft.logins.wargaming = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
@@ -87,19 +96,19 @@ export function NavbarAccount() {
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!patreon.token !== !wargaming.token && (
|
||||
{!logins.patreon !== !logins.wargaming && (
|
||||
<Inset side="x">
|
||||
<Separator my="4" size="4" />
|
||||
</Inset>
|
||||
)}
|
||||
|
||||
{(!patreon.token || !wargaming.token) && (
|
||||
{(!logins.patreon || !logins.wargaming) && (
|
||||
<Flex direction="column" gap="2">
|
||||
<Text align="center" color="gray">
|
||||
Log in with...
|
||||
</Text>
|
||||
|
||||
{!patreon.token && (
|
||||
{!logins.patreon && (
|
||||
<Link style={{ width: '100%' }} href={patreonLoginUrl()}>
|
||||
<Button style={{ width: '100%' }} color="tomato">
|
||||
<PatreonIcon width={15} height={15} /> Patreon
|
||||
@@ -107,7 +116,7 @@ export function NavbarAccount() {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!wargaming.token && <WargamingLoginButton />}
|
||||
{!logins.wargaming && <WargamingLoginButton />}
|
||||
</Flex>
|
||||
)}
|
||||
</Popover.Content>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { assertSecret, patreonLoginUrl } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { ArrowRightIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Badge,
|
||||
@@ -11,12 +10,20 @@ import {
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { useAdExempt } from '../hooks/useAdExempt';
|
||||
import { $patreonLogin } from '../stores/patreonLogin';
|
||||
import { App } from '../stores/app';
|
||||
import { PatreonIcon } from './PatreonIcon';
|
||||
|
||||
export function Plugs() {
|
||||
return (
|
||||
<App.Provider>
|
||||
<Content />
|
||||
</App.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const exempt = useAdExempt();
|
||||
const hasPatreon = useStore($patreonLogin).token !== undefined;
|
||||
const hasPatreon = App.use((state) => state.logins.patreon !== undefined);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
||||
+4
-3
@@ -9,11 +9,12 @@ import {
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { App } from '../../../../../../stores/app';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { $wargamingLogin } from '../../../../../../stores/wargamingLogin';
|
||||
import { Stars } from '../../Stars';
|
||||
|
||||
export function VoteCaster() {
|
||||
const appStore = App.useStore();
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const [easiness, setEasiness] = useState<StarsInt | null>(null);
|
||||
const [firepower, setFirepower] = useState<StarsInt | null>(null);
|
||||
@@ -97,9 +98,9 @@ export function VoteCaster() {
|
||||
survivability === null)
|
||||
}
|
||||
onClick={() => {
|
||||
const wargaming = $wargamingLogin.get();
|
||||
const { wargaming } = appStore.getState().logins;
|
||||
|
||||
if (!wargaming.token) {
|
||||
if (!wargaming) {
|
||||
return setShowDialogue(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { formatCompact } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { memo } from 'react';
|
||||
import { useTankVotes } from '../../../../../hooks/useTankVotes';
|
||||
import { App } from '../../../../../stores/app';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { $wargamingLogin } from '../../../../../stores/wargamingLogin';
|
||||
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
|
||||
import { StarRow } from './components/StarRow';
|
||||
import { VoteCaster } from './components/VoteCaster';
|
||||
@@ -13,7 +12,7 @@ export const Votes = memo(
|
||||
() => {
|
||||
const id = Duel.use((state) => state.protagonist.tank.id);
|
||||
const votes = useTankVotes(id);
|
||||
const wargaming = useStore($wargamingLogin);
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="3">
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { getAccountInfo, idToRegion } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Text } from '@radix-ui/themes';
|
||||
import { useMemo } from 'react';
|
||||
import usePromise from 'react-promise-suspense';
|
||||
import { $wargamingLogin } from '../stores/wargamingLogin';
|
||||
import { App } from '../stores/app';
|
||||
|
||||
export function WargamingAccountName() {
|
||||
const wargaming = useStore($wargamingLogin);
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
const promise = useMemo(async () => {
|
||||
if (!wargaming.token) return null;
|
||||
if (!wargaming) return null;
|
||||
|
||||
const data = await getAccountInfo(
|
||||
idToRegion(Number(wargaming.id)),
|
||||
Number(wargaming.id),
|
||||
idToRegion(wargaming.id),
|
||||
wargaming.id,
|
||||
[],
|
||||
{
|
||||
access_token: wargaming.token,
|
||||
},
|
||||
{ access_token: wargaming.token },
|
||||
);
|
||||
|
||||
return data?.nickname as string | undefined;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { $patreonLogin } from '../stores/patreonLogin';
|
||||
import { App } from '../stores/app';
|
||||
|
||||
let cache: Record<string, boolean | Promise<boolean>> = {};
|
||||
|
||||
export function useAdExempt() {
|
||||
const patreon = useStore($patreonLogin);
|
||||
const patreon = App.use((state) => state.logins.patreon);
|
||||
const [exempt, setExempt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!patreon.token) return setExempt(false);
|
||||
if (!patreon) return setExempt(false);
|
||||
|
||||
if (!(patreon.token in cache)) {
|
||||
cache[patreon.token] = new Promise<boolean>(async (resolve) => {
|
||||
|
||||
@@ -3,18 +3,17 @@ import {
|
||||
type BlitzkitResponse,
|
||||
type TankVotes,
|
||||
} from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { $wargamingLogin } from '../stores/wargamingLogin';
|
||||
import { App } from '../stores/app';
|
||||
|
||||
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
|
||||
|
||||
export function useTankVotes(id: number) {
|
||||
const [votes, setVotes] = useState<TankVotes | null>(null);
|
||||
const wargaming = useStore($wargamingLogin);
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wargaming.token) return;
|
||||
if (!wargaming) return;
|
||||
|
||||
if (id in cache) {
|
||||
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { BlitzKitTheme } from '../components/BlitzKitTheme';
|
||||
import { Checks } from '../components/Checks';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Navbar from '../components/Navbar.astro';
|
||||
import Party3 from '../components/Party3.astro';
|
||||
@@ -43,11 +44,9 @@ const isRoot = pathname === '/';
|
||||
<Flex direction="column" minHeight="100vh">
|
||||
<Navbar />
|
||||
|
||||
<!-- <Checks /> -->
|
||||
<Checks client:only />
|
||||
|
||||
<!-- <Suspense fallback={(<PageLoader />)}> -->
|
||||
<slot />
|
||||
<!-- </Suspense> -->
|
||||
|
||||
<Footer />
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { $patreonLogin } from '../../../stores/patreonLogin';
|
||||
import { App } from '../../../stores/app';
|
||||
import { $wargamingLogin } from '../../../stores/wargamingLogin';
|
||||
|
||||
export const AUTH_PROVIDERS = ['wargaming', 'patreon'] as const;
|
||||
@@ -25,6 +25,8 @@ interface AuthorizeProps {
|
||||
}
|
||||
|
||||
export function Authorize({ provider }: AuthorizeProps) {
|
||||
const mutateApp = App.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
@@ -56,10 +58,12 @@ export function Authorize({ provider }: AuthorizeProps) {
|
||||
);
|
||||
const data = (await response.json()) as PatreonAuthResponse;
|
||||
|
||||
$patreonLogin.set({
|
||||
token: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expires: `${Date.now() + data.expires_in * 1000}`,
|
||||
mutateApp((draft) => {
|
||||
draft.logins.patreon = {
|
||||
token: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expires: Date.now() + data.expires_in * 1000,
|
||||
};
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import { TankSearch } from '../../../components/TankSearch';
|
||||
import { App } from '../../../stores/app';
|
||||
|
||||
export function Page() {
|
||||
return (
|
||||
<PageWrapper color="purple" size={1028}>
|
||||
<TankSearch />
|
||||
<App.Provider>
|
||||
<TankSearch />
|
||||
</App.Provider>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,26 @@
|
||||
import { merge } from 'lodash-es';
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { createContextualStore } from '../../core/zustand/createContextualStore';
|
||||
|
||||
interface AppStore {
|
||||
developerMode: boolean;
|
||||
policiesAgreementIndex: number;
|
||||
logins: {
|
||||
wargaming?: { token: string; id: number; expires: number };
|
||||
patreon?: { token: string; refreshToken: string; expires: number };
|
||||
};
|
||||
}
|
||||
|
||||
export const App = createContextualStore(() =>
|
||||
create<AppStore>()(() => ({
|
||||
developerMode: false,
|
||||
policiesAgreementIndex: 0,
|
||||
})),
|
||||
create<AppStore>()(
|
||||
persist(
|
||||
subscribeWithSelector<AppStore>(() => ({
|
||||
developerMode: false,
|
||||
policiesAgreementIndex: -1,
|
||||
logins: {},
|
||||
})),
|
||||
{ name: 'app', merge: (a, b) => merge(b, a) },
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { persistentMap } from '@nanostores/persistent';
|
||||
|
||||
type PatreonLogin =
|
||||
| {
|
||||
token: undefined;
|
||||
}
|
||||
| {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
expires: string;
|
||||
};
|
||||
|
||||
export const $patreonLogin = persistentMap<PatreonLogin>('patreon:', {
|
||||
token: undefined,
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { persistentMap } from '@nanostores/persistent';
|
||||
|
||||
type WargamingLogin =
|
||||
| {
|
||||
token: undefined;
|
||||
}
|
||||
| {
|
||||
token: string;
|
||||
id: string;
|
||||
expires: string;
|
||||
};
|
||||
|
||||
export const $wargamingLogin = persistentMap<WargamingLogin>('wargaming:', {
|
||||
token: undefined,
|
||||
});
|
||||
Reference in new issue
Block a user