Skip to content

Commit

Permalink
feat: hall of fame page (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
garrappachc authored Dec 11, 2024
1 parent 9d958bd commit c13fd71
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/hall-of-game/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import fp from 'fastify-plugin'

export default fp(
async app => {
await app.register((await import('./routes')).default)
},
{
name: 'hall of fame',
},
)
13 changes: 13 additions & 0 deletions src/hall-of-game/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import fp from 'fastify-plugin'
import { HallOfFamePage } from './views/html/hall-of-fame.page'

export default fp(
async app => {
app.get('/hall-of-fame', async (request, reply) => {
reply.status(200).html(await HallOfFamePage({ user: request.user }))
})
},
{
name: 'hall of fame routes',
},
)
63 changes: 63 additions & 0 deletions src/hall-of-game/views/html/hall-of-fame.page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.hof-board {
display: grid;
grid-template-columns: auto auto 1fr auto;
column-gap: 16px;
row-gap: 10px;

padding: 10px 14px;

background-color: rgba(19, 16, 20, 0.5);
color: theme(colors.abru.light.75);
border-radius: 8px;

font-size: 20px;
font-weight: 500;

.title {
font-size: 24px;
font-weight: 700;
}

.hof-record {
@apply transition-colors;
@apply duration-75;

display: grid;
grid-column: span 4 / span 4;
grid-template-columns: subgrid;
border-radius: 4px;
align-items: center;
padding: 0px 8px 0px 0px;

&:hover {
background-color: theme(colors.abru.light.5 / 40%);
}

&.is-1st,
&.is-2nd,
&.is-3rd {
padding: 10px 8px 10px 10px;
}

&.is-1st {
background-color: theme(colors.abru.light.15);
&:hover {
background-color: darken(theme(colors.abru.light.15), 2%);
}
}

&.is-2nd {
background-color: theme(colors.abru.light.10);
&:hover {
background-color: darken(theme(colors.abru.light.10), 2%);
}
}

&.is-3rd {
background-color: theme(colors.abru.light.5);
&:hover {
background-color: darken(theme(colors.abru.light.5), 2%);
}
}
}
}
133 changes: 133 additions & 0 deletions src/hall-of-game/views/html/hall-of-fame.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { resolve } from 'node:path'
import type { User } from '../../../auth/types/user'
import { Layout } from '../../../html/layout'
import { NavigationBar } from '../../../html/components/navigation-bar'
import { Page } from '../../../html/components/page'
import { Footer } from '../../../html/components/footer'
import { collections } from '../../../database/collections'
import { GameState } from '../../../database/models/game.model'
import { Tf2ClassName } from '../../../shared/types/tf2-class-name'
import type { PlayerModel } from '../../../database/models/player.model'
import { IconAwardFilled } from '../../../html/components/icons'

interface HallOfFameEntry {
player: PlayerModel
count: number
}

export async function HallOfFamePage(props: { user?: User | undefined }) {
const [all, medics] = await Promise.all([getMostActiveOverall(), getMostActiveMedics()])

return (
<Layout title="Hall of fame" embedStyle={resolve(import.meta.dirname, 'hall-of-fame.page.css')}>
<NavigationBar user={props.user} />
<Page>
<div class="container mx-auto grid grid-cols-1 gap-x-4 gap-y-2 p-2 lg:grid-cols-2 lg:gap-y-0 lg:p-0">
<div class="my-9 text-[48px] font-bold text-abru-light-75 lg:col-span-2">
Hall of Fame
</div>

<Board title="All classes" entries={all} />
<Board title="Medics" entries={medics} />
</div>
</Page>
<Footer user={props.user} />
</Layout>
)
}

function Board(props: { title: string; entries: HallOfFameEntry[] }) {
return (
<div class="hof-board">
<div class="title col-span-4" safe>
{props.title}
</div>
{props.entries.map((record, i) => (
<>
<a class="hof-record" href={`/players/${record.player.steamId}`}>
<MaybeAward i={i} />
<img
src={record.player.avatar.medium}
width="64"
height="64"
class="h-[38px] w-[38px]"
alt="{name}'s avatar"
/>
<span safe>{record.player.name}</span>
<span class="justify-self-end">{record.count}</span>
</a>
</>
))}
</div>
)
}

function MaybeAward(props: { i: number }) {
switch (props.i) {
case 0:
return <IconAwardFilled size={32} class="place-self-center text-place-1st"></IconAwardFilled>
case 1:
return <IconAwardFilled size={32} class="place-self-center text-place-2nd"></IconAwardFilled>
case 2:
return <IconAwardFilled size={32} class="place-self-center text-place-3rd"></IconAwardFilled>
default:
return <span class="place-self-center">{props.i + 1}.</span>
}
}

async function getMostActiveOverall(): Promise<HallOfFameEntry[]> {
return await collections.games
.aggregate<HallOfFameEntry>([
{ $match: { state: GameState.ended } },
{ $unwind: '$slots' },
{ $group: { _id: '$slots.player', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 },
{
$lookup: {
from: 'players',
localField: '_id',
foreignField: '_id',
as: 'player',
},
},
{
$project: {
count: 1,
player: {
$arrayElemAt: ['$player', 0],
},
},
},
])
.toArray()
}

async function getMostActiveMedics(): Promise<HallOfFameEntry[]> {
return await collections.games
.aggregate<HallOfFameEntry>([
{ $match: { state: GameState.ended } },
{ $unwind: '$slots' },
{ $match: { 'slots.gameClass': Tf2ClassName.medic } },
{ $group: { _id: '$slots.player', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 10 },
{
$lookup: {
from: 'players',
localField: '_id',
foreignField: '_id',
as: 'player',
},
},
{
$project: {
count: 1,
player: {
$arrayElemAt: ['$player', 0],
},
},
},
])
.toArray()
}
11 changes: 11 additions & 0 deletions src/html/components/icons/icon-award-filled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { makeIconFilled } from './make-icon'

export const IconAwardFilled = makeIconFilled(
'award-filled',
<>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.496 13.983l1.966 3.406a1.001 1.001 0 0 1 -.705 1.488l-.113 .011l-.112 -.001l-2.933 -.19l-1.303 2.636a1.001 1.001 0 0 1 -1.608 .26l-.082 -.094l-.072 -.11l-1.968 -3.407a8.994 8.994 0 0 0 6.93 -3.999z" />
<path d="M11.43 17.982l-1.966 3.408a1.001 1.001 0 0 1 -1.622 .157l-.076 -.1l-.064 -.114l-1.304 -2.635l-2.931 .19a1.001 1.001 0 0 1 -1.022 -1.29l.04 -.107l.05 -.1l1.968 -3.409a8.994 8.994 0 0 0 6.927 4.001z" />
<path d="M12 2l.24 .004a7 7 0 0 1 6.76 6.996l-.003 .193l-.007 .192l-.018 .245l-.026 .242l-.024 .178a6.985 6.985 0 0 1 -.317 1.268l-.116 .308l-.153 .348a7.001 7.001 0 0 1 -12.688 -.028l-.13 -.297l-.052 -.133l-.08 -.217l-.095 -.294a6.96 6.96 0 0 1 -.093 -.344l-.06 -.271l-.049 -.271l-.02 -.139l-.039 -.323l-.024 -.365l-.006 -.292a7 7 0 0 1 6.76 -6.996l.24 -.004z" />
</>,
)
1 change: 1 addition & 0 deletions src/html/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { IconAdjustments } from './icon-adjustments'
export { IconAlignBoxBottomRight } from './icon-align-box-bottom-right'
export { IconArrowBackUp } from './icon-arrow-back-up'
export { IconArrowsShuffle } from './icon-arrows-shuffle'
export { IconAwardFilled } from './icon-award-filled'
export { IconBan } from './icon-ban'
export { IconBrandDiscord } from './icon-brand-discord'
export { IconChartArrowsVertical } from './icon-chart-arrows-vertical'
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ await app.register((await import('./documents')).default)
await app.register((await import('./statistics')).default)
await app.register((await import('./twitch-tv')).default)
await app.register((await import('./admin')).default)
await app.register((await import('./hall-of-game')).default)

await app.listen({ host: environment.APP_HOST, port: environment.APP_PORT })

0 comments on commit c13fd71

Please sign in to comment.