add prettier formatter

js-formatter
Afonso 2024-02-12 18:04:56 +01:00
parent e3f931d4f5
commit 38bddf1763
21 changed files with 1141 additions and 1018 deletions

5
.gitignore vendored
View File

@ -75,4 +75,7 @@ gradle-app.setting
### Gradle Patch ### ### Gradle Patch ###
# Java heap dump # Java heap dump
*.hprof *.hprof
### Docker ###
/db/*

8
.prettierrc 100644
View File

@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"bracketSpacing": false,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -11,6 +11,6 @@ TicketUnlock=true
# Skip the warning screen and logo shown after the POST sequence # Skip the warning screen and logo shown after the POST sequence
SkipWarningScreen=true SkipWarningScreen=true
# Single player: Show 1P only, at the center of the screen # Single player: Show 1P only, at the center of the screen
SinglePlayer=true SinglePlayer=false
# !!EXPERIMENTAL!! Skip from the card-scanning screen directly to music selection screen # !!EXPERIMENTAL!! Skip from the card-scanning screen directly to music selection screen
SkipToMusicSelection=false SkipToMusicSelection=true

View File

@ -1,6 +1,6 @@
# AquaNet # AquaNet
This is the codebase for the new frontend of AquaDX. This is the codebase for the new frontend of AquaDX.
This project is also heavily WIP, so more details will be added later on. This project is also heavily WIP, so more details will be added later on.
## Development ## Development
@ -19,4 +19,3 @@ Finally, run:
yarn install yarn install
yarn dev yarn dev
``` ```

View File

@ -1,35 +1,35 @@
## Technical considerations ## Technical considerations
**Why use this over SvelteKit?** **Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users. - It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?** **Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?** **Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?** **Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts ```ts
// store.ts // store.ts
// An extremely simple external store // An extremely simple external store
import { writable } from 'svelte/store' import {writable} from 'svelte/store'
export default writable(0) export default writable(0)
``` ```

View File

@ -6,15 +6,36 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AquaNet</title> <title>AquaNet</title>
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png"> <link
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png"> rel="apple-touch-icon"
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png"> sizes="180x180"
<link rel="manifest" href="/assets/icons/site.webmanifest"> href="/assets/icons/apple-touch-icon.png"
<link rel="mask-icon" href="/assets/icons/safari-pinned-tab.svg" color="#b3c6ff"> />
<link rel="shortcut icon" href="/assets/icons/favicon.ico"> <link
<meta name="msapplication-TileColor" content="#ffffff"> rel="icon"
<meta name="msapplication-config" content="/assets/icons/browserconfig.xml"> type="image/png"
<meta name="theme-color" content="#ffffff"> sizes="32x32"
href="/assets/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/icons/favicon-16x16.png"
/>
<link rel="manifest" href="/assets/icons/site.webmanifest" />
<link
rel="mask-icon"
href="/assets/icons/safari-pinned-tab.svg"
color="#b3c6ff"
/>
<link rel="shortcut icon" href="/assets/icons/favicon.ico" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta
name="msapplication-config"
content="/assets/icons/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -7,13 +7,16 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json" "check": "svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@iconify/svelte": "^3.1.6", "@iconify/svelte": "^3.1.6",
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2", "@tsconfig/svelte": "^5.0.2",
"chartjs-adapter-moment": "^1.0.1", "chartjs-adapter-moment": "^1.0.1",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2",
"sass": "^1.70.0", "sass": "^1.70.0",
"svelte": "^4.2.10", "svelte": "^4.2.10",
"svelte-check": "^3.6.4", "svelte-check": "^3.6.4",

View File

@ -1,19 +1,19 @@
{ {
"name": "", "name": "",
"short_name": "", "short_name": "",
"icons": [ "icons": [
{ {
"src": "/assets/icons/android-chrome-192x192.png", "src": "/assets/icons/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/assets/icons/android-chrome-512x512.png", "src": "/assets/icons/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }

View File

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Router, Route } from "svelte-routing"; import {Router, Route} from 'svelte-routing'
import Home from "./pages/Home.svelte"; import Home from './pages/Home.svelte'
import MaimaiRating from "./pages/MaimaiRating.svelte"; import MaimaiRating from './pages/MaimaiRating.svelte'
import UserHome from "./pages/UserHome.svelte"; import UserHome from './pages/UserHome.svelte'
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte'
export let url = ""; export let url = ''
</script> </script>
<nav> <nav>
@ -53,4 +53,4 @@
@media (max-width: $w-mobile) @media (max-width: $w-mobile)
justify-content: center justify-content: center
</style> </style>

View File

@ -1,5 +1,5 @@
export interface TrendEntry { export interface TrendEntry {
date: string date: string
rating: number rating: number
plays: number plays: number
} }

View File

@ -1,51 +1,54 @@
import {aqua_host, data_host} from "./config"; import {aqua_host, data_host} from './config'
import type {TrendEntry} from "./generalTypes"; import type {TrendEntry} from './generalTypes'
import type {MaimaiUserSummaryEntry} from "./maimaiTypes"; import type {MaimaiUserSummaryEntry} from './maimaiTypes'
const multTable = [
const multTable = [ [100.5, 22.4, 'SSSp'],
[100.5, 22.4, "SSSp"], [100, 21.6, 'SSS'],
[100, 21.6, "SSS"], [99.5, 21.1, 'SSp'],
[99.5, 21.1, "SSp"], [99, 20.8, 'SS'],
[99, 20.8, "SS"], [98, 20.3, 'Sp'],
[98, 20.3, "Sp"], [97, 20, 'S'],
[97, 20, "S"], [94, 16.8, 'AAA'],
[94, 16.8, "AAA"], [90, 15.2, 'AA'],
[90, 15.2, "AA"], [80, 13.6, 'A'],
[80, 13.6, "A"] ]
]
export function getMult(achievement: number) {
achievement /= 10000
export function getMult(achievement: number) { for (let i = 0; i < multTable.length; i++) {
achievement /= 10000 if (achievement >= (multTable[i][0] as number)) return multTable[i]
for (let i = 0; i < multTable.length; i++) { }
if (achievement >= (multTable[i][0] as number)) return multTable[i] return [0, 0, 0]
} }
return [0, 0, 0]
} export async function getMaimai(endpoint: string, params: any) {
return await fetch(`${aqua_host}/Maimai2Servlet/${endpoint}`, {
method: 'POST',
export async function getMaimai(endpoint: string, params: any) { body: JSON.stringify(params),
return await fetch(`${aqua_host}/Maimai2Servlet/${endpoint}`, { }).then((res) => res.json())
method: "POST", }
body: JSON.stringify(params)
}).then(res => res.json()) export async function getMaimaiAllMusic(): Promise<{[key: string]: any}> {
} return fetch(`${data_host}/maimai/meta/00/all-music.json`).then((it) =>
it.json()
export async function getMaimaiAllMusic(): Promise<{ [key: string]: any }> { )
return fetch(`${data_host}/maimai/meta/00/all-music.json`).then(it => it.json()) }
}
export async function getMaimaiApi(endpoint: string, params: any) {
export async function getMaimaiApi(endpoint: string, params: any) { let url = new URL(`${aqua_host}/api/game/maimai2new/${endpoint}`)
let url = new URL(`${aqua_host}/api/game/maimai2new/${endpoint}`) Object.keys(params).forEach((key) =>
Object.keys(params).forEach(key => url.searchParams.append(key, params[key])) url.searchParams.append(key, params[key])
return await fetch(url).then(res => res.json()) )
} return await fetch(url).then((res) => res.json())
}
export async function getMaimaiTrend(userId: number): Promise<TrendEntry[]> {
return await getMaimaiApi("trend", {userId}) export async function getMaimaiTrend(userId: number): Promise<TrendEntry[]> {
} return await getMaimaiApi('trend', {userId})
}
export async function getMaimaiUser(userId: number): Promise<MaimaiUserSummaryEntry> {
return await getMaimaiApi("user-summary", {userId}) export async function getMaimaiUser(
} userId: number
): Promise<MaimaiUserSummaryEntry> {
return await getMaimaiApi('user-summary', {userId})
}

View File

@ -1,115 +1,115 @@
export interface Rating { export interface Rating {
musicId: number musicId: number
level: number level: number
achievement: number achievement: number
} }
export interface ParsedRating extends Rating { export interface ParsedRating extends Rating {
music: MaimaiMusic, music: MaimaiMusic
calc: number, calc: number
rank: string rank: string
} }
export interface MaimaiMusic { export interface MaimaiMusic {
name: string, name: string
composer: string, composer: string
bpm: number, bpm: number
ver: number, ver: number
note: { note: {
lv: number lv: number
designer: string designer: string
lv_id: number lv_id: number
notes: number notes: number
} }
} }
export interface MaimaiUserSummaryEntry { export interface MaimaiUserSummaryEntry {
name: string name: string
iconId: number iconId: number
serverRank: number serverRank: number
accuracy: number accuracy: number
rating: number rating: number
ratingHighest: number ratingHighest: number
ranks: { name: string, count: number }[] ranks: {name: string; count: number}[]
maxCombo: number maxCombo: number
fullCombo: number fullCombo: number
allPerfect: number allPerfect: number
totalDxScore: number totalDxScore: number
plays: number plays: number
totalPlayTime: number totalPlayTime: number
joined: string joined: string
lastSeen: string lastSeen: string
lastVersion: string lastVersion: string
best35: string best35: string
best15: string best15: string
recent: MaimaiUserPlaylog[] recent: MaimaiUserPlaylog[]
} }
export interface MaimaiUserPlaylog { export interface MaimaiUserPlaylog {
id: number; id: number
musicId: number; musicId: number
level: number; level: number
trackNo: number; trackNo: number
vsRank: number; vsRank: number
achievement: number; achievement: number
deluxscore: number; deluxscore: number
scoreRank: number; scoreRank: number
maxCombo: number; maxCombo: number
totalCombo: number; totalCombo: number
maxSync: number; maxSync: number
totalSync: number; totalSync: number
tapCriticalPerfect: number; tapCriticalPerfect: number
tapPerfect: number; tapPerfect: number
tapGreat: number; tapGreat: number
tapGood: number; tapGood: number
tapMiss: number; tapMiss: number
holdCriticalPerfect: number; holdCriticalPerfect: number
holdPerfect: number; holdPerfect: number
holdGreat: number; holdGreat: number
holdGood: number; holdGood: number
holdMiss: number; holdMiss: number
slideCriticalPerfect: number; slideCriticalPerfect: number
slidePerfect: number; slidePerfect: number
slideGreat: number; slideGreat: number
slideGood: number; slideGood: number
slideMiss: number; slideMiss: number
touchCriticalPerfect: number; touchCriticalPerfect: number
touchPerfect: number; touchPerfect: number
touchGreat: number; touchGreat: number
touchGood: number; touchGood: number
touchMiss: number; touchMiss: number
breakCriticalPerfect: number; breakCriticalPerfect: number
breakPerfect: number; breakPerfect: number
breakGreat: number; breakGreat: number
breakGood: number; breakGood: number
breakMiss: number; breakMiss: number
isTap: boolean; isTap: boolean
isHold: boolean; isHold: boolean
isSlide: boolean; isSlide: boolean
isTouch: boolean; isTouch: boolean
isBreak: boolean; isBreak: boolean
isCriticalDisp: boolean; isCriticalDisp: boolean
isFastLateDisp: boolean; isFastLateDisp: boolean
fastCount: number; fastCount: number
lateCount: number; lateCount: number
isAchieveNewRecord: boolean; isAchieveNewRecord: boolean
isDeluxscoreNewRecord: boolean; isDeluxscoreNewRecord: boolean
comboStatus: number; comboStatus: number
syncStatus: number; syncStatus: number
isClear: boolean; isClear: boolean
beforeRating: number; beforeRating: number
afterRating: number; afterRating: number
beforeGrade: number; beforeGrade: number
afterGrade: number; afterGrade: number
afterGradeRank: number; afterGradeRank: number
beforeDeluxRating: number; beforeDeluxRating: number
afterDeluxRating: number; afterDeluxRating: number
isPlayTutorial: boolean; isPlayTutorial: boolean
isEventMode: boolean; isEventMode: boolean
isFreedomMode: boolean; isFreedomMode: boolean
playMode: number; playMode: number
isNewFree: boolean; isNewFree: boolean
trialPlayAchievement: number; trialPlayAchievement: number
extNum1: number; extNum1: number
extNum2: number; extNum2: number
} }

View File

@ -1,101 +1,116 @@
import { import {
Chart as ChartJS, Chart as ChartJS,
Title, Title,
Tooltip, Tooltip,
Legend, Legend,
LineElement, LineElement,
LinearScale, LinearScale,
PointElement, PointElement,
CategoryScale, TimeScale, type ChartOptions, type LineOptions, CategoryScale,
} from 'chart.js'; TimeScale,
import moment from "moment/moment"; type ChartOptions,
// @ts-ignore type LineOptions,
import CalHeatmap from "cal-heatmap"; } from 'chart.js'
// @ts-ignore import moment from 'moment/moment'
import CalTooltip from 'cal-heatmap/plugins/Tooltip'; // @ts-ignore
import type {Line} from "svelte-chartjs"; import CalHeatmap from 'cal-heatmap'
// @ts-ignore
export function title(t: string) { import CalTooltip from 'cal-heatmap/plugins/Tooltip'
document.title = `AquaNet - ${t}` import type {Line} from 'svelte-chartjs'
}
export function title(t: string) {
export function registerChart() { document.title = `AquaNet - ${t}`
ChartJS.register( }
Title,
Tooltip, export function registerChart() {
Legend, ChartJS.register(
LineElement, Title,
LinearScale, Tooltip,
PointElement, Legend,
CategoryScale, LineElement,
TimeScale LinearScale,
); PointElement,
} CategoryScale,
TimeScale
export function renderCal(el: HTMLElement, d: {date: any, value: any}[]) { )
const cal = new CalHeatmap(); }
return cal.paint({
itemSelector: el, export function renderCal(el: HTMLElement, d: {date: any; value: any}[]) {
domain: { const cal = new CalHeatmap()
type: 'month', return cal.paint(
label: { text: 'MMM', textAlign: 'start', position: 'top' }, {
}, itemSelector: el,
subDomain: { domain: {
type: 'ghDay', type: 'month',
radius: 2, width: 11, height: 11, gutter: 4 label: {text: 'MMM', textAlign: 'start', position: 'top'},
}, },
range: 12, subDomain: {
data: {source: d, x: 'date', y: 'value'}, type: 'ghDay',
scale: { radius: 2,
color: { width: 11,
type: 'linear', height: 11,
range: ['#14432a', '#4dd05a'], gutter: 4,
domain: [0, d.reduce((a, b) => Math.max(a, b.value), 0)] },
}, range: 12,
}, data: {source: d, x: 'date', y: 'value'},
date: {start: moment().subtract(1, 'year').add(1, 'month').toDate()}, scale: {
theme: "dark", color: {
}, [ type: 'linear',
[CalTooltip, {text: (_: Date, v: number, d: any) => range: ['#14432a', '#4dd05a'],
`${v ?? "No"} songs played on ${d.format('MMMM D, YYYY')}`}] domain: [0, d.reduce((a, b) => Math.max(a, b.value), 0)],
]); },
} },
date: {start: moment().subtract(1, 'year').add(1, 'month').toDate()},
theme: 'dark',
export const CHARTJS_OPT: ChartOptions<"line"> = { },
responsive: true, [
maintainAspectRatio: false, [
// TODO: Show point on hover CalTooltip,
elements: { {
point: { text: (_: Date, v: number, d: any) =>
radius: 0 `${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}`,
} },
}, ],
scales: { ]
xAxis: { )
type: 'time', }
display: false
}, export const CHARTJS_OPT: ChartOptions<'line'> = {
y: { responsive: true,
display: false, maintainAspectRatio: false,
} // TODO: Show point on hover
}, elements: {
plugins: { point: {
legend: { radius: 0,
display: false },
}, },
tooltip: { scales: {
mode: "index", xAxis: {
intersect: false type: 'time',
} display: false,
}, },
} y: {
display: false,
/** },
* Usage: clazz({a: false, b: true}) -> "b" },
* plugins: {
* @param obj HashMap<string, boolean> legend: {
*/ display: false,
export function clazz(obj: { [key: string]: boolean }) { },
return Object.keys(obj).filter(k => obj[k]).join(" ") tooltip: {
} mode: 'index',
intersect: false,
},
},
}
/**
* Usage: clazz({a: false, b: true}) -> "b"
*
* @param obj HashMap<string, boolean>
*/
export function clazz(obj: {[key: string]: boolean}) {
return Object.keys(obj)
.filter((k) => obj[k])
.join(' ')
}

