Compare commits

...

17 Commits

20 changed files with 186 additions and 74 deletions

View File

@ -1,5 +1,5 @@
{
"name": "chuni-national-matchmaking-webui",
"name": "yukiotoko",
"private": true,
"version": "0.0.1",
"type": "module",

View File

@ -7,7 +7,7 @@
<title>yukiotoko</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="bg-dark text-light">
<body data-sveltekit-preload-data="hover" class="bg-dark text-text">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -4,7 +4,7 @@
let { rank, ...rest }: { rank: number } & HTMLAttributes<HTMLDivElement> = $props();
const [xOffset, yOffset] = imageOffset(rank);
const [xOffset, yOffset] = $derived(imageOffset(rank));
</script>
<div {...rest}>

View File

@ -1,16 +1,31 @@
<script lang="ts">
const { date }: { date: Date } = $props();
const difference = new Date().getTime() / 1000 - date.getTime() / 1000;
const seconds = ~~(difference % 60);
const minutes = ~~(difference / 60) % 60;
const hours = ~~(difference / 3600) % 24;
const days = ~~(difference / 86400);
let { date }: { date: Date } = $props();
let currentDate = $state(Date.now());
let difference = $derived(currentDate / 1000 - date.getTime() / 1000);
let seconds = $derived(~~(difference % 60));
let minutes = $derived(~~(difference / 60) % 60);
let hours = $derived(~~(difference / 3600) % 24);
let days = $derived(~~(difference / 86400));
setInterval(() => {
currentDate = Date.now();
});
</script>
<p class="group underline">
<span class="group text-light underline">
<span class="group-hover:hidden">
about
{#if minutes == 0}
{#if days > 0}
<span>{days}d</span>
{:else if hours > 0}
<span>{hours}h</span>
{:else if minutes > 0}
<span>{minutes}m</span>
{:else}
<span>{seconds}s</span>
{/if}
<!-- {#if minutes == 0}
<span>{seconds}s</span>
{:else if hours == 0}
<span>{minutes}m</span>
@ -18,10 +33,10 @@
<span>{hours}h</span>
{:else}
<span>{days}d</span>
{/if}
{/if} -->
ago
</span>
<span class="hidden group-hover:block">
{date.toLocaleString()}
</span>
</p>
</span>

View File

@ -1,4 +1,4 @@
<footer class="bg-medium">
<footer class="bg-medium text-light">
<div class="mx-auto flex w-full max-w-screen-xl flex-col p-4 md:flex-row md:justify-between">
<p>
made with <a href="https://svelte.dev/">sveltekit</a>, powered by
@ -6,8 +6,8 @@
</p>
<p>
you can find the repo of the website <a href="https://tea.metatable.sh/meta/yukiotoko-webui"
>here</a
wana help out, or issue a bug? you can find the repo <a
href="https://tea.metatable.sh/meta/yukiotoko-webui">here</a
>
</p>
</div>

View File

@ -5,7 +5,7 @@
let { version, ...rest }: { version: string } & HTMLAttributes<HTMLImageElement> = $props();
const { minor } = fromVersionString(version);
const { minor } = $derived(fromVersionString(version));
</script>
<img src="/versions/{gameVersions[minor.toString()]}" alt={version} {...rest} />

40
src/lib/Pagination.svelte Normal file
View File

@ -0,0 +1,40 @@
<script lang="ts" generics="T">
let {
items,
itemsPerPage,
pageItems = $bindable()
}: { items: Array<T>; itemsPerPage: number; pageItems: Array<T> } = $props();
let currentPage = $state(0);
let pageIndexStart = $derived(currentPage * itemsPerPage);
let pageIndexEnd = $derived(Math.min(itemsPerPage * (currentPage + 1), items.length));
$effect(() => {
currentPage = Math.min(Math.max(currentPage, 0), ~~(items.length / itemsPerPage));
pageItems = items.slice(pageIndexStart, pageIndexEnd);
});
</script>
<div class="flex flex-col gap-2">
<p>
showing <span class="font-bold text-light">{pageIndexStart + 1}</span> to
<span class="font-bold text-light">{pageIndexEnd}</span>
of <span class="font-bold text-light">{items.length}</span>
entires
</p>
<div class="flex divide-x divide-border">
<button class="rounded-bl-lg rounded-tl-lg text-light" onclick={() => currentPage--}
>prev</button
>
<button class="rounded-br-lg rounded-tr-lg text-light" onclick={() => currentPage++}
>next</button
>
</div>
</div>
<style>
button {
@apply bg-medium px-4 py-2 transition hover:brightness-150;
}
</style>

View File

@ -9,13 +9,11 @@
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<p>{player.name}</p>
<p class="font-bold text-light">{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}
<Team team={player?.team ?? { name: 'No Team', rank: 0 }} />
</div>

View File

@ -4,8 +4,8 @@
let { rating }: { rating: number } = $props();
const characters = rating.toString().padStart(4, '0');
const type = ratingType(rating);
const characters = $derived(rating.toString().padStart(4, '0'));
const type = $derived(ratingType(rating));
</script>
<div class="flex items-center">

View File

@ -8,7 +8,7 @@
...rest
}: { character: number | '.'; type: number } & HTMLAttributes<HTMLDivElement> = $props();
const [xOffset, yOffset] = imageOffset(character, type);
const [xOffset, yOffset] = $derived(imageOffset(character, type));
</script>
<div {...rest}>

View File

@ -1,20 +1,44 @@
<script lang="ts">
import { User, Calendar } from 'lucide-svelte';
import { onMount } from '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';
import Date from './Date.svelte';
import axios from 'axios';
import type { Room } from './types';
let { room }: { room: Room } = $props();
let secondsRemaining = $state(room.secondsRemaining);
let roomTimer = $state(room.secondsRemaining);
let players = $state(room.players);
const interval = setInterval(() => {
if (secondsRemaining <= 0 || room.finished) clearInterval(interval);
secondsRemaining--;
onMount(() => {
if (!room.finished) {
const roomTimerUpdateInterval = setInterval(() => {
if (roomTimer <= 0) {
roomTimer = 0;
return;
}
roomTimer--;
}, 1000);
const roomTimerSyncInterval = setInterval(async () => {
console.log('updating');
const updatedRoom = await axios
.get<Room>(`/api/room/${room.roomId}`)
.then((result) => result.data);
roomTimer = updatedRoom.secondsRemaining;
players = updatedRoom.players;
if (updatedRoom.finished) {
console.log('killing invervals');
clearInterval(roomTimerUpdateInterval);
clearInterval(roomTimerSyncInterval);
}
}, 5000);
}
});
</script>
<div class="flex flex-col gap-8 rounded-lg bg-medium p-6">
@ -27,19 +51,19 @@
</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">
<p class="flex items-center gap-2">game version <Separator /> {room.gameVersion}</p>
<p class="flex items-center gap-2">
room battle rank <Separator />
{room.roomBattleRank}
</p>
{#if !room.finished}
<p class="flex items-center gap-2">seconds remaining <Separator /> {secondsRemaining}s</p>
<p class="flex items-center gap-2">seconds remaining <Separator /> {roomTimer}s</p>
{/if}
</div>
</div>
<div class="flex flex-col gap-4">
<p class="flex items-center gap-2">
<p class="flex items-center gap-2 text-light">
<User class="inline-block w-6" />
{room.players.length} / 4
<Separator />
@ -48,7 +72,7 @@
</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:grid-rows-2">
{#each room.players as player}
{#each players as player}
<Player {player} />
{/each}
</div>

View File

@ -1,3 +1 @@
<span class="inline-block"
><div class="h-[2px] w-4 rounded-full bg-current brightness-50"></div></span
>
<span class="inline-block h-[2px] w-4 rounded-full bg-current brightness-50"></span>

View File

@ -11,7 +11,7 @@
? 'team-second'
: team.rank == 3
? 'team-third'
: ''}"
: 'text-light'}"
>
<p>
{team.name}

View File

@ -1,3 +1,5 @@
export function arrayPadEnd<T>(array: T[], amount: number, data: T) {
for (let i = 0; i < amount - array.length; i++) array.push(data);
if (amount > array.length) {
array.push(...Array(amount - array.length).fill(data));
}
}

View File

@ -20,7 +20,6 @@ function toPlayer(apiPlayer: APIPlayer): Player {
function toRoom(apiRoom: APIRoom): Room {
return {
id: apiRoom.id,
roomId: apiRoom.roomId,
createdAt: new Date(apiRoom.updatedAt),
gameVersion: apiRoom.dataVersion,
@ -31,10 +30,12 @@ function toRoom(apiRoom: APIRoom): Room {
};
}
function validRoom(room: Room): boolean {
return !greaterThan(fromVersionString(room.gameVersion), fromVersionString(env.MAX_VERSION));
}
function filterRooms(rooms: Room[]): Room[] {
return rooms.filter(
(room) => !greaterThan(fromVersionString(room.gameVersion), fromVersionString(env.MAX_VERSION))
);
return rooms.filter((room) => validRoom(room));
}
export async function fetchRooms() {
@ -56,7 +57,20 @@ export async function fetchRooms() {
};
}
// TODO!
// export async function fetchRoom(roomId: string): Promise<Room> {
// return new Promise()
// }
export async function fetchRoom(roomId: number | string): Promise<Room | undefined> {
const result = await axios.get<APIRoom[]>(`http://yukiotoko.chara.lol:9000/api/get/${roomId}`, {
headers: {
Authorization: env.YUKIOTOKO_API_TOKEN
}
});
const apiRoom = result.data[0];
if (!apiRoom) return;
const room = toRoom(apiRoom);
if (!validRoom(room)) return;
return room;
}

View File

@ -56,7 +56,6 @@ export interface APIPlayer {
}
export interface Room {
id: string;
roomId: number;
createdAt: Date;
gameVersion: string;

View File

@ -1,6 +1,20 @@
import type { PageServerLoad } from './$types';
import { fetchRooms } from '$lib/server/yukiotoko';
import { arrayPadEnd } from '$lib/array';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
return await fetchRooms();
const { activeRooms, archivedRooms } = await fetchRooms();
archivedRooms.forEach((archivedRoom) =>
arrayPadEnd(archivedRoom.players, 4, {
name: 'CPU',
battleRank: 0,
rating: 0
})
);
return {
activeRooms,
archivedRooms
};
};

View File

@ -1,25 +1,20 @@
<script lang="ts">
import { arrayPadEnd } from '$lib/array';
import Pagination from '$lib/Pagination.svelte';
import Room from '$lib/Room.svelte';
import type { PageData } from './$types';
let props: { data: PageData } = $props();
let { data }: { data: PageData } = $props();
let { activeRooms, archivedRooms } = data;
let data = $state(props.data);
data.archivedRooms.forEach((archivedRoom) =>
arrayPadEnd(archivedRoom.players, 4, {
name: 'CPU',
battleRank: 0,
rating: 0
})
);
let paginationArchivedRooms = $state([]);
</script>
<div class="mb-16 space-y-8">
<div class="mb-8 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>
<h1 class="text-4xl font-bold text-light">yukiotoko webui</h1>
<p>
a frontend redesign for <a href="http://yukiotoko.chara.lol/">yukiotoko</a> made by metamethods
</p>
<p class="text-sub">refresh the page to fetch newer yukiotoko data</p>
</section>
</div>
@ -27,14 +22,14 @@
<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 class="text-2xl font-bold text-light">
active rooms <span class="text-sub">({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}
{#each activeRooms as activeRoom}
<Room room={activeRoom} />
{/each}
</div>
@ -42,14 +37,16 @@
<section class="flex flex-col gap-4">
<div>
<h1 class="text-2xl font-bold">
<h1 class="text-2xl font-bold text-light">
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>
<Pagination items={archivedRooms} itemsPerPage={20} bind:pageItems={paginationArchivedRooms} />
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
{#each data.archivedRooms as archivedRoom}
{#each paginationArchivedRooms as archivedRoom}
<Room room={archivedRoom} />
{/each}
</div>

View File

@ -0,0 +1,10 @@
import { fetchRoom } from '$lib/server/yukiotoko';
import { error, json } from '@sveltejs/kit';
export async function GET({ params }) {
const room = await fetchRoom(params.roomId);
if (!room) return error(404);
return json(room);
}

View File

@ -7,11 +7,12 @@ export default {
theme: {
extend: {
colors: {
dark: '#060710',
medium: '#0A0B17',
dark: '#111117',
medium: '#1F2128',
light: '#CFD3FA',
sub: '#4D4D80',
accent: '#7E61FF'
text: '#9B9FC0',
accent: '#8D74FF',
border: '#353741'
},
fontFamily: {