Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ export async function generateMetadata() {
}
export default async function Page() {
return (
<PageWrapper className="space-y-6">
<HomeLogo />
<HomeSearchButton />
<PageWrapper className="mx-auto flex max-w-[120rem] flex-col gap-6 py-2">
<div className="flex flex-col items-center gap-4">
<HomeLogo />
<HomeSearchButton />
</div>
<HomeQuickLinks />
<Buckets />
</PageWrapper>
Expand Down
1 change: 1 addition & 0 deletions src/app/leaderboards/LeaderboardControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const LeaderboardControls = (props: { hasPages: boolean; hasSearch: boole
throw new Error("This function should not be called when search is enabled")
}

// @ts-expect-error generic hell
return await getRaidHubApi(apiUrl, params, {
search: membershipId,
count: entriesPerPage
Expand Down
91 changes: 91 additions & 0 deletions src/app/leaderboards/team/custom/pantheon-community-race/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { type Metadata } from "next"
import { notFound } from "next/navigation"
import { LeaderboardSSR } from "~/app/leaderboards/LeaderboardSSR"
import { CloudflareActivitySplash } from "~/components/CloudflareImage"
import { getActivePantheonIds, PANTHEON_COMMUNITY_RACE_VERSION_ID } from "~/lib/manifest/pantheon"
import { baseMetadata } from "~/lib/metadata"
import { prefetchManifest } from "~/services/raidhub/prefetchRaidHubManifest"
import { Leaderboard } from "../../../Leaderboard"
import { Splash } from "../../../LeaderboardSplashComponents"

export const revalidate = 900
export const dynamic = "force-static"
export const fetchCache = "default-no-store"

export async function generateMetadata(): Promise<Metadata> {
const manifest = await prefetchManifest()
const activityId = getActivePantheonIds(manifest)[0]
const activity = activityId != null ? manifest.activityDefinitions[activityId] : undefined
const version = manifest.versionDefinitions[PANTHEON_COMMUNITY_RACE_VERSION_ID]

if (!activity || version?.associatedActivityId == null) {
notFound()
}
Comment on lines +13 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Calling notFound() in generateMetadata for a statically generated page (force-static) will cause the entire application build to fail if the necessary data is missing.
Severity: CRITICAL

Suggested Fix

In generateMetadata, instead of calling notFound(), return fallback metadata (e.g., a generic title). Move the logic for checking data availability and calling notFound() into the Page component itself. This ensures the page can still be statically generated, and the 404 is handled at request time.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/app/leaderboards/team/custom/pantheon-community-race/page.tsx#L11-L23

Potential issue: The Pantheon community race page is configured for static generation
via `export const dynamic = "force-static"`. Its `generateMetadata` function calls
`notFound()` if the required manifest data is unavailable at build time. In Next.js,
calling `notFound()` within `generateMetadata` for a statically generated page is not
supported and will cause the entire application build to fail. This could happen if the
API manifest is incomplete or unavailable during deployment, thus preventing new
versions of the site from being deployed.


const title = `${activity.name}: ${version.name} Community Race Leaderboard`
const description = `View community race placements for ${version.name} in ${activity.name}`

return {
title,
description,
keywords: [
activity.name,
version.name,
"community race",
"pantheon",
...baseMetadata.keywords
],
openGraph: {
...baseMetadata.openGraph,
title,
description
}
}
}

export default async function Page({ searchParams }: { searchParams: Record<string, string> }) {
const manifest = await prefetchManifest()
const activityId = getActivePantheonIds(manifest)[0]
const activity = activityId != null ? manifest.activityDefinitions[activityId] : undefined
const version = manifest.versionDefinitions[PANTHEON_COMMUNITY_RACE_VERSION_ID]

if (!activity || version?.associatedActivityId == null) {
notFound()
}

return (
<Leaderboard
heading={
<Splash
tertiaryTitle={activity.name}
title={version.name}
subtitle="Community Race Leaderboard">
<CloudflareActivitySplash
activityId={version.associatedActivityId}
versionId={version.id}
fill
className="z-[-1]"
/>
Comment on lines +63 to +68

This comment was marked as outdated.

</Splash>
}
hasPages
hasSearch
external={false}
pageProps={{
layout: "team",
queryKey: ["raidhub", "leaderboard", "pantheon-community-race"],
entriesPerPage: 50,
apiUrl: "/leaderboard/team/custom/pantheon-community-race",
params: null
}}
entries={
<LeaderboardSSR
page={searchParams.page ?? "1"}
entriesPerPage={50}
apiUrl="/leaderboard/team/custom/pantheon-community-race"
params={null}
/>
}
/>
)
}
32 changes: 24 additions & 8 deletions src/components/CloudflareImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Image, { type ImageLoader } from "next/image"
import { useCallback, type ComponentPropsWithoutRef } from "react"
import { HomePageSplash } from "~/lib/activity-images"
import { VaultEmblems } from "~/lib/bungie-foundation-emblems"
import { cn } from "~/lib/tw"
import { type ImageSize } from "~/services/raidhub/types"
import { useRaidHubManifest } from "./providers/RaidHubManifestManager"

Expand Down Expand Up @@ -125,31 +126,38 @@ export const CloudflareActivitySplash = ({
alt,
...props
}: { activityId: number; versionId?: number } & StrippedImageProps) => {
const { getImageVariantsForActivity, getActivityDefinition, getVersionString } =
useRaidHubManifest()
const {
getImageVariantsForActivity,
getImageVariantsForVersion,
getActivityDefinition,
getVersionString
} = useRaidHubManifest()

const loader = useCallback<ImageLoader>(
({ width, quality }) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const minWidth = (width * (quality || 75)) / 100

const activityVariants = getImageVariantsForActivity(activityId)
if (!activityVariants?.length) {
const splashVariants =
versionId != null
? getImageVariantsForVersion(versionId)
: getImageVariantsForActivity(activityId)
if (!splashVariants.length) {
return FallbackSplash
}

const availableSizes = new Set(activityVariants.map(c => c.size))
const availableSizes = new Set(splashVariants.map(c => c.size))

const variants = cloudflareVariants.filter(item => availableSizes.has(item.name))
const size = (
variants.find(item => item.w >= minWidth && availableSizes.has(item.name)) ??
variants[variants.length - 1]
).name
const content = activityVariants.find(c => c.size === size)!
const content = splashVariants.find(c => c.size === size)!
Comment on lines 154 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The image loader may crash with a TypeError if the API returns only image sizes, like "full", that are not in the hardcoded cloudflareVariants list.
Severity: HIGH

Suggested Fix

Add a guard to handle the case where the filtered variants array is empty. Before accessing variants[variants.length - 1], check if the array has elements. If it is empty, provide a safe fallback variant or default size to prevent the TypeError.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/components/CloudflareImage.tsx#L154-L156

Potential issue: The loader function in `CloudflareImage.tsx` filters a hardcoded list
of variants (`cloudflareVariants`) against sizes returned by the API. The API can return
a size of `"full"`, which is not present in the hardcoded list. If the API returns only
the `"full"` size, the filtered `variants` array will be empty. The subsequent code
attempts to access an element from this empty array (`variants[variants.length - 1]`),
which evaluates to `undefined`. Accessing the `.name` property on this `undefined` value
will cause a `TypeError`, crashing the component.


return content.url
},
[activityId, getImageVariantsForActivity]
[activityId, versionId, getImageVariantsForActivity, getImageVariantsForVersion]
)

const activityDefinition = getActivityDefinition(activityId)
Expand All @@ -159,5 +167,13 @@ export const CloudflareActivitySplash = ({
? activityDefinition.name
: getVersionString(versionId ?? activityId))

return <Image loader={loader} {...props} src="placeholder" alt={altText} />
return (
<Image
loader={loader}
{...props}
className={cn(props.fill && "object-cover object-[center_30%]", props.className)}
src="placeholder"
alt={altText}
/>
)
}
Loading
Loading