mirror of https://github.com/hykilpikonna/AquaDX
add prettier formatter
parent
e3f931d4f5
commit
38bddf1763
|
@ -75,4 +75,7 @@ gradle-app.setting
|
||||||
|
|
||||||
### Gradle Patch ###
|
### Gradle Patch ###
|
||||||
# Java heap dump
|
# Java heap dump
|
||||||
*.hprof
|
*.hprof
|
||||||
|
|
||||||
|
### Docker ###
|
||||||
|
/db/*
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
```
|
```
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export interface TrendEntry {
|
export interface TrendEntry {
|
||||||
date: string
|
date: string
|
||||||
rating: number
|
rating: number
|
||||||
plays: number
|
plays: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(' ')
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"}]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue