Skip to content
Draft
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
12 changes: 12 additions & 0 deletions src/lib/api/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export interface RankedUser {
lastSeen: string
}

export interface PopularSongItem {
musicId: number,
weight: number
}

const Routes = {
...Slug(
{
Expand Down Expand Up @@ -89,6 +94,13 @@ const Routes = {
})
},
response: Json<RankedUser[]>()
},
"{}/song-pop": {
authenticated: false,
request: {
method: "GET"
},
response: Json<PopularSongItem[]>()
}
},
games
Expand Down
9 changes: 8 additions & 1 deletion src/lib/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const en = {
"Turnstile failed to validate your network. Disable any VPNs or proxies and please try again."
}
},
missing: `unknown {id:number}`,
// TODO: replace with markdown
return: `<a href="/support">Get support</a> or <a href="/">return home</a>.`
},
Expand Down Expand Up @@ -110,7 +111,13 @@ const en = {
},
home: {
welcome: "Hello, {name:string}!",
songLeaderboard: "{game:string} Top 10 songs",
leaderboard: {
header: "{game:string} Global Song Leaderboard",
playsSuffix: "{plays:number} plays",
},
rankings: {
header: "{game:string} User Rankings"
},
banner: {
title: "Welcome to {aquanet: string}",
description:
Expand Down
53 changes: 44 additions & 9 deletions src/lib/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ type RootTranslation = {
error: string
}
}
/**
* u​n​k​n​o​w​n​ ​{​i​d​}
* @param {number} id
*/
missing: RequiredParams<'id'>
/**
* <​a​ ​h​r​e​f​=​"​/​s​u​p​p​o​r​t​"​>​G​e​t​ ​s​u​p​p​o​r​t​<​/​a​>​ ​o​r​ ​<​a​ ​h​r​e​f​=​"​/​"​>​r​e​t​u​r​n​ ​h​o​m​e​<​/​a​>​.
*/
Expand Down Expand Up @@ -329,11 +334,25 @@ type RootTranslation = {
* @param {string} name
*/
welcome: RequiredParams<'name'>
/**
* {​g​a​m​e​}​ ​T​o​p​ ​1​0​ ​s​o​n​g​s
* @param {string} game
*/
songLeaderboard: RequiredParams<'game'>
leaderboard: {
/**
* {​g​a​m​e​}​ ​G​l​o​b​a​l​ ​S​o​n​g​ ​L​e​a​d​e​r​b​o​a​r​d
* @param {string} game
*/
header: RequiredParams<'game'>
/**
* {​p​l​a​y​s​}​ ​p​l​a​y​s
* @param {number} plays
*/
playsSuffix: RequiredParams<'plays'>
}
rankings: {
/**
* {​g​a​m​e​}​ ​U​s​e​r​ ​R​a​n​k​i​n​g​s
* @param {string} game
*/
header: RequiredParams<'game'>
}
banner: {
/**
* W​e​l​c​o​m​e​ ​t​o​ ​{​a​q​u​a​n​e​t​}
Expand Down Expand Up @@ -803,6 +822,10 @@ export type TranslationFunctions = {
error: () => LocalizedString
}
}
/**
* unknown {id}
*/
missing: (arg: { id: number }) => LocalizedString
/**
* <a href="/support">Get support</a> or <a href="/">return home</a>.
*/
Expand Down Expand Up @@ -997,10 +1020,22 @@ export type TranslationFunctions = {
* Hello, {name}!
*/
welcome: (arg: { name: string }) => LocalizedString
/**
* {game} Top 10 songs
*/
songLeaderboard: (arg: { game: string }) => LocalizedString
leaderboard: {
/**
* {game} Global Song Leaderboard
*/
header: (arg: { game: string }) => LocalizedString
/**
* {plays} plays
*/
playsSuffix: (arg: { plays: number }) => LocalizedString
}
rankings: {
/**
* {game} User Rankings
*/
header: (arg: { game: string }) => LocalizedString
}
banner: {
/**
* Welcome to {aquanet}
Expand Down
18 changes: 17 additions & 1 deletion src/routes/(app)/(nonuser)/home/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import { api } from "$lib/api"
import { music } from "$lib/api/data"
import { coerceToGame } from "$lib/app/formatting.js"
import pickKeys from "$lib/utility/pickKeys"
import { redirect } from "@sveltejs/kit"

export function load({ locals: { user } }) {
export async function load({ locals: { user }, parent }) {
if (!user) throw redirect(307, "/")

const { cardUserGames, activeGame } = await parent()
const targetGame = coerceToGame(activeGame) || "mai2"

let songLeaderboard = (await api(`game/${targetGame}/song-pop`, {})).slice(0, 5)

return {
songLeaderboard,
music: pickKeys(await music(targetGame!),
songLeaderboard.map(v => v.musicId)
)
}
}
43 changes: 18 additions & 25 deletions src/routes/(app)/(nonuser)/home/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
<!-- Note: This is the home page. Landing page is in Landing.svelte. -->
<script lang="ts">
import { formatName, games } from "$lib/app/formatting"
import type { Song } from "$lib/api/data"
import type { PopularSongItem } from "$lib/api/game"
import { formatName, games, type Game } from "$lib/app/formatting"
import Announcement from "$lib/components/generic/Announcement.svelte"
import Banner from "$lib/components/generic/Banner.svelte"
import Song from "$lib/components/generic/Song.svelte"
import Header from "$lib/components/Header.svelte"

import LL from "$lib/i18n/i18n-svelte";
import SongLeaderboard from "./SongLeaderboard.svelte"

let { data } = $props();
interface Props {
data: {
activeGame: Game,
songLeaderboard: PopularSongItem[],
music: Record<string, Song>
}
}
let { data } : Props = $props();
</script>
<Banner
image={{src: "/asset/banner/home/background.webp", alt: "2 people playing maimai"}}
Expand All @@ -23,25 +31,10 @@
content={$LL.gameSetup.banner.description({aquadx: $LL.meta.aquadx()})}
/>
<h2>
{$LL.home.songLeaderboard({game: $LL.games[data.activeGame as keyof typeof $LL.games]()})}
{$LL.home.leaderboard.header({game: $LL.games[data.activeGame as keyof typeof $LL.games]()})}
</h2>
<div class="song-list">
{#each {length: 10} as _}
<Song song={{
thumbnail: "/asset/sample/onimai.png",
difficulty: "MASTER",
level: "13",
name: "ひめごと*クライシスターズ"
}} skill={{
ranking: "SS",
accuracy: 100.2385
}}></Song>
{/each}
</div>
<style lang="scss">
.song-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
</style>
<SongLeaderboard targetGame={data.activeGame} songLeaderboard={data.songLeaderboard} music={data.music} />
<h2>
{$LL.home.rankings.header({game: $LL.games[data.activeGame as keyof typeof $LL.games]()})}
</h2>
<!-- TODO: rework rankings component so it can be included here -->
84 changes: 84 additions & 0 deletions src/routes/(app)/(nonuser)/home/SongLeaderboard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script lang="ts">
import type { Song } from "$lib/api/data"
import type { PopularSongItem } from "$lib/api/game"
import { getMusicJacket } from "$lib/app/assets"
import type { Game } from "$lib/app/formatting"
import LL from "$lib/i18n/i18n-svelte"

interface Props {
targetGame: Game,
music: Record<string, Song>,
songLeaderboard: PopularSongItem[]
}
let { targetGame, music, songLeaderboard }: Props = $props();
let greatestWeight = $derived(songLeaderboard[0].weight);
</script>
<div class="song-leaderboard panel">
{#each songLeaderboard as song, index}
<div class="song-leaderboard-item" style:width={`${(song.weight / greatestWeight) * 100}%`}>
<img class="song-leaderboard-jacket" src={music[song.musicId] ? getMusicJacket(targetGame, song.musicId) : `/asset/fallback/jacket.webp`} alt="Jacket">
<img class="song-leaderboard-background" src={music[song.musicId] ? getMusicJacket(targetGame, song.musicId) : `/asset/fallback/jacket.webp`} alt="Background">

<div class="song-leaderboard-name">
<span style:font-feature-settings={`"tnum"`}>
{index + 1}.
</span>
{music[song.musicId]?.name ?? $LL.errors.missing({id: song.musicId})}
</div>
<div class="song-leaderboard-weight" style:font-feature-settings={`"tnum"`}>
{$LL.home.leaderboard.playsSuffix({plays: song.weight})}
</div>
</div>
{/each}
</div>
<style lang="scss">
.song-leaderboard {
width: 100%;
overflow: hidden;

.song-leaderboard-item {
height: 3em;
position: relative;
overflow: hidden;
border-radius: 0 8px 8px 0;

display: flex;
align-items: center;

.song-leaderboard-weight,
.song-leaderboard-name,
.song-leaderboard-jacket {
z-index: 1;
}
.song-leaderboard-weight {
margin: 0.5em;
margin-left: auto;
}
.song-leaderboard-jacket {
height: calc(100% - 0.5em);
margin: 0.25em;
border-radius: 8px;
}
.song-leaderboard-name {
word-break: break-word;
line-height: 1em;
text-overflow: ellipsis;
overflow: hidden;
margin: 0.75em;
height: 1em;
max-width: calc(100% - 10em);
}
.song-leaderboard-background {
width: calc(100% + 16px);
height: calc(100% + 16px);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translate3d(0, 0, 0); // lmao
opacity: 10%;
filter: blur(4px);
object-fit: cover;
}
}
}
</style>
Binary file added static/asset/fallback/jacket.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed static/asset/sample/banner-sample.jpg
Binary file not shown.
Binary file removed static/asset/sample/onimai.png
Binary file not shown.
Binary file removed static/asset/sample/sample.png
Binary file not shown.