View File

@ -4,4 +4,4 @@ import App from './App.svelte'
// @ts-ignore // @ts-ignore
const app = new App({target: document.getElementById('app')}) const app = new App({target: document.getElementById('app')})
export default app export default app

View File

@ -1,83 +1,83 @@
<main id="home" class="no-margin"> <main id="home" class="no-margin">
<h1>AquaNet</h1> <h1>AquaNet</h1>
<div class="btn-group"> <div class="btn-group">
<button>Login</button> <button>Login</button>
<button>Sign Up</button> <button>Sign Up</button>
</div> </div>
<div class="light-pollution"> <div class="light-pollution">
<div class="l1"></div> <div class="l1"></div>
<div class="l2"></div> <div class="l2"></div>
<div class="l3"></div> <div class="l3"></div>
</div> </div>
</main> </main>
<style lang="sass"> <style lang="sass">
@import "../vars" @import "../vars"
#home #home
color: $c-main color: $c-main
position: relative position: relative
width: 100% width: 100%
height: 100% height: 100%
padding-left: 100px padding-left: 100px
overflow: hidden overflow: hidden
box-sizing: border-box box-sizing: border-box
display: flex display: flex
flex-direction: column flex-direction: column
justify-content: center justify-content: center
margin-top: -$nav-height margin-top: -$nav-height
> h1 > h1
font-family: Quicksand, $font font-family: Quicksand, $font
user-select: none user-select: none
// Gap between text characters // Gap between text characters
letter-spacing: 0.2em letter-spacing: 0.2em
margin-top: 0 margin-top: 0
opacity: 0.9 opacity: 0.9
.btn-group .btn-group
display: flex display: flex
gap: 8px gap: 8px
.light-pollution .light-pollution
pointer-events: none pointer-events: none
opacity: 0.6 opacity: 0.6
> div > div
position: absolute position: absolute
z-index: -1 z-index: -1
.l1 .l1
left: -560px left: -560px
top: 90px top: 90px
height: 1130px height: 1130px
width: 1500px width: 1500px
$color: rgb(158, 110, 230) $color: rgb(158, 110, 230)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%) background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%)
.l2 .l2
left: -200px left: -200px
top: 560px top: 560px
height: 1200px height: 1200px
width: 1500px width: 1500px
$color: rgb(92, 195, 250) $color: rgb(92, 195, 250)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%) background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%)
.l3 .l3
left: -600px left: -600px
opacity: 0.7 opacity: 0.7
top: -630px top: -630px
width: 1500px width: 1500px
height: 1000px height: 1000px
$color: rgb(230, 110, 156) $color: rgb(230, 110, 156)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%) background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba($color, 0) 100%)
@media (max-width: 500px) @media (max-width: 500px)
align-items: center align-items: center
padding-left: 0 padding-left: 0
</style> </style>

