feat: initial commit
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
15
.prettierrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
7
LICENSE
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright 2024 metamethods
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
34
eslint.config.js
Normal file
@ -0,0 +1,34 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "chuni-national-matchmaking-webui",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/plus-jakarta-sans": "^5.1.1",
|
||||
"axios": "^1.7.9",
|
||||
"lucide-svelte": "^0.469.0"
|
||||
}
|
||||
}
|
2807
pnpm-lock.yaml
generated
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
7
src/app.css
Normal file
@ -0,0 +1,7 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
a {
|
||||
@apply text-accent;
|
||||
}
|
13
src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="bg-dark text-light">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
20
src/lib/BattleRank.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { RANK_IMAGE_WIDTH, RANK_IMAGE_HEIGHT, imageOffset } from './battleRank';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let { rank, ...rest }: { rank: number } & HTMLAttributes<HTMLDivElement> = $props();
|
||||
|
||||
const [xOffset, yOffset] = imageOffset(rank);
|
||||
</script>
|
||||
|
||||
<div {...rest}>
|
||||
<svg viewBox="0 0 {RANK_IMAGE_WIDTH} {RANK_IMAGE_HEIGHT}" xmlns="http://www.w3.org/2000/svg">
|
||||
<image
|
||||
id="img"
|
||||
href="/battle_ranks.png"
|
||||
width="638"
|
||||
height="475"
|
||||
transform="translate({-xOffset} {-yOffset})"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
8
src/lib/Footer.svelte
Normal file
@ -0,0 +1,8 @@
|
||||
<footer class="bg-medium">
|
||||
<div class="mx-auto w-full max-w-screen-xl p-4">
|
||||
<p>
|
||||
made with <a href="https://svelte.dev/">sveltekit</a>, powered by
|
||||
<a href="https://cloudflare.com/">cloudflare</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
11
src/lib/GameVersion.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { fromVersionString } from './versioning';
|
||||
import gameVersions from './gameVersions';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let { version, ...rest }: { version: string } & HTMLAttributes<HTMLImageElement> = $props();
|
||||
|
||||
const { minor } = fromVersionString(version);
|
||||
</script>
|
||||
|
||||
<img src="/versions/{gameVersions[minor.toString()]}" alt={version} {...rest} />
|
21
src/lib/Player.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import BattleRank from './BattleRank.svelte';
|
||||
import Rating from './Rating.svelte';
|
||||
import Team from './Team.svelte';
|
||||
import type { Player } from './types';
|
||||
|
||||
let { player }: { player: Player } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p>{player.name}</p>
|
||||
<div class="flex gap-2">
|
||||
<Rating rating={player.rating} />
|
||||
<BattleRank rank={player.battleRank} class="w-16" />
|
||||
</div>
|
||||
</div>
|
||||
{#if player.team}
|
||||
<Team team={player.team} />
|
||||
{/if}
|
||||
</div>
|
18
src/lib/Rating.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import RatingCharacter from './RatingCharacter.svelte';
|
||||
import { ratingType } from './ratings';
|
||||
|
||||
let { rating }: { rating: number } = $props();
|
||||
|
||||
const characters = rating.toString().padStart(4, '0');
|
||||
const type = ratingType(rating);
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#each characters as ratingCharacter, i}
|
||||
{#if i == characters.length - 2}
|
||||
<RatingCharacter character={'.'} {type} class="w-3" />
|
||||
{/if}
|
||||
<RatingCharacter character={Number(ratingCharacter)} {type} class="w-3" />
|
||||
{/each}
|
||||
</div>
|
27
src/lib/RatingCharacter.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { CHARACTER_IMAGE_WIDTH, CHARACTER_IMAGE_HEIGHT, imageOffset } from './ratings';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
character,
|
||||
type,
|
||||
...rest
|
||||
}: { character: number | '.'; type: number } & HTMLAttributes<HTMLDivElement> = $props();
|
||||
|
||||
const [xOffset, yOffset] = imageOffset(character, type);
|
||||
</script>
|
||||
|
||||
<div {...rest}>
|
||||
<svg
|
||||
viewBox="0 0 {CHARACTER_IMAGE_WIDTH} {CHARACTER_IMAGE_HEIGHT}"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<image
|
||||
id="img"
|
||||
href="/ratings.png"
|
||||
width="247"
|
||||
height="159"
|
||||
transform="translate({-xOffset} {-yOffset})"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
45
src/lib/Room.svelte
Normal file
@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { User, Calendar } from 'lucide-svelte';
|
||||
import Player from './Player.svelte';
|
||||
import BattleRank from './BattleRank.svelte';
|
||||
import GameVersion from './GameVersion.svelte';
|
||||
import Separator from './Separator.svelte';
|
||||
import type { Room } from './types';
|
||||
|
||||
let { room }: { room: Room } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8 rounded-lg bg-medium p-6">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col items-center gap-4 sm:flex-row sm:items-start">
|
||||
<GameVersion version={room.gameVersion} class="w-48 sm:w-32" />
|
||||
<BattleRank rank={room.roomBattleRank} class="w-28 sm:w-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<p class="flex items-center gap-2 text-sub">game version <Separator /> {room.gameVersion}</p>
|
||||
<p class="flex items-center gap-2 text-sub">
|
||||
room battle rank <Separator />
|
||||
{room.roomBattleRank}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="flex items-center gap-2">
|
||||
<User class="inline-block w-6" />
|
||||
{room.players.length} / 4
|
||||
<Separator />
|
||||
<Calendar class="inline-block w-6" />
|
||||
{room.createdAt.toLocaleString()}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:grid-rows-2">
|
||||
{#each room.players as player}
|
||||
<Player {player} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
1
src/lib/Separator.svelte
Normal file
@ -0,0 +1 @@
|
||||
<div class="inline-block h-[2px] w-4 rounded-full bg-current brightness-50"></div>
|
41
src/lib/Team.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import type { Team } from './types';
|
||||
|
||||
let { team }: { team: Team } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between rounded-md bg-dark px-2 py-1 {team.rank == 1
|
||||
? 'team-first'
|
||||
: team.rank == 2
|
||||
? 'team-second'
|
||||
: team.rank == 3
|
||||
? 'team-third'
|
||||
: ''}"
|
||||
>
|
||||
<p>
|
||||
{team.name}
|
||||
</p>
|
||||
|
||||
{#if team.rank <= 5}
|
||||
<p class="font-bold">
|
||||
#{team.rank}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.team-first {
|
||||
background-image: linear-gradient(135deg, #f4a7fd 0%, #8768f4 50%, #55f28b 100%);
|
||||
|
||||
@apply text-dark;
|
||||
}
|
||||
|
||||
.team-second {
|
||||
@apply bg-gradient-to-br from-yellow-400 to-yellow-600 text-dark;
|
||||
}
|
||||
|
||||
.team-third {
|
||||
@apply bg-gradient-to-br from-teal-400 to-blue-600 text-dark;
|
||||
}
|
||||
</style>
|
3
src/lib/array.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function arrayPadEnd<T>(array: T[], amount: number, data: T) {
|
||||
for (let i = 0; i < amount - array.length; i++) array.push(data);
|
||||
}
|
14
src/lib/battleRank.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const RANK_IMAGE_WIDTH = 120;
|
||||
export const RANK_IMAGE_HEIGHT = 57;
|
||||
|
||||
const ROWS = 5;
|
||||
const ROWS_SPACING = 41;
|
||||
const COLUMNS = 5;
|
||||
const COLUMS_SPACING = 10;
|
||||
|
||||
export function imageOffset(rank: number): [number, number] {
|
||||
const row = rank % ROWS;
|
||||
const column = ~~(rank / COLUMNS);
|
||||
|
||||
return [(RANK_IMAGE_WIDTH + COLUMS_SPACING) * row, (RANK_IMAGE_HEIGHT + ROWS_SPACING) * column];
|
||||
}
|
22
src/lib/gameVersions.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// we are just assuming that major is just gonna stay at 2
|
||||
// then just map every minor version for the game version here
|
||||
export default {
|
||||
'0': 'new.png',
|
||||
'1': 'new.png',
|
||||
'2': 'new.png',
|
||||
|
||||
'5': 'new_plus.png',
|
||||
|
||||
'10': 'sun.png',
|
||||
'11': 'sun.png',
|
||||
|
||||
'15': 'sun_plus.png',
|
||||
'16': 'sun_plus.png',
|
||||
|
||||
'20': 'luminous.png',
|
||||
'22': 'luminous.png',
|
||||
|
||||
'25': 'luminous_plus.png',
|
||||
'26': 'luminous_plus.png',
|
||||
'27': 'luminous_plus.png'
|
||||
} as Record<string, string>;
|
26
src/lib/ratings.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export const CHARACTER_IMAGE_WIDTH = 14;
|
||||
export const CHARACTER_IMAGE_HEIGHT = 19;
|
||||
|
||||
const ROWS = 6;
|
||||
const ROWS_SPACING = 9;
|
||||
const COLUMNS = 11;
|
||||
const COLUMS_SPACING = 10;
|
||||
|
||||
export function ratingType(rating: number): number {
|
||||
if (rating <= 1199) return 4;
|
||||
else if (rating <= 1324) return 3;
|
||||
else if (rating <= 1449) return 2;
|
||||
else if (rating <= 1524) return 1;
|
||||
else if (rating <= 1599) return 0;
|
||||
else return 5;
|
||||
}
|
||||
|
||||
export function imageOffset(character: number | '.', type: number): [number, number] {
|
||||
const row = type % ROWS;
|
||||
const column = (character == '.' ? 10 : character) % COLUMNS;
|
||||
|
||||
return [
|
||||
(CHARACTER_IMAGE_WIDTH + COLUMS_SPACING) * column,
|
||||
(CHARACTER_IMAGE_HEIGHT + ROWS_SPACING) * row
|
||||
];
|
||||
}
|
55
src/lib/server/fetchData.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type { APIPlayer, APIRoom, Player, Room } from '$lib/types';
|
||||
import { fromVersionString, greaterThan } from '$lib/versioning';
|
||||
import axios from 'axios';
|
||||
import type { FetchFunction } from 'vite';
|
||||
|
||||
function toPlayer(apiPlayer: APIPlayer): Player {
|
||||
return {
|
||||
name: apiPlayer.userName,
|
||||
rating: Number(apiPlayer.playerRating),
|
||||
battleRank: Number(apiPlayer.battleRankId),
|
||||
team:
|
||||
apiPlayer.isJoinTeam == 'true'
|
||||
? {
|
||||
name: apiPlayer.teamName,
|
||||
rank: Number(apiPlayer.teamRank) - 1 // team ranks are one off for some reason?
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function toRoom(apiRoom: APIRoom): Room {
|
||||
return {
|
||||
id: apiRoom.roomId,
|
||||
createdAt: new Date(apiRoom.updatedAt),
|
||||
gameVersion: apiRoom.dataVersion,
|
||||
roomBattleRank: apiRoom.roomRanking,
|
||||
players: apiRoom.matchingMemberInfoList.map((apiPlayer) => toPlayer(apiPlayer))
|
||||
};
|
||||
}
|
||||
|
||||
function filterRooms(rooms: Room[]): Room[] {
|
||||
return rooms.filter(
|
||||
(room) => !greaterThan(fromVersionString(room.gameVersion), fromVersionString(env.MAX_VERSION))
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchData() {
|
||||
return {
|
||||
activeRooms: await axios
|
||||
.get<APIRoom[]>('http://yukiotoko.chara.lol:9000/api/active', {
|
||||
headers: {
|
||||
Authorization: env.YUKIOTOKO_API_TOKEN
|
||||
}
|
||||
})
|
||||
.then((result) => filterRooms(result.data.map((apiRoom) => toRoom(apiRoom)))),
|
||||
archivedRooms: await axios
|
||||
.get<APIRoom[]>('http://yukiotoko.chara.lol:9000/api/history', {
|
||||
headers: {
|
||||
Authorization: env.YUKIOTOKO_API_TOKEN
|
||||
}
|
||||
})
|
||||
.then((result) => filterRooms(result.data.map((apiRoom) => toRoom(apiRoom))))
|
||||
};
|
||||
}
|
79
src/lib/types.ts
Normal file
@ -0,0 +1,79 @@
|
||||
export interface Team {
|
||||
name: string;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
name: string;
|
||||
rating: number;
|
||||
battleRank: number;
|
||||
team?: Team;
|
||||
}
|
||||
|
||||
export interface APIPlayer {
|
||||
errCnt: string;
|
||||
userId: string;
|
||||
placeId: string;
|
||||
skillId: string;
|
||||
skillLv: string;
|
||||
clientId?: unknown;
|
||||
joinTime: string;
|
||||
reginId: string;
|
||||
teamName: string;
|
||||
teamRank: string;
|
||||
trophyId: string;
|
||||
userName: string;
|
||||
messageId: string;
|
||||
emblemBase: string;
|
||||
hostErrCnt: string;
|
||||
isJoinTeam: string;
|
||||
romVersion: string;
|
||||
avatarEquip: {
|
||||
backID: string;
|
||||
faceID: string;
|
||||
headID: string;
|
||||
itemID: string;
|
||||
skinID: string;
|
||||
wearID: string;
|
||||
frontID: string;
|
||||
};
|
||||
characterId: string;
|
||||
dataVersion: string;
|
||||
emblemMedal: string;
|
||||
optRatingId: string;
|
||||
battleIconId: string;
|
||||
battleRankId: string;
|
||||
playerRating: string;
|
||||
battleIconNum: string;
|
||||
bestRatingAvg: string;
|
||||
characterRank: string;
|
||||
avatarEffectID: string;
|
||||
genreGraphList: { genreId: string; musicCount: string }[];
|
||||
giftMusicIdList: { musicId: string }[];
|
||||
skillIdForChara: string;
|
||||
battleCorrection: string;
|
||||
ratingEffectColorId: string;
|
||||
}
|
||||
|
||||
export interface Room {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
gameVersion: string;
|
||||
roomBattleRank: number;
|
||||
players: Player[];
|
||||
}
|
||||
|
||||
export interface APIRoom {
|
||||
userId: string;
|
||||
roomId: number;
|
||||
dataVersion: string;
|
||||
romVersion: string;
|
||||
roomRanking: number;
|
||||
roomMSec: number;
|
||||
isFull: boolean;
|
||||
matchingMemberInfoList: APIPlayer[];
|
||||
isFinished: boolean;
|
||||
allowAnybody: boolean;
|
||||
updatedAt: string;
|
||||
mergedRoom?: unknown;
|
||||
}
|
20
src/lib/versioning.ts
Normal file
@ -0,0 +1,20 @@
|
||||
interface Version {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
}
|
||||
|
||||
export function fromVersionString(versionString: string): Version {
|
||||
const [_, major, minor, patch] = /(\d+)\.(\d+)\.(\d+)/.exec(versionString) ?? [null, -1, -1, -1];
|
||||
return { major: Number(major), minor: Number(minor), patch: Number(patch) };
|
||||
}
|
||||
|
||||
export function greaterThan(versionA: Version, versionB: Version): boolean {
|
||||
return (
|
||||
versionA.major > versionB.major ||
|
||||
(versionA.major === versionB.major && versionA.minor > versionB.minor) ||
|
||||
(versionA.major === versionB.major &&
|
||||
versionA.minor === versionB.minor &&
|
||||
versionA.patch > versionB.patch)
|
||||
);
|
||||
}
|
15
src/routes/+layout.svelte
Normal file
@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Footer from '$lib/Footer.svelte';
|
||||
|
||||
import '@fontsource-variable/plus-jakarta-sans';
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen">
|
||||
<main class="mx-auto w-full max-w-screen-xl p-4">
|
||||
{@render children()}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Footer />
|
6
src/routes/+page.server.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fetchData } from '$lib/server/fetchData';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch }) => {
|
||||
return await fetchData();
|
||||
};
|
68
src/routes/+page.svelte
Normal file
@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { arrayPadEnd } from '$lib/array';
|
||||
import Room from '$lib/Room.svelte';
|
||||
import axios from 'axios';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let props: { data: PageData } = $props();
|
||||
|
||||
let data = $state(props.data);
|
||||
let refreshTimer = $state(60);
|
||||
|
||||
async function refresh() {
|
||||
data = await axios.get('/api/data').then((result) => result.data);
|
||||
}
|
||||
|
||||
$effect(() =>
|
||||
data.archivedRooms.forEach((archivedRoom) =>
|
||||
arrayPadEnd(archivedRoom.players, 4, {
|
||||
name: 'CPU',
|
||||
battleRank: 0,
|
||||
rating: 0
|
||||
})
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="mb-16 space-y-8">
|
||||
<section>
|
||||
<h1 class="text-4xl font-bold">yukiotoko webui</h1>
|
||||
<p>a frontend redesign for <a href="http://yukiotoko.chara.lol/">yukiotoko</a></p>
|
||||
</section>
|
||||
<div>
|
||||
<button class="bg-accent rounded-lg px-3 py-2" onclick={refresh}>Refresh</button>
|
||||
<p class="text-sub">load the current data from yukiotoko</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<section class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">
|
||||
active rooms <span class="text-sub">({data.activeRooms.length})</span>
|
||||
</h1>
|
||||
<p class="text-sub">all of the currently matchmaking rooms</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
{#each data.activeRooms as activeRoom}
|
||||
<Room room={activeRoom} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">
|
||||
archived rooms <span class="text-sub">({data.archivedRooms.length})</span>
|
||||
</h1>
|
||||
<p class="text-sub">rooms that were created from the last 24 hours</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
{#each data.archivedRooms as archivedRoom}
|
||||
<Room room={archivedRoom} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
5
src/routes/api/data/+server.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { fetchData } from '$lib/server/fetchData';
|
||||
|
||||
export async function GET({}) {
|
||||
return new Response(JSON.stringify(await fetchData()));
|
||||
}
|
BIN
static/battle_ranks.png
Normal file
After Width: | Height: | Size: 236 KiB |
BIN
static/favicon.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/ratings.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
static/versions/luminous.png
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
static/versions/luminous_plus.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
static/versions/new.png
Normal file
After Width: | Height: | Size: 395 KiB |
BIN
static/versions/new_plus.png
Normal file
After Width: | Height: | Size: 663 KiB |
BIN
static/versions/sun.png
Normal file
After Width: | Height: | Size: 703 KiB |
BIN
static/versions/sun_plus.png
Normal file
After Width: | Height: | Size: 714 KiB |
18
svelte.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
22
tailwind.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: '#060710',
|
||||
medium: '#0A0B17',
|
||||
light: '#CFD3FA',
|
||||
sub: '#4D4D80',
|
||||
accent: '#7E61FF'
|
||||
},
|
||||
|
||||
fontFamily: {
|
||||
sans: ['Plus Jakarta Sans Variable', ...defaultTheme.fontFamily.sans]
|
||||
}
|
||||
}
|
||||
}
|
||||
} satisfies Config;
|
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
6
vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|