Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lunar New Year festival #1866

Merged
merged 9 commits into from
Jan 23, 2025
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
4 changes: 3 additions & 1 deletion apps/web/app/[language]/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PageView } from '@/components/PageView/PageView';
import type { PageProps } from '@/lib/next';
import { Snow } from '../festival/wintersday/snow';
import { Festival, getActiveFestival } from '../festival/festivals';
import { LunarNewYearHero } from '../festival/lunar-new-year/hero';

async function HomePage({ params }: PageProps) {
const { language } = await params;
Expand Down Expand Up @@ -139,4 +140,5 @@ export async function generateMetadata({ params }: PageProps) {
const festivalHero: Record<Festival | 'default', { color: string, wrapper: ComponentType<{ children: ReactNode }> }> = {
default: { color: '#b7000d', wrapper: Fragment },
[Festival.Wintersday]: { color: '#7993a9', wrapper: Snow },
}
[Festival.LunarNewYear]: { color: '#be3413', wrapper: LunarNewYearHero },
};
14 changes: 10 additions & 4 deletions apps/web/app/[language]/festival/festivals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum Festival {
Wintersday,
LunarNewYear,
}

export interface FestivalInfo {
Expand All @@ -13,16 +14,21 @@ export function getActiveFestival(): FestivalInfo | undefined {
return festivals.find((festival) => isFestivalActive(festival, now));
}

export function getFestival(type: Festival): FestivalInfo {
return festivals.findLast((festival) => festival.type === type)!;
export function getFestival(type: Festival): FestivalInfo | undefined {
return festivals.findLast((festival) => festival.type === type);
}

export function isFestivalActive(festival: FestivalInfo, timestamp?: Date) {
export function isFestivalActive(festival: FestivalInfo | undefined, timestamp?: Date) {
if(!festival) {
return false;
}

timestamp ??= new Date();

return festival.startsAt <= timestamp && festival.endsAt > timestamp;
}

export const festivals: FestivalInfo[] = [
{ type: Festival.Wintersday, startsAt: new Date('2024-12-10T16:00:00.000Z'), endsAt: new Date('2025-01-02T17:00:00.000Z') },
{ type: Festival.Wintersday, startsAt: new Date('2024-12-10T17:00:00.000Z'), endsAt: new Date('2025-01-02T17:00:00.000Z') },
{ type: Festival.LunarNewYear, startsAt: new Date('2025-01-28T17:00:00.000Z'), endsAt: new Date('2025-02-18T17:00:00.000Z') },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Coins } from '@/components/Format/Coins';
import { ItemLink, type ItemLinkProps } from '@/components/Item/ItemLink';
import { Table } from '@gw2treasures/ui/components/Table/Table';
import type { FC } from 'react';

export interface InventoryTableProps {
envelope: ItemLinkProps['item']
}

export const InventoryTable: FC<InventoryTableProps> = ({ envelope }) => {
return (
<Table>
<thead>
<tr>
<Table.HeaderCell>Account</Table.HeaderCell>
<Table.HeaderCell align="right">Coins</Table.HeaderCell>
<Table.HeaderCell align="right"><ItemLink item={envelope} icon={32}/></Table.HeaderCell>
</tr>
</thead>
<tbody>
<tr>
<td>darthmaim.6017</td>
<td align="right"><Coins value={10204975}/></td>
<td align="right">0</td>
</tr>
<tr>
<td>2nd Account</td>
<td align="right"><Coins value={0}/></td>
<td align="right">0</td>
</tr>
</tbody>
</Table>
);
};

/*

<Gw2Accounts requiredScopes={requiredScopes} loading={null} loginMessage={<Trans id="festival.items.login"/>} authorizationMessage={<Trans id="festival.items.authorize"/>}>
{items.map((item) => (
<Fragment key={item.id}>
<Headline id={item.id.toString()}><ItemLink item={item}/></Headline>
<ItemInventoryTable itemId={item.id}/>
</Fragment>
))}
</Gw2Accounts>
*/
83 changes: 83 additions & 0 deletions apps/web/app/[language]/festival/lunar-new-year/(index)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Trans } from '@/components/I18n/Trans';
import { ItemTable } from '@/components/ItemTable/ItemTable';
import { ItemTableColumnsButton } from '@/components/ItemTable/ItemTableColumnsButton';
import { ItemTableContext } from '@/components/ItemTable/ItemTableContext';
import { PageLayout } from '@/components/Layout/PageLayout';
import { pageView } from '@/lib/pageView';
import { Headline } from '@gw2treasures/ui/components/Headline/Headline';
import type { Metadata } from 'next';
import { db } from '@/lib/prisma';
import { linkProperties } from '@/lib/linkProperties';
import { cache } from '@/lib/cache';
import type { PageProps } from '@/lib/next';
import { getTranslate } from '@/lib/translate';
import { groupById } from '@gw2treasures/helper/group-by';

const ITEM_ENVELOPE = 68646;
const ITEM_DB_CHAMPION_ENVELOPE = 68647;
const ITEM_LITTLE_ENVELOPE = 68645;
const ITEM_TOKEN_CHAMPION = 92659;
const ITEM_TOKEN_CHAMPION_FRAGMENT = 94668;
const ITEM_TOKEN_DB_CHAMPION = 68618;

const itemIds = [
ITEM_ENVELOPE,
ITEM_DB_CHAMPION_ENVELOPE,
ITEM_LITTLE_ENVELOPE,
ITEM_TOKEN_CHAMPION,
ITEM_TOKEN_CHAMPION_FRAGMENT,
ITEM_TOKEN_DB_CHAMPION,
];

const loadData = cache(async function loadData() {
const [items] = await Promise.all([
db.item.findMany({
where: { id: { in: itemIds }},
select: {
...linkProperties,
tpTradeable: true, tpCheckedAt: true,
buyPrice: true, buyQuantity: true,
sellPrice: true, sellQuantity: true,
tpHistory: { orderBy: { time: 'asc' }}
},
orderBy: { relevancy: 'desc' },
})
]);

return { items };
}, ['lunar-new-year-items'], { revalidate: 60 * 60 });

export default async function LunarNewYearPage() {
const { items } = await loadData();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const itemsById = groupById(items);

await pageView('festival/lunar-new-year');

return (
<PageLayout>
<ItemTableContext id="lunar-new-year">
<p><Trans id="festival.lunar-new-year.intro"/></p>
<p><Trans id="festival.lunar-new-year.description"/></p>
<Headline actions={<ItemTableColumnsButton/>} id="items"><Trans id="navigation.items"/></Headline>
<ItemTable query={{ where: { id: { in: itemIds }}}} defaultColumns={['item', 'rarity', 'type', 'buyPrice', 'buyPriceTrend', 'sellPrice', 'sellPriceTrend']}/>
</ItemTableContext>

<div style={{ marginTop: 32 }}/>

{/* <Headline id="inventory">Inventory</Headline>
<InventoryTable envelope={itemsById.get(ITEM_ENVELOPE)!}/> */}
</PageLayout>
);
}

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

return {
title: {
absolute: `${t('festival.lunar-new-year')} · gw2treasures.com`
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { AchievementTable } from '@/components/Achievement/AchievementTable';
import { Gw2Accounts } from '@/components/Gw2Api/Gw2Accounts';
import { Trans } from '@/components/I18n/Trans';
import { Description } from '@/components/Layout/Description';
import { PageLayout } from '@/components/Layout/PageLayout';
import { cache } from '@/lib/cache';
import { linkProperties } from '@/lib/linkProperties';
import type { PageProps } from '@/lib/next';
import { db } from '@/lib/prisma';
import type { AchievementFlags } from '@gw2api/types/data/achievement';
import { Headline } from '@gw2treasures/ui/components/Headline/Headline';
import { Notice } from '@gw2treasures/ui/components/Notice/Notice';
import type { Metadata } from 'next';
import { requiredScopes } from '../helper';
import { pageView } from '@/lib/pageView';
import { getTranslate } from '@/lib/translate';

const achievementIds = [
6031, // The Goldclaw Holiday Collection
5005, // Festival Frequenter
];
const achievementCategoryIds = [
199, // Dragon Ball
200, // New Year's Customs
201, // Daily Lunar New Year
202, // Lunar New Year
];

const dailyFlags: AchievementFlags[] = ['Daily', 'Weekly', 'Monthly'];

const loadData = cache(async function loadData() {
const [allAchievements] = await Promise.all([
db.achievement.findMany({
where: {
OR: [
{ id: { in: achievementIds }},
{ achievementCategoryId: { in: achievementCategoryIds }}
]
},
include: { icon: true, rewardsItem: { select: linkProperties }, rewardsTitle: { select: { id: true, name_de: true, name_en: true, name_es: true, name_fr: true }}},
})
]);

const groupedAchievements = allAchievements.reduce<{ achievements: typeof allAchievements, dailyAchievements: typeof allAchievements }>((grouped, achievement) => {
const isDaily = dailyFlags.some((flag) => achievement.flags.includes(flag));
grouped[isDaily ? 'dailyAchievements' : 'achievements'].push(achievement);
return grouped;
}, { achievements: [], dailyAchievements: [] });

return { ...groupedAchievements };
}, ['lunar-new-year'], { revalidate: 60 * 60 });

export default async function LunarNewYearAchievementsPage({ params }: PageProps) {
const { language } = await params;
const { achievements, dailyAchievements } = await loadData();
await pageView('festival/lunar-new-year/achievements');

return (
<PageLayout>
<Gw2Accounts requiredScopes={requiredScopes} loading={null} loginMessage={<Trans id="festival.achievements.login"/>} authorizationMessage={<Trans id="festival.achievements.authorize"/>}/>

<AchievementTable achievements={achievements} language={language} includeRewardsColumns>
{(table, ColumnSelect) => (
<>
<Description actions={ColumnSelect}><Trans id="festival.lunar-new-year.achievements.description"/></Description>
{table}
</>
)}
</AchievementTable>

<AchievementTable achievements={dailyAchievements} language={language}>
{(table, columnSelect) => (
<>
<Headline actions={columnSelect} id="daily">Daily Achievements</Headline>
<Notice>The Guild Wars 2 API does not report progress for daily achievements.</Notice>
{table}
</>
)}
</AchievementTable>
</PageLayout>
);
}

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

return {
title: t('navigation.achievements')
};
}

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions apps/web/app/[language]/festival/lunar-new-year/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Scope } from '@gw2me/client';

export const requiredScopes = [
// general
Scope.GW2_Account,

// skins / minis
Scope.GW2_Unlocks,

// achievements
Scope.GW2_Progression,

// inventory
Scope.GW2_Characters,
Scope.GW2_Inventories,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.hero {
border: 16px solid;
border-image: url(./border-decoration.svg) 33%;
margin: -16px 0;
padding: 0 16px;
}
14 changes: 14 additions & 0 deletions apps/web/app/[language]/festival/lunar-new-year/hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { FC, ReactNode } from 'react';
import styles from './hero.module.css';

export interface LunarNewYearHeroProps {
children: ReactNode
}

export const LunarNewYearHero: FC<LunarNewYearHeroProps> = ({ children }) => {
return (
<div className={styles.hero}>
{children}
</div>
);
};
57 changes: 57 additions & 0 deletions apps/web/app/[language]/festival/lunar-new-year/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 { getTranslate } from '@/lib/translate';
import { getCurrentUrl } from '@/lib/url';
import { Headline } from '@gw2treasures/ui/components/Headline/Headline';
import { LunarNewYearHero } from './hero';
import type { Metadata } from 'next';
import ogImage from './og.png';
import { Festival, getFestival, isFestivalActive } from '../festivals';
import { Notice } from '@gw2treasures/ui/components/Notice/Notice';
import { FestivalTimer } from '@/components/Reset/FestivalTimer';

export default function LunarNewYearFestivalLayout({ children }: LayoutProps) {
const lunarNewYear = getFestival(Festival.LunarNewYear);

return (
<HeroLayout color="#be3413" hero={(<LunarNewYearHero><Headline id="lunar-new-year" actions={<FestivalTimer festival={lunarNewYear}/>}><Trans id="festival.lunar-new-year"/></Headline></LunarNewYearHero>)}
skipLayout
navBar={(
<NavBar base="/festival/lunar-new-year/" items={[
{ segment: '(index)', href: '/festival/lunar-new-year', label: <Trans id="festival.lunar-new-year"/> },
{ segment: 'achievements', label: <Trans id="navigation.achievements"/> },
{ segment: 'skins', label: <Trans id="navigation.skins"/> },
{ segment: 'minis', label: <Trans id="festival.lunar-new-year.minis"/> },
{ segment: 'wizards-vault', label: <Trans id="navigation.wizardsVault"/> },
]}/>
)}
>
{!isFestivalActive(lunarNewYear) && (
<div style={{ margin: '16px 16px -16px' }}>
<Notice>The Lunar New Year festival is currently not active!</Notice>
</div>
)}
{children}
</HeroLayout>
);
}

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

return {
title: {
template: `${t('festival.lunar-new-year')}: %s · gw2treasures.com`,
default: ''
},
description: t('festival.lunar-new-year.description'),
keywords: ['lunar new year', 'gw2', 'guild wars 2', 'festival', 'dragon ball', 'firecracker', 'firework', 'celestial challenge', 'lucky envelope', 'gold', 'coin', 'luck', 'mini', 'achievement', 'skin', 'unlock'],
openGraph: {
images: [{ url: new URL(ogImage.src, await getCurrentUrl()), width: ogImage.width, height: ogImage.height }],
},
twitter: { card: 'summary_large_image' }
};
}
10 changes: 10 additions & 0 deletions apps/web/app/[language]/festival/lunar-new-year/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PageLayout } from '@/components/Layout/PageLayout';
import { Skeleton } from '@/components/Skeleton/Skeleton';

export default function WintersdayLoading() {
return (
<PageLayout>
<Skeleton/>
</PageLayout>
);
}
Loading
Loading