Skip to content

Commit

Permalink
Merge pull request #2053 from GW2Treasures/feature/legendary-relics
Browse files Browse the repository at this point in the history
Add page for legendary relics
  • Loading branch information
darthmaim authored Feb 14, 2025
2 parents 203b940 + 6b89d3d commit 3e3952a
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 1 deletion.
24 changes: 24 additions & 0 deletions apps/web/app/[language]/legendary/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Scope } from '@gw2me/client';

export const requiredScopes = [
// always required
Scope.GW2_Account,

// get all the characters
// TODO: remove once using armory subscription
Scope.GW2_Characters,

// get inventories
// TODO: remove once using armory subscription
Scope.GW2_Inventories,

// legendary armory
Scope.GW2_Unlocks,

// delivered items waiting for pickup
// TODO: remove once using armory subscription
Scope.GW2_Tradingpost,

// Relic unlocks using `/v2/account/achievements`
Scope.GW2_Progression,
];
28 changes: 28 additions & 0 deletions apps/web/app/[language]/legendary/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Gw2Accounts } from '@/components/Gw2Api/Gw2Accounts';
import { Trans } from '@/components/I18n/Trans';
import { HeroLayout } from '@/components/Layout/HeroLayout';
import { NavBar } from '@/components/Layout/NavBar';
import type { LayoutProps } from '@/lib/next';
import { Headline } from '@gw2treasures/ui/components/Headline/Headline';
import { requiredScopes } from './helper';

export default function LegendaryLayout({ children }: LayoutProps) {
return (
<HeroLayout color="rgb(185 0 185)"
hero={<Headline id="legendary-armory"><Trans id="legendary-armory"/></Headline>}
navBar={(
<NavBar base="/legendary/" items={[
{ segment: 'weapons', label: <Trans id="legendary-armory.weapons"/> },
{ segment: 'armor', label: <Trans id="legendary-armory.armor"/> },
{ segment: 'trinkets', label: <Trans id="legendary-armory.trinkets"/> },
{ segment: 'relics', label: <Trans id="legendary-armory.relics"/> },
]}/>
)}
>
<>
<Gw2Accounts requiredScopes={requiredScopes} loading={null} loginMessage={<Trans id="legendary-armory.login"/>} authorizationMessage={<Trans id="legendary-armory.authorize"/>}/>
{children}
</>
</HeroLayout>
);
}
7 changes: 7 additions & 0 deletions apps/web/app/[language]/legendary/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Skeleton } from '@/components/Skeleton/Skeleton';

export default function LegendaryLoading() {
return (
<Skeleton/>
);
}
119 changes: 119 additions & 0 deletions apps/web/app/[language]/legendary/relics/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { AccountAchievementProgressHeader, AccountAchievementProgressRow } from '@/components/Achievement/AccountAchievementProgress';
import { AchievementLink } from '@/components/Achievement/AchievementLink';
import { Trans } from '@/components/I18n/Trans';
import { ItemLink } from '@/components/Item/ItemLink';
import { Description } from '@/components/Layout/Description';
import { ColumnSelect } from '@/components/Table/ColumnSelect';
import { cache } from '@/lib/cache';
import { linkProperties, linkPropertiesWithoutRarity } from '@/lib/linkProperties';
import type { PageProps } from '@/lib/next';
import { db } from '@/lib/prisma';
import { getTranslate } from '@/lib/translate';
import type { Achievement } from '@gw2api/types/data/achievement';
import { isDefined } from '@gw2treasures/helper/is';
import { Headline } from '@gw2treasures/ui/components/Headline/Headline';
import { createDataTable } from '@gw2treasures/ui/components/Table/DataTable';
import type { Metadata } from 'next';
import { createItemTable, LegendaryItemDataTable } from '../table';

// item id of the legendary relic
const legendaryRelicId = 101582;

// all the achievements are in this category
const rareCollectionsAchievementCategoryId = 75;

// core and SotO relics are always unlocked
const coreAchievementIds = [
7685, // Relics—Core Set 1
7686, // Relics—Secrets of the Obscure Set 1
7684, // Relics—Secrets of the Obscure Set 2
7960, // Relics—Secrets of the Obscure Set 3
];

const loadItems = cache(async () => {
const items = await db.item.findMany({
where: { id: legendaryRelicId },
select: linkProperties
});

return items;
}, ['legendary-relic'], { revalidate: 60 * 60 });

const loadAchievements = cache(async () => {
const achievements = await db.achievement.findMany({
where: {
name_en: { startsWith: 'Relics—%' },
id: { notIn: coreAchievementIds },
achievementCategoryId: rareCollectionsAchievementCategoryId
},
select: {
...linkPropertiesWithoutRarity,
flags: true,
prerequisitesIds: true,
bitsItem: { select: linkProperties },
current_en: { select: { data: true }},
},
orderBy: { id: 'asc' },
});

return achievements;
}, ['legendary-relics'], { revalidate: 60 * 60 });

function achievementsToRelics(achievements: Awaited<ReturnType<typeof loadAchievements>>) {
return achievements.flatMap((achievement) => {
const data = JSON.parse(achievement.current_en.data) as Achievement;

return data.bits?.map((bit, index) => {
if(bit.type !== 'Item') {
return undefined;
}

const item = achievement.bitsItem.find(({ id }) => id === bit.id);

return item ? { bitId: index, item, achievement } : undefined;
});
}).filter(isDefined);
}