View File

@ -1,199 +1,219 @@
<script lang="ts"> <script lang="ts">
import {data_host} from "../libs/config"; import {data_host} from '../libs/config'
import {getMaimaiAllMusic, getMaimai, getMult} from "../libs/maimai"; import {getMaimaiAllMusic, getMaimai, getMult} from '../libs/maimai'
import type {ParsedRating, Rating} from "../libs/maimaiTypes"; import type {ParsedRating, Rating} from '../libs/maimaiTypes'
export let userId: any export let userId: any
userId = +userId userId = +userId
if (!userId) console.error("No user ID provided") if (!userId) console.error('No user ID provided')
Promise.all([ Promise.all([
getMaimai("GetUserRatingApi", {userId}), getMaimai('GetUserRatingApi', {userId}),
getMaimaiAllMusic().then(it => it.json()) getMaimaiAllMusic(),
]).then(([rating, music]) => { ]).then(([rating, music]) => {
data = rating data = rating
musicInfo = music musicInfo = music
if (!data || !musicInfo) { if (!data || !musicInfo) {
console.error("Failed to fetch data") console.error('Failed to fetch data')
return return
} }
parsedRatings = { parsedRatings = {
old: parseRating(data.userRating.ratingList), old: parseRating(data.userRating.ratingList),
new: parseRating(data.userRating.newRatingList) new: parseRating(data.userRating.newRatingList),
} }
}) })
function parseRating(arr: Rating[]) { function parseRating(arr: Rating[]) {
return arr.map(x => { return arr
const music = musicInfo[x.musicId] .map((x) => {
const music = musicInfo[x.musicId]
if (!music) {
console.error(`Music not found: ${x.musicId}`) if (!music) {
return null console.error(`Music not found: ${x.musicId}`)
} return null
}
music.note = music.notes[x.level]
const mult = getMult(x.achievement) music.note = music.notes[x.level]
return {...x, const mult = getMult(x.achievement)
music: music, return {
calc: (mult[1] as number) * music.note.lv, ...x,
rank: mult[2] music: music,
} calc: (mult[1] as number) * music.note.lv,
}).filter(x => x != null) as ParsedRating[] rank: mult[2],
} }
})
let parsedRatings: { .filter((x) => x != null) as ParsedRating[]
old: ParsedRating[], }
new: ParsedRating[]
} | null = null let parsedRatings: {
old: ParsedRating[]
let data: { new: ParsedRating[]
userRating: { } | null = null
rating: number,
ratingList: Rating[], let data: {
newRatingList: Rating[] userRating: {
} rating: number
} | null = null ratingList: Rating[]
newRatingList: Rating[]
let musicInfo: any = null }
</script> } | null = null
<main> let musicInfo: any = null
<!-- Display all parsed ratings --> </script>
{#if parsedRatings}
{#each [{title: "Old", data: parsedRatings.old}, {title: "New", data: parsedRatings.new}] as section} <main>
<h2>{section.title}</h2> <!-- Display all parsed ratings -->
<div class="rating-cards"> {#if parsedRatings}
{#each section.data as rating} {#each [{title: 'Old', data: parsedRatings.old}, {title: 'New', data: parsedRatings.new}] as section}
<div class="level-{rating.level}"> <h2>{section.title}</h2>
<img class="cover" src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId.toString().padStart(6, '0').substring(2)}.png`} alt=""> <div class="rating-cards">
{#each section.data as rating}
<div class="detail"> <div class="level-{rating.level}">
<span class="name">{rating.music.name}</span> <img
<span class="rating"> class="cover"
<span>{(rating.achievement / 10000).toFixed(2)}%</span> src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId
<img class="rank" src={`${data_host}/maimai/sprites/rankimage/UI_GAM_Rank_${rating.rank}.png`} alt=""> .toString()
</span> .padStart(6, '0')
<span>{rating.calc.toFixed(1)}</span> .substring(2)}.png`}
</div> alt=""
<img class="ver" src={`${data_host}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver.toString().substring(0, 3)}.png`} alt=""> />
<div class="lv">{rating.music.note.lv}</div>
</div> <div class="detail">
{/each} <span class="name">{rating.music.name}</span>
</div> <span class="rating">
{/each} <span>{(rating.achievement / 10000).toFixed(2)}%</span>
{/if} <img
</main> class="rank"
src={`${data_host}/maimai/sprites/rankimage/UI_GAM_Rank_${rating.rank}.png`}
<style lang="sass"> alt=""
.rating-cards />
display: grid </span>
gap: 2rem <span>{rating.calc.toFixed(1)}</span>
width: 100% </div>
<img
// Fill as many columns as possible class="ver"
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) src={`${data_host}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver
.toString()
// Center the cards .substring(0, 3)}.png`}
justify-items: center alt=""
align-items: center />
<div class="lv">{rating.music.note.lv}</div>
// Style each card </div>
> div {/each}
$border-radius: 20px </div>
width: 200px {/each}
height: 200px {/if}
border-radius: $border-radius </main>
display: flex <style lang="sass">
position: relative .rating-cards
display: grid
// Difficulty border gap: 2rem
border: 5px solid var(--lv-color, #60aaff) width: 100%
&.level-1
--lv-color: #aaff60 // Fill as many columns as possible
&.level-2 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
--lv-color: #f25353
&.level-3 // Center the cards
--lv-color: #e881ff justify-items: center
align-items: center
img
object-fit: cover // Style each card
pointer-events: none > div
$border-radius: 20px
img.cover width: 200px
width: 100% height: 200px
height: 100% border-radius: $border-radius
border-radius: calc($border-radius - 3px)
display: flex
img.ver position: relative
position: absolute
top: -20px // Difficulty border
left: -30px border: 5px solid var(--lv-color, #60aaff)
height: 50px &.level-1
--lv-color: #aaff60
// Information &.level-2
.detail --lv-color: #f25353
position: absolute &.level-3
bottom: 0 --lv-color: #e881ff
left: 0
right: 0 img
padding: 10px object-fit: cover
background: rgba(0, 0, 0, 0.5) pointer-events: none
border-radius: 0 0 calc($border-radius - 3px) calc($border-radius - 3px)
img.cover
// Blur width: 100%
backdrop-filter: blur(3px) height: 100%
border-radius: calc($border-radius - 3px)
display: flex
flex-direction: column img.ver
text-align: left position: absolute
top: -20px
> span left: -30px
// Disable text wrapping, max 2 lines height: 50px
overflow: hidden
text-overflow: ellipsis // Information
white-space: nowrap .detail
position: absolute
.name bottom: 0
font-size: 1.2em left: 0
font-weight: bold right: 0
padding: 10px
.rating background: rgba(0, 0, 0, 0.5)
display: flex border-radius: 0 0 calc($border-radius - 3px) calc($border-radius - 3px)
img
height: 1.5em // Blur
backdrop-filter: blur(3px)
.lv
position: absolute display: flex
bottom: 0 flex-direction: column
right: 0 text-align: left
padding: 5px 10px
background: var(--lv-color) > span
// Top left border radius // Disable text wrapping, max 2 lines
border-radius: 10px 0 overflow: hidden
text-overflow: ellipsis
font-size: 1.3em white-space: nowrap
&:before .name
content: "Lv" font-size: 1.2em
font-size: 0.8em font-weight: bold
// Mobile .rating
@media (max-width: 500px) display: flex
margin-left: -1rem img
margin-right: -1rem height: 1.5em
width: calc(100% + 2rem)
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)) .lv
font-size: 0.8em position: absolute
> div bottom: 0
width: 150px right: 0
height: 150px padding: 5px 10px
background: var(--lv-color)
img.ver // Top left border radius
height: 45px border-radius: 10px 0
left: -20px
</style> font-size: 1.3em
&:before
content: "Lv"
font-size: 0.8em
// Mobile
@media (max-width: 500px)
margin-left: -1rem
margin-right: -1rem
width: calc(100% + 2rem)
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr))
font-size: 0.8em
> div
width: 150px
height: 150px
img.ver
height: 45px
left: -20px
</style>

