diff --git a/bun.lockb b/bun.lockb index 312593ef1..634ca645f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index ea5120699..d053cefe2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "license": "MIT", "dependencies": { + "cheerio": "^1.1.2", "googleapis": "^144.0.0", "lodash-es": "^4.17.21", "protobufjs": "^7.5.2", diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 539136d13..371b6e0da 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -1026,7 +1026,6 @@ "review": { "title": "Review videos", - "disclaimer": "BlitzKit does not endorse any review videos.", "join": "Apply to %s1 YouTuber program" }, diff --git a/packages/website/src/components/Tankopedia/GuideSection/components/GuidesBlitz.tsx b/packages/website/src/components/Tankopedia/GuideSection/components/GuidesBlitz.tsx new file mode 100644 index 000000000..4d40c39d6 --- /dev/null +++ b/packages/website/src/components/Tankopedia/GuideSection/components/GuidesBlitz.tsx @@ -0,0 +1,51 @@ +import { CaretRightIcon } from '@radix-ui/react-icons'; +import { Box, Flex, Link, Text } from '@radix-ui/themes'; +import { Var } from '../../../../core/radix/var'; +import type { TankGuide } from '../../../../pages/[...locale]/tanks/[slug]/index.astro'; + +interface Props { + guide: TankGuide; +} + +export function GuidesBlitz({ guide }: Props) { + return ( + <> + + + + + GuidesBlitz + + + + {guide.description} + + + + + + + + Read the full guide + + + + + ); +} diff --git a/packages/website/src/components/Tankopedia/GuideSection/components/YouTube.tsx b/packages/website/src/components/Tankopedia/GuideSection/components/YouTube.tsx new file mode 100644 index 000000000..55c74e66f --- /dev/null +++ b/packages/website/src/components/Tankopedia/GuideSection/components/YouTube.tsx @@ -0,0 +1,118 @@ +import { youtubers } from '@blitzkit/core'; +import { CaretRightIcon } from '@radix-ui/react-icons'; +import { Box, Flex, Link, Skeleton, Text } from '@radix-ui/themes'; +import { awaitableReviews } from '../../../../core/awaitables/reviews'; +import { Var } from '../../../../core/radix/var'; +import { Duel } from '../../../../stores/duel'; +import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps'; +import { VerifiedIcon } from '../../../VerifiedIcon'; + +const reviews = await awaitableReviews; + +export function YouTube({ skeleton }: MaybeSkeletonComponentProps) { + const tank = Duel.use((state) => state.protagonist.tank); + const videos = reviews.reviews[tank.id]?.videos; + + if (!videos) return null; + + return ( + <> + + + + + YouTube + + + + + {videos.map((video) => { + const image = `https://i.ytimg.com/vi/${video.id}/hqdefault.jpg`; + const youtuber = youtubers.find( + (youtuber) => youtuber.id === video.author, + ); + + if (!youtuber) return null; + + return ( + + + + + + {!skeleton && youtuber.name} + {skeleton && } + + {!skeleton && } + + + + ); + })} + + + + + + + + Read the full guide + + + + + ); +} diff --git a/packages/website/src/components/Tankopedia/GuideSection/index.tsx b/packages/website/src/components/Tankopedia/GuideSection/index.tsx new file mode 100644 index 000000000..d3660a1b6 --- /dev/null +++ b/packages/website/src/components/Tankopedia/GuideSection/index.tsx @@ -0,0 +1,17 @@ +import { Flex } from '@radix-ui/themes'; +import type { TankGuide } from '../../../pages/[...locale]/tanks/[slug]/index.astro'; +import { GuidesBlitz } from './components/GuidesBlitz'; + +interface Props { + guide: TankGuide; +} + +export function GuideSection({ guide }: Props) { + return ( + + + + + + ); +} diff --git a/packages/website/src/components/Tankopedia/VideoSection.tsx b/packages/website/src/components/Tankopedia/VideoSection.tsx index aa2353a52..19293ed1b 100644 --- a/packages/website/src/components/Tankopedia/VideoSection.tsx +++ b/packages/website/src/components/Tankopedia/VideoSection.tsx @@ -24,10 +24,6 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) { {strings.website.tools.tankopedia.review.title} - - - {strings.website.tools.tankopedia.review.disclaimer} - diff --git a/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx b/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx index 933d53c8d..b7961104a 100644 --- a/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx +++ b/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx @@ -3,6 +3,7 @@ import { PageWrapper } from '../../../../components/PageWrapper'; import { CalloutsSection } from '../../../../components/Tankopedia/CalloutsSection'; import { CharacteristicsSection } from '../../../../components/Tankopedia/CharacteristicsSection'; import { GameModeSection } from '../../../../components/Tankopedia/GameModeSection'; +import { GuideSection } from '../../../../components/Tankopedia/GuideSection'; import { HeroSection } from '../../../../components/Tankopedia/HeroSection'; import { MetaSection } from '../../../../components/Tankopedia/MetaSection'; import { TechTreeSection } from '../../../../components/Tankopedia/TechTreeSection'; @@ -19,10 +20,12 @@ import { Duel } from '../../../../stores/duel'; import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent'; import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps'; +import type { TankGuide } from './index.astro'; type PageProps = MaybeSkeletonComponentProps & LocaleAcceptorProps & { id: number; + guide?: TankGuide; }; const [tankDefinitions, provisionDefinitions, modelDefinitions] = @@ -32,7 +35,7 @@ const [tankDefinitions, provisionDefinitions, modelDefinitions] = awaitableModelDefinitions, ]); -export function Page({ id, skeleton, locale }: PageProps) { +export function Page({ id, skeleton, locale, guide }: PageProps) { const tank = tankDefinitions.tanks[id]; const model = modelDefinitions.models[id]; @@ -53,6 +56,7 @@ export function Page({ id, skeleton, locale }: PageProps) { )} + {guide && } diff --git a/packages/website/src/pages/[...locale]/tanks/[slug]/index.astro b/packages/website/src/pages/[...locale]/tanks/[slug]/index.astro index 5db83f641..dead21e65 100644 --- a/packages/website/src/pages/[...locale]/tanks/[slug]/index.astro +++ b/packages/website/src/pages/[...locale]/tanks/[slug]/index.astro @@ -1,6 +1,7 @@ --- import { AstroSeo } from '@astrolib/seo'; import { + assertSecret, asset, createDefaultProvisions, createDefaultSkills, @@ -14,6 +15,7 @@ import { import { DEFAULT_LOCALE, unwrapper } from '@blitzkit/i18n'; import { literals } from '@blitzkit/i18n/src/literals'; import type { GetStaticPaths } from 'astro'; +import { load } from 'cheerio'; import { tankCharacteristics } from '../../../../core/blitzkit/tankCharacteristics'; import { getStrings } from '../../../../core/i18n/getStrings'; import BaseLayout from '../../../../layouts/BaseLayout.astro'; @@ -43,6 +45,7 @@ const skillDefinitions = await fetchSkillDefinitions(); const provisionDefinitions = await fetchProvisionDefinitions(); const equipmentDefinitions = await fetchEquipmentDefinitions(); const tankModelDefinitions = await fetchModelDefinitions(); + const tank = tankDefinitions.tanks[id]; const tankModelDefinition = tankModelDefinitions.models[tank.id]; const unwrap = unwrapper(locale); @@ -57,6 +60,7 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, [ strings.common.nations[tank.nation as keyof typeof strings.common.nations], ]); const url = import.meta.env.SITE + Astro.url.pathname; + const icon = asset(`icons/tanks/big/${id}.webp`); const turret = tank.turrets.at(-1)!; const gun = turret.guns.at(-1)!; @@ -96,6 +100,28 @@ const description = literals( Math.round(characteristics.health).toLocaleString(locale), ], ); + +export interface TankGuide { + url: string; + description?: string; + image?: string; +} + +const guideUrl = `${assertSecret(import.meta.env.PUBLIC_GUIDE_WEBSITE)}/${tank.slug}`; +let guide: TankGuide | undefined = undefined; +const guideResponse = await fetch(guideUrl); + +if (guideResponse.ok) { + const $ = await guideResponse.text().then(load); + const description = $('.entry-content p').first().text(); + const image = $('.entry-content img').first().attr('src')?.toString(); + + guide = { + url: guideUrl, + description, + image, + }; +} --- - - + +