export default async function LegendaryRelicsPage() {
const [items, achievements] = await Promise.all([
loadItems(),
loadAchievements(),
]);

const Items = createItemTable(items);
const Relics = createDataTable(achievementsToRelics(achievements), ({ item }) => item.id);

return (
<>
<Description actions={<ColumnSelect table={Items}/>}>
<Trans id="legendary-armory.relics.description"/>
</Description>
<LegendaryItemDataTable table={Items}/>

<Headline id="unlocks"><Trans id="legendary-armory.relics.unlocks"/></Headline>
<p><Trans id="legendary-armory.relics.unlocks.description"/></p>
<Relics.Table>
<Relics.Column id="relic" title="Relic">
{({ item }) => <ItemLink item={item}/>}
</Relics.Column>
<Relics.Column id="set" title="Set">
{({ achievement }) => <AchievementLink achievement={achievement}/>}
</Relics.Column>
<Relics.DynamicColumns headers={<AccountAchievementProgressHeader/>}>
{({ achievement, bitId }) => <AccountAchievementProgressRow achievement={achievement} bitId={bitId}/>}
</Relics.DynamicColumns>
</Relics.Table>
</>
);
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { language } = await params;
const t = getTranslate(language);

return {
title: t('legendary-armory.relics.title'),
description: t('legendary-armory.relics.description'),
};
}
39 changes: 39 additions & 0 deletions apps/web/app/[language]/legendary/table.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { ProgressCell } from '@/components/Achievement/ProgressCell';
import { FormatNumber } from '@/components/Format/FormatNumber';
import { useInventoryItem, UseInventoryItemAccountLocation } from '@/components/Inventory/use-inventory';
import { Skeleton } from '@/components/Skeleton/Skeleton';
import type { FC } from 'react';

interface LegendaryArmoryCellProps {
itemId: number;
accountId: string;
}

export const LegendaryArmoryCell: FC<LegendaryArmoryCellProps> = ({ itemId, accountId }) => {
// TODO: only subscribe to legendary armory
const inventory = useInventoryItem(accountId, itemId);

if(inventory.loading) {
return <td><Skeleton/></td>;
}

if(inventory.error) {
return <td/>;
}

// get items in legendary armory
const legendaryArmory = inventory.locations.find(
({ location }) => location === UseInventoryItemAccountLocation.LegendaryArmory
);

// TODO use correct `max_count`
const max_count = 1;

return (
<ProgressCell progress={Math.min(legendaryArmory?.count ?? 0, 1)}>
<FormatNumber value={legendaryArmory?.count ?? 0}/> / <FormatNumber value={max_count}/>
</ProgressCell>
);
};
29 changes: 29 additions & 0 deletions apps/web/app/[language]/legendary/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { FC } from 'react';
import type { LocalizedEntity } from '@/lib/localizedName';
import type { Item } from '@gw2treasures/database';
import { createDataTable } from '@gw2treasures/ui/components/Table/DataTable';
import { ItemLink } from '@/components/Item/ItemLink';
import { Gw2AccountBodyCells, Gw2AccountHeaderCells } from '@/components/Gw2Api/Gw2AccountTableCells';
import { LegendaryArmoryCell } from './table.client';
import { requiredScopes } from './helper';
import { Trans } from '@/components/I18n/Trans';

export function createItemTable(items: Pick<Item, keyof LocalizedEntity | 'id' | 'rarity'>[]) {
return createDataTable(items, ({ id }) => id);
}

interface LegendaryItemDataTableProps {
table: ReturnType<typeof createItemTable>
}

export const LegendaryItemDataTable: FC<LegendaryItemDataTableProps> = ({ table: items }) => {
return (
<items.Table>
<items.Column id="id" title={<Trans id="itemTable.column.id"/>} small hidden align="right">{({ id }) => id}</items.Column>
<items.Column id="item" title={<Trans id="itemTable.column.item"/>} fixed>{(item) => <ItemLink item={item}/>}</items.Column>
<items.DynamicColumns headers={<Gw2AccountHeaderCells small requiredScopes={requiredScopes}/>}>
{(item) => <Gw2AccountBodyCells requiredScopes={requiredScopes}><LegendaryArmoryCell itemId={item.id} accountId={undefined as never}/></Gw2AccountBodyCells>}
</items.DynamicColumns>
</items.Table>
);
};
13 changes: 12 additions & 1 deletion apps/web/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,16 @@
"currency.category.dungeon": "Dungeon",
"currency.category.blacklion": "Black Lion",
"currency.category.historic": "Historic",
"currency.order": "Order"
"currency.order": "Order",
"legendary-armory": "Legendary Armory",
"legendary-armory.login": "Login to view your personal Legendary Armory",
"legendary-armory.authorize": "You need to authorize gw2treasures.com to be able to see your accounts Legendary Armory",
"legendary-armory.weapons": "Weapons",
"legendary-armory.armor": "Armor",
"legendary-armory.trinkets": "Trinkets",
"legendary-armory.relics": "Relics",
"legendary-armory.relics.title": "Legendary Relics",
"legendary-armory.relics.description": "Legendary Relics can take the effect of any relic, and can be used on every character once unlocked in the Legendary Armory.",
"legendary-armory.relics.unlocks": "Unlocks",
"legendary-armory.relics.unlocks.description": "Relics introduced after Secrets of the Obscure have to be unlocked, before they can be used with the Legendary Relic."
}

0 comments on commit 3e3952a

Please # to comment.