View File

@ -1,385 +1,426 @@
<script lang="ts"> <script lang="ts">
import {CHARTJS_OPT, clazz, registerChart, renderCal, title} from "../libs/ui"; import {CHARTJS_OPT, clazz, registerChart, renderCal, title} from '../libs/ui'
import {getMaimaiAllMusic, getMaimaiTrend, getMaimaiUser, getMult} from "../libs/maimai"; import {
import type {MaimaiMusic, MaimaiUserPlaylog, MaimaiUserSummaryEntry} from "../libs/maimaiTypes"; getMaimaiAllMusic,
import type {TrendEntry} from "../libs/generalTypes"; getMaimaiTrend,
import {data_host} from "../libs/config"; getMaimaiUser,
import 'cal-heatmap/cal-heatmap.css'; getMult,
import { Line } from 'svelte-chartjs'; } from '../libs/maimai'
import moment from "moment"; import type {
import 'chartjs-adapter-moment'; MaimaiMusic,
MaimaiUserPlaylog,
registerChart() MaimaiUserSummaryEntry,
} from '../libs/maimaiTypes'
export let userId: any; import type {TrendEntry} from '../libs/generalTypes'
userId = +userId import {data_host} from '../libs/config'
let calElement: HTMLElement import 'cal-heatmap/cal-heatmap.css'
import {Line} from 'svelte-chartjs'
title(`User ${userId}`) import moment from 'moment'
import 'chartjs-adapter-moment'
interface MusicAndPlay extends MaimaiMusic, MaimaiUserPlaylog {}
registerChart()
let d: {
user: MaimaiUserSummaryEntry, export let userId: any
trend: TrendEntry[] userId = +userId
recent: MusicAndPlay[] let calElement: HTMLElement
} | null = null
title(`User ${userId}`)
Promise.all([
getMaimaiUser(userId), interface MusicAndPlay extends MaimaiMusic, MaimaiUserPlaylog {}
getMaimaiTrend(userId),
getMaimaiAllMusic() let d: {
]).then(([user, trend, music]) => { user: MaimaiUserSummaryEntry
console.log(user) trend: TrendEntry[]
console.log(trend) recent: MusicAndPlay[]
console.log(music) } | null = null
d = {user, trend, recent: user.recent.map(it => {return {...music[it.musicId], ...it}})} Promise.all([
localStorage.setItem("tmp-user-details", JSON.stringify(d)) getMaimaiUser(userId),
renderCal(calElement, trend.map(it => {return {date: it.date, value: it.plays}})) getMaimaiTrend(userId),
}) getMaimaiAllMusic(),
</script> ]).then(([user, trend, music]) => {
console.log(user)
<main id="user-home"> console.log(trend)
{#if d !== null} console.log(music)
<div class="user-pfp">
<img src={`${data_host}/maimai/assetbundle/icon/${d.user.iconId.toString().padStart(6, "0")}.png`} alt="" class="pfp"> d = {
<h2>{d.user.name}</h2> user,
</div> trend,
recent: user.recent.map((it) => {
<div> return {...music[it.musicId], ...it}
<h2>Rating Statistics</h2> }),
<div class="scoring-info"> }
<div class="chart"> localStorage.setItem('tmp-user-details', JSON.stringify(d))
<div class="info-top"> renderCal(
<div class="rating"> calElement,
<span>DX Rating</span> trend.map((it) => {
<span>{d.user.rating.toLocaleString()}</span> return {date: it.date, value: it.plays}
</div> })
)
<div class="rank"> })
<span>Server Rank</span> </script>
<span>#{d.user.serverRank.toLocaleString()}</span>
</div> <main id="user-home">
</div> {#if d !== null}
<div class="user-pfp">
<div class="trend"> <img
<!-- ChartJS cannot be fully responsive unless there is a parent div that's independent from its size and helps it determine its size --> src={`${data_host}/maimai/assetbundle/icon/${d.user.iconId.toString().padStart(6, '0')}.png`}
<div class="chartjs-box-reference"> alt=""
<Line data={{ class="pfp"
datasets: [ />
{ <h2>{d.user.name}</h2>
label: 'Rating', </div>
data: d.trend.map(it => {return {x: Date.parse(it.date), y: it.rating}}),
borderColor: '#646cff', <div>
tension: 0.1, <h2>Rating Statistics</h2>
<div class="scoring-info">
// TODO: Set X axis span to 3 months <div class="chart">
} <div class="info-top">
] <div class="rating">
}} options={CHARTJS_OPT} /> <span>DX Rating</span>
</div> <span>{d.user.rating.toLocaleString()}</span>
</div> </div>
<div class="info-bottom"> <div class="rank">
{#each d.user.ranks as r} <span>Server Rank</span>
<div> <span>#{d.user.serverRank.toLocaleString()}</span>
<span>{r.name}</span> </div>
<span>{r.count}</span> </div>
</div>
{/each} <div class="trend">
</div> <!-- ChartJS cannot be fully responsive unless there is a parent div that's independent from its size and helps it determine its size -->
</div> <div class="chartjs-box-reference">
<Line
<div class="other-info"> data={{
<div class="accuracy"> datasets: [
<span>Accuracy</span> {
<span>{(d.user.accuracy / 10000).toFixed(2)}%</span> label: 'Rating',
</div> data: d.trend.map((it) => {
return {x: Date.parse(it.date), y: it.rating}
<div class="max-combo"> }),
<span>Max Combo</span> borderColor: '#646cff',
<span>{d.user.maxCombo}</span> tension: 0.1,
</div>
// TODO: Set X axis span to 3 months
<div class="full-combo"> },
<span>Full Combo</span> ],
<span>{d.user.fullCombo}</span> }}
</div> options={CHARTJS_OPT}
/>
<div class="all-perfect"> </div>
<span>All Perfect</span> </div>
<span>{d.user.allPerfect}</span>
</div> <div class="info-bottom">
{#each d.user.ranks as r}
<div class="total-dx-score"> <div>
<span>DX Score</span> <span>{r.name}</span>
<span>{d.user.totalDxScore.toLocaleString()}</span> <span>{r.count}</span>
</div> </div>
</div> {/each}
</div> </div>
</div> </div>
<div> <div class="other-info">
<h2>Play Activity</h2> <div class="accuracy">
<div class="activity-info"> <span>Accuracy</span>
<div id="cal-heatmap" bind:this={calElement} /> <span>{(d.user.accuracy / 10000).toFixed(2)}%</span>
</div>
<div class="info-bottom">
<div class="plays"> <div class="max-combo">
<span>Plays</span> <span>Max Combo</span>
<span>{d.user.plays}</span> <span>{d.user.maxCombo}</span>
</div> </div>
<div class="time"> <div class="full-combo">
<span>Play Time</span> <span>Full Combo</span>
<span>{(d.user.totalPlayTime / 60 / 60).toFixed(1)} hr</span> <span>{d.user.fullCombo}</span>
</div> </div>
<div class="first-play"> <div class="all-perfect">
<span>First Seen</span> <span>All Perfect</span>
<span>{moment(d.user.joined).format("YYYY-MM-DD")}</span> <span>{d.user.allPerfect}</span>
</div> </div>
<div class="last-play"> <div class="total-dx-score">
<span>Last Seen</span> <span>DX Score</span>
<span>{moment(d.user.lastSeen).format("YYYY-MM-DD")}</span> <span>{d.user.totalDxScore.toLocaleString()}</span>
</div> </div>
</div>
<div class="last-version"> </div>
<span>Last Version</span> </div>
<span>{d.user.lastVersion}</span>
</div> <div>
</div> <h2>Play Activity</h2>
</div> <div class="activity-info">
</div> <div id="cal-heatmap" bind:this={calElement} />
<div class="recent"> <div class="info-bottom">
<h2>Recent Scores</h2> <div class="plays">
<div class="scores"> <span>Plays</span>
{#each d.recent as r, i} <span>{d.user.plays}</span>
<div class={clazz({alt: i % 2 === 0})}> </div>
<img src={`${data_host}/maimai/assetbundle/jacket_s/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`} alt="">
<div class="info"> <div class="time">
<span class="name">{r.name}</span> <span>Play Time</span>
<div> <span>{(d.user.totalPlayTime / 60 / 60).toFixed(1)} hr</span>
<span class={"rank-" + ("" + getMult(r.achievement)[2])[0]}> </div>
<span class="rank-text">{("" + getMult(r.achievement)[2]).replace("p", "+")}</span>
<span class="rank-num">{(r.achievement / 10000).toFixed(2)}%</span> <div class="first-play">
</span> <span>First Seen</span>
<span class={"dx-change " + clazz({increased: r.afterDeluxRating - r.beforeDeluxRating > 0})}> <span>{moment(d.user.joined).format('YYYY-MM-DD')}</span>
{r.afterDeluxRating - r.beforeDeluxRating} </div>
</span>
</div> <div class="last-play">
</div> <span>Last Seen</span>
</div> <span>{moment(d.user.lastSeen).format('YYYY-MM-DD')}</span>
{/each} </div>
</div>
</div> <div class="last-version">
{:else} <span>Last Version</span>
<p>Loading...</p> <span>{d.user.lastVersion}</span>
{/if} </div>
</main> </div>
</div>
<style lang="sass"> </div>
@import "../vars"
<div class="recent">
$gap: 20px <h2>Recent Scores</h2>
<div class="scores">
#user-home {#each d.recent as r, i}
display: flex <div class={clazz({alt: i % 2 === 0})}>
flex-direction: column <img
gap: $gap src={`${data_host}/maimai/assetbundle/jacket_s/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`}
margin: 100px auto 0 alt=""
padding: 0 32px 32px />
min-height: 100% <div class="info">
max-width: $w-max <span class="name">{r.name}</span>
<div>
background-color: rgba(black, 0.2) <span class={'rank-' + ('' + getMult(r.achievement)[2])[0]}>
border-radius: 16px 16px 0 0 <span class="rank-text"
>{('' + getMult(r.achievement)[2]).replace('p', '+')}</span
@media (max-width: #{$w-max + (64px) * 2}) >
margin: 100px 32px 0 <span class="rank-num"
padding: 0 32px 16px >{(r.achievement / 10000).toFixed(2)}%</span
>
@media (max-width: $w-mobile) </span>
margin: 100px 0 0 <span
padding: 0 32px 16px class={'dx-change ' +
clazz({
.user-pfp increased: r.afterDeluxRating - r.beforeDeluxRating > 0,
display: flex })}
align-items: flex-end >
gap: $gap {r.afterDeluxRating - r.beforeDeluxRating}
margin-top: -40px </span>
</div>
h2 </div>
font-size: 2rem </div>
margin: 0 {/each}
</div>
.pfp </div>
width: 100px {:else}
height: 100px <p>Loading...</p>
border-radius: 5px {/if}
object-fit: cover </main>
.info-bottom, .info-top, .other-info <style lang="sass">
display: flex @import "../vars"
gap: $gap
$gap: 20px
> div
display: flex #user-home
flex-direction: column display: flex
flex-direction: column
> span:first-child gap: $gap
font-weight: bold margin: 100px auto 0
font-size: 0.8rem padding: 0 32px 32px
min-height: 100%
// character spacing max-width: $w-max
letter-spacing: 0.1em
color: $c-main background-color: rgba(black, 0.2)
border-radius: 16px 16px 0 0
.info-top > div > span:last-child
font-size: 1.5rem @media (max-width: #{$w-max + (64px) * 2})
margin: 100px 32px 0
.scoring-info padding: 0 32px 16px
display: flex
gap: $gap @media (max-width: $w-mobile)
max-height: 250px margin: 100px 0 0
padding: 0 32px 16px
.chart
flex: 0 1 790px .user-pfp
display: flex display: flex
flex-direction: column align-items: flex-end
gap: $gap
.other-info margin-top: -40px
flex: 1 0 100px
flex-direction: column h2
gap: 0 font-size: 2rem
justify-content: space-between margin: 0
.trend .pfp
height: 300px width: 100px
width: 100% height: 100px
max-width: 790px border-radius: 5px
object-fit: cover
position: relative
.info-bottom, .info-top, .other-info
> .chartjs-box-reference display: flex
position: absolute gap: $gap
inset: 0
> div
@media (max-width: $w-mobile) display: flex
flex-direction: column flex-direction: column
max-height: unset
> span:first-child
.chart font-weight: bold
flex: 0 font-size: 0.8rem
.trend // character spacing
max-height: 130px letter-spacing: 0.1em
color: $c-main
.other-info
> div .info-top > div > span:last-child
flex-direction: row font-size: 1.5rem
justify-content: space-between
.scoring-info
.info-bottom display: flex
justify-content: space-between gap: $gap
max-height: 250px
.activity-info
display: flex .chart
flex-direction: column flex: 0 1 790px
gap: $gap display: flex
flex-direction: column
#cal-heatmap
overflow-x: auto .other-info
flex: 1 0 100px
@media (max-width: $w-mobile) flex-direction: column
#cal-heatmap gap: 0
width: 100% justify-content: space-between
.info-bottom .trend
flex-direction: column height: 300px
gap: 0 width: 100%
max-width: 790px
> div
flex-direction: row position: relative
justify-content: space-between
> .chartjs-box-reference
// Recent Scores section position: absolute
.recent inset: 0
.scores
display: flex @media (max-width: $w-mobile)
flex-direction: column flex-direction: column
flex-wrap: wrap max-height: unset
gap: $gap
.chart
> div.alt flex: 0
background-color: rgba(white, 0.03)
border-radius: 10px .trend
max-height: 130px
// Image and song info
> div .other-info
display: flex > div
align-items: center flex-direction: row
gap: $gap justify-content: space-between
padding-right: 16px
max-width: 100% .info-bottom
box-sizing: border-box justify-content: space-between
img .activity-info
width: 50px display: flex
height: 50px flex-direction: column
border-radius: 10px gap: $gap
object-fit: cover
#cal-heatmap
// Song info and score overflow-x: auto
> div
flex: 1 @media (max-width: $w-mobile)
display: flex #cal-heatmap
justify-content: space-between width: 100%
// Limit song name to one line .info-bottom
overflow: hidden flex-direction: column
.name gap: 0
overflow: hidden
overflow-wrap: anywhere > div
white-space: nowrap flex-direction: row
text-overflow: ellipsis justify-content: space-between
@media (max-width: $w-mobile) // Recent Scores section
flex-direction: column .recent
gap: 0 .scores
display: flex
span flex-direction: column
text-align: left flex-wrap: wrap
gap: $gap
.rank-S
// Gold green gradient on text > div.alt
background: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94) background-color: rgba(white, 0.03)
-webkit-background-clip: text border-radius: 10px
color: transparent
// Image and song info
.rank-A > div
color: #ff8a8a display: flex
align-items: center
.rank-B gap: $gap
color: #6ba6ff padding-right: 16px
max-width: 100%
span box-sizing: border-box
display: inline-block
text-align: right img
width: 50px
// Vertical table-like alignment height: 50px
span.rank-text border-radius: 10px
min-width: 30px object-fit: cover
span.rank-num
min-width: 60px // Song info and score
span.dx-change > div
min-width: 30px flex: 1
display: flex
span.increased justify-content: space-between
&:before
content: "+" // Limit song name to one line
color: $c-good overflow: hidden
</style> .name
overflow: hidden
overflow-wrap: anywhere
white-space: nowrap
text-overflow: ellipsis
@media (max-width: $w-mobile)
flex-direction: column
gap: 0
span
text-align: left
.rank-S
// Gold green gradient on text
background: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94)
-webkit-background-clip: text
color: transparent
.rank-A
color: #ff8a8a
.rank-B
color: #6ba6ff
span
display: inline-block
text-align: right
// Vertical table-like alignment
span.rank-text
min-width: 30px
span.rank-num
min-width: 60px
span.dx-change
min-width: 30px
span.increased
&:before
content: "+"
color: $c-good
</style>

View File

@ -1,4 +1,4 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'
export default { export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess // Consult https://svelte.dev/docs#compile-time-svelte-preprocess

View File

@ -16,5 +16,5 @@
"isolatedModules": true "isolatedModules": true
}, },
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{"path": "./tsconfig.node.json"}]
} }

View File

@ -1,5 +1,5 @@
import { defineConfig } from 'vite' import {defineConfig} from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte' import {svelte} from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({

View File

@ -1047,6 +1047,16 @@ postcss@^8.4.35:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
prettier-plugin-svelte@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-3.1.2.tgz#2e050eb56dbb467a42c45ad6ce18bb277d28ffa0"
integrity sha512-7xfMZtwgAWHMT0iZc8jN4o65zgbAQ3+O32V6W7pXrqNvKnHnkoyQCGCbKeUyXKZLbYE0YhFRnamfxfkEGxm8qA==
prettier@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
queue-microtask@^1.2.2: queue-microtask@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"